Order Messenger for WooCommerce

Developers documentation

This reference is for developers extending Order Messenger: template overrides, styling, hooks, the REST API and the PHP services. For setup and day-to-day use see the user documentation. Everything below applies to version 4 of the plugin; both the free and the premium version share this code.

Architecture at a glance

The plugin lives in the U2Code\OrderMessenger namespace. Services are registered in a container and reached through typed getters:

use U2Code\OrderMessenger\Core\ServiceContainer;

$container = ServiceContainer::getInstance();

$container->getMessageService();     // write messages
$container->getMessageRepository();  // read and query messages
$container->getAttachments();        // uploads and the download endpoint
$container->getNotifications();      // email notifications
$container->getScheduler();          // Action Scheduler jobs
$container->getPermissions();        // who may do what
$container->getLicense();            // free / premium
$container->getSettings();           // the WooCommerce settings tab

Messages are rows of the {prefix}wc_order_messages table. Dates are stored in UTC.

ColumnMeaning
idMessage id.
order_idThe order the conversation belongs to.
user_idThe order’s customer id (0 for guests).
sender_idThe user who wrote the message; 0 for system messages.
attachment_idMedia library id of the attachment, or NULL.
messagePlain text, up to 3000 characters.
type0 team, 1 customer, 2 order note, 3 status change, 4 scheduled (waiting), 5 scheduled (sent).
date_sent / date_readUTC timestamps; date_read is NULL while unread.
is_notified1 once an email went out (or none was needed).
dataJSON: scheduled_date, notify, from/to for status changes, note_id, purchase_product_id.

Sending and reading messages from code

use U2Code\OrderMessenger\Core\ServiceContainer;
use U2Code\OrderMessenger\Entity\MessageType;
use U2Code\OrderMessenger\Services\MessageException;

$container = ServiceContainer::getInstance();
$order     = wc_get_order( 123 );

try {
    // From the store to the customer. Sender 0 shows the store signature.
    $container->getMessageService()->sendAdminMessage( $order, get_current_user_id(), 'Your parcel ships tomorrow.', array(
        'notify'        => true,                         // email the customer
        'attachment_id' => 0,                            // a media library id
        'schedule_at'   => new DateTimeImmutable( '+2 days 10:00', wp_timezone() ), // omit to send now
    ) );

    // From the customer (permissions are checked: the customer, or a guest with the order key).
    $container->getMessageService()->sendCustomerMessage( $order, $order->get_customer_id(), 'Thanks!' );

    // A system entry, for example from a shipping plugin.
    $container->getMessageService()->createSystemMessage( $order, MessageType::service(), 'Shipped with DHL, tracking 123456.', array( 'tracking' => '123456' ) );
} catch ( MessageException $e ) {
    // $e->getCode() is the HTTP status the REST API would answer with (400, 403, 429).
    error_log( $e->getMessage() );
}

// Reading.
$repository = $container->getMessageRepository();
$messages   = $repository->getForOrder( $order->get_id(), array( 'context' => 'admin', 'limit' => 50 ) );
$unread     = $repository->countUnreadForOrder( $order->get_id(), 'admin' );   // waiting for the team
$repository->markReadForOrder( $order->get_id(), 'admin' );

foreach ( $messages as $message ) {
    $message->getMessage();                    // text
    $message->getMessageType()->isCustomer();  // who wrote it
    $message->getDateSent();                   // DateTimeImmutable in the site timezone
    $message->getAttachment();                 // MessageAttachment or null
}

context is admin for the team’s view and customer for the customer’s; it decides which message types are included and whose unread state is meant.

Template overrides

Storefront templates live in the plugin’s views/frontend folder and can be copied into your theme under order-messenger-for-woocommerce/, keeping the path below frontend/:

your-theme/order-messenger-for-woocommerce/my-account/messenger.php
your-theme/order-messenger-for-woocommerce/my-account/dialogs.php
your-theme/order-messenger-for-woocommerce/my-account/no-access.php
your-theme/order-messenger-for-woocommerce/my-account/no-conversations.php
your-theme/order-messenger-for-woocommerce/message/message-attachment.php
your-theme/order-messenger-for-woocommerce/message/message-types/admin-message.php
your-theme/order-messenger-for-woocommerce/message/message-types/customer-message.php
your-theme/order-messenger-for-woocommerce/message/message-types/service-message.php
your-theme/order-messenger-for-woocommerce/message/message-types/order-status-message.php
your-theme/order-messenger-for-woocommerce/message/message-types/scheduled-message.php

The conversation template receives $order, $orderId, $orderKey, $messages (oldest first), $totalMessages, $hasMore, $canSend, $filesEnabled, $config (the data attributes the script reads) and $presenter. Message templates receive $message, $order and $orderKey. Keep the data-om-* attributes and the om-* classes if you want the script to keep working: it sends messages, polls for new ones and opens the lightbox through them.

The notification emails are regular WooCommerce templates. Copy them from the plugin’s views/emails folder to your-theme/woocommerce/emails/: admin-messenger-notification.php, customer-messenger-notification.php, partials/messages.php and the plain/ variants.

The lookup can be redirected with the order_messenger/template/location filter (arguments: resolved file, template name).

Styling

The customer-side conversation is intentionally theme-neutral: it inherits the page font, mixes its lines and fills from the current text color and uses the theme’s own inputs and buttons. The named palettes set these custom properties on .om-messenger and .om-conversations; you can set them yourself for a custom look:

.om-messenger {
    --om-primary-color: #2b6cb0;          /* store bubbles */
    --om-primary-color-darker: #245a93;
    --om-primary-text-color: #ffffff;
    --om-secondary-color: #f1f5f9;        /* customer bubbles */
    --om-secondary-color-darker: #e2e8f0;
    --om-secondary-text-color: #0f172a;
}

Useful classes: .om-messenger (the whole widget), .om-message with --customer, --admin, --system, --own modifiers, .om-message__bubble, .om-message__meta, .om-attachment, .om-messenger__composer, .om-badge and .om-nav-badge (unread counts), .om-conversations (the My Account list). Theme-specific rules can be added with the order_messenger/frontend/theme_integration_css filter, which is how the plugin gives Storefront a chat icon in the account menu.

Hooks

All hook names start with order_messenger/. The most useful ones are listed here; grep the plugin for the complete set.

Actions

ActionArgumentsWhen
messages/message_createdMessage $messageA message was stored (any type).
messages/message_updatedMessage $messageA message changed (read state, publish).
messages/message_deletedMessage $messageAfter a message was deleted; messages/before_delete fires before.
messages/changed?Message $message, int $orderIdAnything changed in an order’s conversation; the unread counters listen here.
messages/scheduled_message_publishedMessage $published, Message $scheduledA scheduled message went out.
notifications/sentint $orderId, Message[] $messages, string $side, bool $sentA notification email was attempted; $side is admin or customer.
attachments/uploadedint $attachmentId, WC_Order $order, array $resultA customer upload was stored.
admin/product_page/messenger_tab_begin / _endint $productIdAdd fields to the product’s Order Messenger tab.
upgradedstring $from, string $toAfter a data migration.
rest/register_routesstring $namespaceRegister your own routes next to the plugin’s.

Permissions

FilterArgumentsPurpose
permissions/capabilitystring $capabilityThe capability that marks team members (default manage_order_messenger).
permissions/can_managebool $allowed, int $userIdWhether a user is part of the team.
permissions/userCanViewMessengerbool $allowed, WC_Order $order, int $userIdWhether a customer may open a conversation.
permissions/userCanSendMessagebool $allowed, int $userId, WC_Order $order, string $textWhether a customer may write.
permissions/userCanSendAdminMessagebool $allowed, int $userId, WC_Order $orderWhether a team member may write to this order.
permissions/userCanDeleteMessage / userCanUnreadMessagebool $allowed, Message $messageMessage actions.
settings/allowed_roles_to_manage_admin_messenger/defaultstring[] $rolesDefault team roles.

Messages and content

FilterArgumentsPurpose
messages/before_saving_customer_messageMessage $message, WC_Order $order, ?array $fileChange a customer message before it is stored.
messages/before_saving_admin_messageMessage $message, $request, array $customData, WC_Order $order, int $attachmentIdChange a team message before it is stored.
messages/before_saving_system_messageMessage $message, WC_Order $orderChange an automatic entry.
messages/before_publishing_scheduled_messageMessage $published, Message $scheduledChange a scheduled message as it goes out.
message/sender_namestring $name, Message $messageThe name shown for a message.
message/formatted_textstring $html, ?string $textThe rendered text (links, line breaks).
message/message_template_pagestring $path, string $place, Message $messageThe template used for a message.
system_messages/post_order_notebool $post, $note, WC_Order $orderSkip specific order notes.
config/rate_limitarray{limit:int, window:int}Customer messages per window (30 per 10 minutes by default).
notification/email_delayint $minutesThe digest delay.
config/message_types_customer_should_be_notified / _admin_int[] $typesWhich message types trigger emails for each side.

Settings, templates and storefront

FilterArgumentsPurpose
settings/sectionsSectionAbstract[] $sectionsAdd a settings section.
settings/settingsarray $fields, SectionAbstract $sectionChange the fields of a section.
settings/available_themesarray $palettesAdd a color palette (name → custom properties).
settings/available_file_typesarray $typesAdd an attachment type (key → title, mime, ext).
config/get_{key}mixed $value, array $argsEvery setting passes through a filter named after it, e.g. config/get_polling_interval.
template/locationstring $file, string $templateWhere a template is loaded from.
frontend/conversation_configarray $config, WC_Order $orderThe data attributes the storefront script reads.
frontend/show_on_order_receivedbool $show, WC_Order $orderShow the conversation on the order confirmation page.
frontend/theme_integration_cssstring $cssExtra CSS printed with the storefront stylesheet.
order/customer_name, order/summarystring|array, WC_Order $orderHow an order is described in the admin app and emails.
attachments/sideloadbool $sideload, array $file, ?WC_Order $orderAccept files that are not browser uploads (imports, tests).
rest/featuresarray $features, WC_Order $order, bool $isManagerWhat the composer offers for an order and viewer.
rest/messagearray $data, Message $message, string $contextThe JSON shape of a message.
license/can_use_premium_codebool $premiumPremium features on or off. Always true in the WooCommerce.com version.

Examples

// Post a message when an order is completed.
add_action( 'woocommerce_order_status_completed', function ( $order_id ) {
    $order = wc_get_order( $order_id );
    \U2Code\OrderMessenger\Core\ServiceContainer::getInstance()->getMessageService()
        ->sendAdminMessage( $order, 0, 'Your order is complete. Thank you for shopping with us!', array( 'notify' => true ) );
} );

// Show a role name instead of the team member's name.
add_filter( 'order_messenger/message/sender_name', function ( $name, $message ) {
    return $message->getMessageType()->isFromStore() ? 'Support team' : $name;
}, 10, 2 );

// Allow SVG attachments.
add_filter( 'order_messenger/settings/available_file_types', function ( $types ) {
    $types['svg'] = array( 'title' => 'SVG', 'mime' => 'image/svg+xml', 'ext' => 'svg' );
    return $types;
} );

// Keep the conversation off the order confirmation page.
add_filter( 'order_messenger/frontend/show_on_order_received', '__return_false' );

// Let customers write only while the order is not completed.
add_filter( 'order_messenger/permissions/userCanSendMessage', function ( $allowed, $user_id, $order ) {
    return $allowed && ! $order->has_status( 'completed' );
}, 10, 3 );

REST API

Namespace order-messenger/v1. Requests from the site itself authenticate with the WordPress cookie and an X-WP-Nonce header (wp_rest nonce); external clients use application passwords. Customers may only reach their own orders; guests pass the order key as key. Team routes require the manage_order_messenger capability.

RouteMethodParameters
/orders/{id}GETkey (guests). Returns the order summary, totals, unread count and the features the composer may offer.
/orders/{id}/messagesGETbefore_id, after_id, per_page (≤ 200), render = json or html. Newest page first; after_id is what live updates poll with.
/orders/{id}/messagesPOSTmessage, file (multipart, customers), attachment_id (team), notify, schedule_at (Y-m-d\TH:i:s, site time), custom_data, render. Answers 201 with the message.
/orders/{id}/readPOSTMark the viewer’s side of the conversation as read.
/messages/{id}DELETETeam only.
/messages/{id}/unreadPOSTTeam only.
/conversationsGETTeam only. search, unread_only, page, per_page (≤ 100).
/unread-countGETTeam only. Unread customer messages across all orders.
// From a page on the same site.
const response = await fetch( wpApiSettings.root + 'order-messenger/v1/orders/123/messages', {
    method: 'POST',
    credentials: 'same-origin',
    headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': wpApiSettings.nonce },
    body: JSON.stringify( { message: 'Hello from the storefront' } ),
} );
const { message } = await response.json();

The routes of version 3 (/get/{limit}/{offset}, /send, /sendAdmin, /delete/{id}, /unread/{id}) still answer with their old shapes for integrations that rely on them.

Background jobs

Scheduled messages and email digests run on WooCommerce’s Action Scheduler in the order-messenger group:

HookSchedule
order_messenger/publish_scheduled_messageOne action per scheduled message, at its time; argument message_id.
order_messenger/publish_due_scheduled_messagesEvery 10 minutes, publishes anything that was missed (scheduler/catch_up_interval filter).
order_messenger/send_notification_digestsEvery few minutes while digests are enabled (notifications/digest_interval filter).

Attachments

Customer uploads go through wp_handle_upload() with the allowed types from the settings, are renamed to random names, stored under uploads/order-messenger/ behind an .htaccess deny rule, and registered as private media library items linked to the order. Team attachments are ordinary media library files. Every file is served by admin-post.php?action=om_attachment, which checks that the viewer belongs to the conversation (customer, guest with the order key, or team) and streams the file with a nonce that is tied to the attachment. Use AttachmentService::getDownloadUrl( $attachment, $size, $orderKey ) to build such a link, and the attachments/sideload filter to feed files that are not browser uploads.

Shortcodes and endpoints

ItemDetails
[order_chatroom order_id=""]One conversation; falls back to the view-order or messenger query var.
[order_chatrooms limit="10" current_page="1"]The current customer’s conversations.
[order_chatroom_unread_messages_count order_id=""]Unread count for an order, or for the customer when empty.
My Account endpointsmessages (the list) and messenger/{order-id} (one conversation), registered through woocommerce_get_query_vars.
Admin pageadmin.php?page=om-messenger&order={id} opens a conversation on the Messenger page.

JavaScript

The admin app is a React application on @wordpress/components; its configuration is window.omMessengerConfig (filterable through admin/app_config). The storefront script is dependency-free: each [data-om-messenger] element gets a Conversation instance available as element.omConversation, with poll(), loadOlder() and send(). window.omFrontendConfig carries the polling interval and the unread count for the account menu badge.

Free and premium

The WordPress.org version ships the Freemius SDK; the WooCommerce.com version does not. LicenseManager::canUsePremiumCode() is the only place that asks the SDK, and the license/can_use_premium_code filter sits on top of it. Attachments are the premium feature: on the free plan customer uploads are refused with a 403 and the team composer shows a locked control; everything else works identically in both versions.

Something missing?

Ask us and we will answer within one working day, and improve the docs while we are at it.