Skip to content

Integrations

Automation events

Report what happened in a store, site or system, and decide in the console which message follows: channel, text or template, language, timing and fallback.

How it works#

A plugin, a platform webhook or your own code reports an event, such as an order being paid, in one normalised shape. It does not carry a message text. What is sent is defined in the OmniMessage console as automations, so changing a text never needs a new plugin version and every platform gets the same editor.

  1. The event arrives at POST /v1/automation_events, or as a native WooCommerce or Shopify webhook at POST /v1/ingest/{platform}/{source_id}.
  2. It is stored as a receipt and deduplicated on its id.
  3. The enabled automations of the source for that event type are matched, in the order the console shows them, against their conditions.
  4. For each match the recipient and the consent are checked and a run is scheduled: occurred_at plus the delay, moved out of quiet hours.
  5. When the run is due, the message is rendered for the customer’s language and sent through the normal message pipeline: billing, refunds on failure and message.* webhooks work as for POST /v1/messages.
  6. If the message fails, or is not delivered within the automation’s timeout, the fallback channel is tried once.

Acceptance never depends on the balance. An event is accepted with an empty wallet; the run then fails with insufficient_balance and the account gets the usual balance.low signal.

Integration sources#

An integration source is one connected store, site or system. Every event belongs to a source, and automations are defined per source. Sources belong to a mode: a test source sends through sandbox channels only. An account can have up to 50 sources per mode.

Integration source
{
  "id": "src_9Kd2mQ5vB8cX1zL0pK3j",
  "object": "integration_source",
  "mode": "live",
  "platform": "woocommerce",
  "external_id": "shop.example.com",
  "name": "Acme Shop",
  "url": "https://shop.example.com/",
  "status": "active",
  "plugin_version": "0.1.0",
  "platform_version": "woocommerce/10.1 wordpress/7.0",
  "emits": [
    "order.created",
    "order.paid",
    "order.shipped",
    "cart.abandoned"
  ],
  "enabled_events": [
    "order.paid",
    "order.shipped"
  ],
  "event_settings": {
    "cart.abandoned": {
      "delay_seconds": 3600
    }
  },
  "console_url": "https://omnimessage.co/console/integrations/src_9Kd2mQ5vB8cX1zL0pK3j/automations",
  "automation_url_template": "https://omnimessage.co/console/integrations/src_9Kd2mQ5vB8cX1zL0pK3j/automations/{event_type}",
  "ingest_urls": {
    "woocommerce": "https://api.omnimessage.co/v1/ingest/woocommerce/src_9Kd2mQ5vB8cX1zL0pK3j",
    "shopify": "https://api.omnimessage.co/v1/ingest/shopify/src_9Kd2mQ5vB8cX1zL0pK3j"
  },
  "config_version": 12,
  "last_event_at": "2026-10-05T09:30:00.000Z",
  "created_at": "2026-10-01T08:00:00.000Z"
}
FieldDescription
platformwoocommerce, wordpress, shopify, salla, zid, ikas, ticimax or custom, or any other lower-case slug of up to 32 characters.
external_idThe platform’s stable identifier of the store, such as the host name or shop domain, up to 255 characters. (mode, platform, external_id) is unique per account.
emitsThe event types the plugin can send. Informational: the console suggests automations for them.
enabled_eventsThe event types a plugin should send: those with at least one enabled automation, plus the types those automations are cancelled by. ["*"] when the source is set to record every event.
event_settingsHints for plugins, keyed by event type. Today: cart.abandoned.delay_seconds, the shortest delay of the enabled abandoned-cart automations.
config_versionIncreases whenever enabled_events or event_settings change. It is also the ETag of the source.
console_url, automation_url_templateWhere a plugin sends its user: the automations of the source, and the editor of one event type ({event_type} replaced).
ingest_urlsThe delivery URLs for native WooCommerce and Shopify webhooks.
source_key, signing_secretReturned only when the source is created and when its key is rolled. See below.

Authentication and scopes#

Every source has two secrets of its own. The source key (om_src_…) is a bearer token that can only push events for that source and read or describe it: a key that leaks from a shop cannot send free-form messages, read messages or touch billing. The signing secret (isec_…) verifies native platform webhooks. Both are shown once and replaced together with POST /v1/integration_sources/{id}/roll_key; the old ones stop working at once.

CredentialMay call
Source key om_src_…POST /v1/automation_events, POST /v1/automation_events/batch, GET and PATCH /v1/integration_sources/current. Anything else is 403 scope_missing.
API key with integrations:write / integrations:readRegister, list, read, update, roll and delete sources.
API key with events:writePush events. The body must then name the source: "source": "src_…".
API key with events:readList and read event receipts.
Platform signaturePOST /v1/ingest/{platform}/{source_id} only.
  • An unknown source key is 401 api_key_invalid; a disabled source is 403 source_disabled; a suspended account is 403 account_suspended.
  • With a source key the body must not carry source. With an API key it must (400 parameter_missing), and the source must be in the mode of the key.
  • An API key cannot call the /current endpoints: they are for source keys only.
  • GET /v1/me lists automation_events in capabilities. Plugins check that list, not the endpoints, before offering automation events.

Endpoints#

Method and pathAuthPurpose
POST /v1/integration_sourcesintegrations:writeRegister or refresh a source. Upsert on (mode, platform, external_id): 201 with the secrets when created, 200 without them when it existed (name, URL, versions and emits are updated).
GET /v1/integration_sourcesintegrations:readList the sources of the key mode.
GET /v1/integration_sources/{id}integrations:readRead one source, with ETag.
GET /v1/integration_sources/currentSource keyThe source of the key. Sends ETag: "<config_version>" and answers 304 to a matching If-None-Match.
PATCH /v1/integration_sources/currentSource keyUpdate name, url, plugin_version, platform_version, emits.
PATCH /v1/integration_sources/{id}integrations:writeThe same fields, plus status (active or disabled), signing_secret and settings (contact_sync, consent_mode, store_all_events).
POST /v1/integration_sources/{id}/roll_keyintegrations:writeNew source key and signing secret.
DELETE /v1/integration_sources/{id}integrations:writeDelete the source with its automations and receipts; scheduled runs are cancelled. Messages already sent stay.
POST /v1/automation_eventsSource key or events:writePush one event.
POST /v1/automation_events/batchSource key or events:writePush up to 100 events.
GET /v1/automation_eventsevents:readList receipts, newest first.
GET /v1/automation_events/{id}events:readOne receipt with its runs.
POST /v1/ingest/{platform}/{source_id}Platform signatureNative WooCommerce and Shopify webhooks.

Every operation, with its parameters and responses, is in the API reference under Integration sources and Automation events.

Register a source#

In the console, Integrations creates sources for WooCommerce and Shopify webhooks and for your own code. A plugin that holds an API key registers itself instead, once, when the user connects the site. It stores the id and the source_key and uses only the source key from then on. Registering again is safe and is how a plugin reports a new version or new emits.

curl https://api.omnimessage.co/v1/integration_sources \
  -H "Authorization: Bearer om_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2a0e-3b7d-4c59-9f0a-2d8e5b1c7a43" \
  -d '{
    "platform": "woocommerce",
    "external_id": "shop.example.com",
    "name": "Acme Shop",
    "url": "https://shop.example.com/",
    "plugin_version": "0.1.0",
    "platform_version": "woocommerce/10.1 wordpress/7.0",
    "emits": [
      "order.created",
      "order.paid",
      "order.shipped",
      "cart.abandoned"
    ]
  }'
201 Created
{
  "id": "src_9Kd2mQ5vB8cX1zL0pK3j",
  "object": "integration_source",
  "mode": "live",
  "platform": "woocommerce",
  "external_id": "shop.example.com",
  "name": "Acme Shop",
  "url": "https://shop.example.com/",
  "status": "active",
  "plugin_version": "0.1.0",
  "platform_version": "woocommerce/10.1 wordpress/7.0",
  "emits": [
    "order.created",
    "order.paid",
    "order.shipped",
    "cart.abandoned"
  ],
  "enabled_events": [],
  "event_settings": {},
  "console_url": "https://omnimessage.co/console/integrations/src_9Kd2mQ5vB8cX1zL0pK3j/automations",
  "automation_url_template": "https://omnimessage.co/console/integrations/src_9Kd2mQ5vB8cX1zL0pK3j/automations/{event_type}",
  "ingest_urls": {
    "woocommerce": "https://api.omnimessage.co/v1/ingest/woocommerce/src_9Kd2mQ5vB8cX1zL0pK3j",
    "shopify": "https://api.omnimessage.co/v1/ingest/shopify/src_9Kd2mQ5vB8cX1zL0pK3j"
  },
  "config_version": 1,
  "last_event_at": null,
  "created_at": "2026-10-01T08:00:00.000Z",
  "source_key": "om_src_Zk8vQ2mX5cB7nL0pR3tY6wA9dF1gH4jK",
  "signing_secret": "isec_N7bT4xK1mQ8wE5rY2uI9oP3aS6dF0gHjZk8vQ2mX"
}

Push an event#

With the source key, the event is the whole body:

Push an event with a source key
curl https://api.omnimessage.co/v1/automation_events \
  -H "Authorization: Bearer om_src_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "woocommerce:order:5012:order.paid:1",
    "type": "order.paid",
    "occurred_at": "2026-10-05T09:30:00Z",
    "customer": { "first_name": "Layla", "phone": "+971501234567", "locale": "en" },
    "order": { "id": "5012", "number": "1042", "total": { "amount_minor": 12550, "currency": "AED", "formatted": "AED 125.50" } }
  }'

With an API key that has events:write, add source. This is the full payload of the example in the OpenAPI document:

curl https://api.omnimessage.co/v1/automation_events \
  -H "Authorization: Bearer om_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "woocommerce:order:5012:order.paid:1",
    "type": "order.paid",
    "occurred_at": "2026-10-05T09:30:00Z",
    "site": {
      "name": "Acme Shop",
      "url": "https://shop.example.com/"
    },
    "customer": {
      "id": "17",
      "name": "Layla Hassan",
      "first_name": "Layla",
      "last_name": "Hassan",
      "phone": "+971501234567",
      "email": "layla@example.com",
      "locale": "en",
      "country": "AE",
      "consent": {
        "transactional": true,
        "marketing": true,
        "source": "checkout_checkbox",
        "collected_at": "2026-10-05T09:29:40Z"
      }
    },
    "order": {
      "id": "5012",
      "number": "1042",
      "status": "processing",
      "previous_status": "pending",
      "currency": "AED",
      "total": {
        "amount_minor": 12550,
        "currency": "AED",
        "formatted": "AED 125.50"
      },
      "subtotal": {
        "amount_minor": 8000,
        "currency": "AED",
        "formatted": "AED 80.00"
      },
      "shipping_total": {
        "amount_minor": 1500,
        "currency": "AED",
        "formatted": "AED 15.00"
      },
      "discount_total": {
        "amount_minor": 0,
        "currency": "AED",
        "formatted": "AED 0.00"
      },
      "items": [
        {
          "id": "11",
          "name": "Mug",
          "sku": "MUG-1",
          "quantity": 2,
          "unit_price": {
            "amount_minor": 4000,
            "currency": "AED",
            "formatted": "AED 40.00"
          },
          "url": "https://shop.example.com/mug"
        }
      ],
      "items_count": 2,
      "items_summary": "2 × Mug",
      "payment_method": "cod",
      "payment_method_title": "Cash on delivery",
      "shipping_method": "Flat rate",
      "tracking": {
        "number": "",
        "url": "",
        "carrier": ""
      },
      "status_url": "https://shop.example.com/my-account/view-order/5012/",
      "note": "",
      "created_at": "2026-10-05T09:29:41Z"
    },
    "data": {},
    "source": "src_9Kd2mQ5vB8cX1zL0pK3j"
  }'
202 Accepted
{
  "id": "aev_3kL9pQ2wE5rT8yU1iO4a",
  "object": "automation_event",
  "mode": "live",
  "source_id": "src_9Kd2mQ5vB8cX1zL0pK3j",
  "event_id": "woocommerce:order:5012:order.paid:1",
  "type": "order.paid",
  "status": "accepted",
  "reason": null,
  "automations_matched": 1,
  "received_at": "2026-10-05T09:30:00.120Z"
}
  • The answer is 202 with the receipt. status is accepted when at least one message is scheduled, otherwise ignored with a reason.
  • Sending the same id again answers 200 with the first receipt, status: "duplicate" and the header Idempotent-Replayed: true, whatever the body. Platforms and queues retry the same occurrence with bodies that are not byte-identical, so the event id is the idempotency key; an Idempotency-Key header is accepted and ignored on this endpoint.
  • Receipts, and with them the deduplication window, are kept for 30 days.
  • Retry only transport errors, 429 and 5xx. A 4xx will be refused again.

Batches#

POST /v1/automation_events/batch takes { "events": [ … ] } with 1 to 100 events. They are processed in order, so a later event can cancel what an earlier one scheduled. Each item is validated and deduplicated on its own; with an API key each item names its source. The batch counts as one request for rate limiting and is answered with 207:

207 Multi-Status
{
  "object": "batch",
  "data": [
    {
      "index": 0,
      "status": 202,
      "event": {
        "id": "aev_3kL9pQ2wE5rT8yU1iO4a",
        "object": "automation_event",
        "mode": "live",
        "source_id": "src_9Kd2mQ5vB8cX1zL0pK3j",
        "event_id": "woocommerce:order:5012:order.paid:1",
        "type": "order.paid",
        "status": "accepted",
        "reason": null,
        "automations_matched": 1,
        "received_at": "2026-10-05T09:30:00.120Z"
      }
    },
    {
      "index": 1,
      "status": 400,
      "error": {
        "type": "invalid_request_error",
        "code": "parameter_invalid",
        "message": "occurred_at is more than 10 minutes in the future.",
        "param": "events.1.occurred_at",
        "request_id": "req_0aB3cD6eF9gH2iJ5kL8m",
        "doc_url": "https://omnimessage.co/docs/errors#parameter_invalid"
      }
    }
  ],
  "accepted": 1,
  "rejected": 1
}

The event payload#

FieldRules
id (required)String of 1 to 255 characters, unique per source for one real-world occurrence. Build it from stable parts, such as <platform>:<object>:<object id>:<type>:<sequence>, never from the time of the send attempt.
type (required)One of the types below, or custom.<name>.
occurred_at (required)ISO-8601 time at which it happened at the source. Delays are measured from it. Older than 7 days: accepted but ignored with stale. More than 10 minutes in the future: 400 parameter_invalid.
sourceThe src_… ID. Required with an API key, forbidden with a source key.
testtrue marks a test send: automations run without delay and without cancelling anything, contacts are not changed, and the run is left out of the statistics.
sitename and url.
customerWho the event concerns. Optional for events that only notify the account owner.
order, cart, form, appointment, otp, user, commentThe block that matches the event family.
dataFree-form extras up to 32 KB, available as {{data.*}}.

Event types#

TypeBlockTypical trigger
order.createdorderOrder lifecycle
order.paidorderOrder lifecycle
order.shippedorderOrder lifecycle
order.deliveredorderOrder lifecycle
order.cancelledorderOrder lifecycle
order.refundedorderOrder lifecycle
order.status_changedorder (status, previous_status)Order lifecycle
order.note_addedorder (note)Order lifecycle
cart.abandonedcartCheckout started, no order followed
customer.createdcustomerA new customer
user.registereduserAccounts on the site
user.password_reset_requesteduserAccounts on the site
form.submittedformContact and lead forms
appointment.bookedappointmentScheduling
appointment.rescheduledappointmentScheduling
appointment.cancelledappointmentScheduling
appointment.reminder_dueappointmentScheduling
otp.requestedotpOne-time passwords
comment.approvedcommentBlog comments
custom.<name>dataAnything else. The name uses lower-case letters, digits, dots and underscores, up to 64 characters.

Blocks#

Every field is optional unless marked. Unknown fields are kept and can be used in merge tags.

  • customer: id, name, first_name, last_name, phone, phone_raw, email, locale (BCP 47), country (ISO 3166-1 alpha-2), consent. phone should be E.164; otherwise phone_raw (or phone) is read as a number of customer.country, or of the account country.
  • customer.consent: transactional and marketing are true, false or null (unknown); channels maps a channel type to opted_in, opted_out or unknown and wins over the two flags for that channel; source and collected_at describe where the consent came from.
  • Money is always { "amount_minor": <integer>, "currency": "<ISO 4217>", "formatted": "<as the shop shows it>" }. amount_minor uses the exponent of the currency: 0 for JPY, 3 for KWD, BHD, OMR, JOD and TND, otherwise 2. No floats.
  • order: id (required), number, status, previous_status, currency, total, subtotal, shipping_total, discount_total, items (id, name, sku, quantity, unit_price, url, image_url; up to 200), items_count, items_summary, payment_method, payment_method_title, shipping_method, tracking (number, url, carrier), status_url, note, created_at.
  • cart: id (required), currency, total, items, items_count, items_summary, recovery_url, updated_at.
  • form: plugin, id, name, fields (label to value), fields_summary, page_url.
  • appointment: id (required), service, starts_at, ends_at, timezone, location, staff, manage_url.
  • otp: code (required), expires_in_seconds, purpose.
  • user: id, login, reset_url.
  • comment: id, post_title, post_url, excerpt.

IDs inside the blocks may be sent as numbers; they are stored as strings. A one-time code (otp.code) and a reset link (user.reset_url) are encrypted while a run still needs them, removed once it was sent, and always masked in the console and the API.

Sample payloads#

The console previews automations against these samples until the source has sent a real event of the type. They are also the request examples of the OpenAPI document.

{
  "id": "sample:order.paid",
  "type": "order.paid",
  "occurred_at": "2026-10-05T09:30:00Z",
  "site": {
    "name": "Acme Shop",
    "url": "https://shop.example.com/"
  },
  "customer": {
    "id": "17",
    "name": "Layla Hassan",
    "first_name": "Layla",
    "last_name": "Hassan",
    "phone": "+971501234567",
    "email": "layla@example.com",
    "locale": "en",
    "country": "AE",
    "consent": {
      "transactional": true,
      "marketing": true,
      "source": "checkout_checkbox",
      "collected_at": "2026-10-05T09:29:40Z"
    }
  },
  "order": {
    "id": "5012",
    "number": "1042",
    "status": "processing",
    "previous_status": "pending",
    "currency": "AED",
    "total": {
      "amount_minor": 12550,
      "currency": "AED",
      "formatted": "AED 125.50"
    },
    "subtotal": {
      "amount_minor": 8000,
      "currency": "AED",
      "formatted": "AED 80.00"
    },
    "shipping_total": {
      "amount_minor": 1500,
      "currency": "AED",
      "formatted": "AED 15.00"
    },
    "discount_total": {
      "amount_minor": 0,
      "currency": "AED",
      "formatted": "AED 0.00"
    },
    "items": [
      {
        "id": "11",
        "name": "Mug",
        "sku": "MUG-1",
        "quantity": 2,
        "unit_price": {
          "amount_minor": 4000,
          "currency": "AED",
          "formatted": "AED 40.00"
        },
        "url": "https://shop.example.com/mug"
      }
    ],
    "items_count": 2,
    "items_summary": "2 × Mug",
    "payment_method": "cod",
    "payment_method_title": "Cash on delivery",
    "shipping_method": "Flat rate",
    "tracking": {
      "number": "",
      "url": "",
      "carrier": ""
    },
    "status_url": "https://shop.example.com/my-account/view-order/5012/",
    "note": "",
    "created_at": "2026-10-05T09:29:41Z"
  },
  "data": {}
}

Processing rules#

Matching#

The enabled automations of the source for the event type are taken in the order the console shows them. Each can carry up to 10 conditions on payload fields, all of which must hold: field, an operator (eq, neq, in, gt, lt, exists, not_exists) and a value, for example order.payment_method eq cod or order.total.amount_minor gt 50000. Several automations can answer the same event.

No enabled automation for the type gives ignored with event_not_enabled; automations whose conditions all failed give no_automation_matched.

Recipient#

  • Audience customer (default): the customer’s phone number, or the contact’s address on the automation’s channel. If there is none and the automation has a fallback channel, the fallback channel is used directly.
  • Audience owner: the automation’s owner numbers plus data.admin_recipients of the event (a list, or a string separated by commas, semicolons or line breaks), up to 10 numbers.
  • Nobody to send to: the run is skipped with no_recipient.

Each automation is transactional or marketing. The console preselects marketing for cart.abandoned and custom.*, transactional for everything else. The source’s consent mode, set in its settings, decides how transactional messages are treated.

SituationResult
Audience owner, or otp.requested / user.password_reset_requestedNo consent check.
The contact is blocked or unsubscribed from the channelNever messaged: unsubscribed. Checked again just before sending.
customer.consent.channels.<type> is opted_out / opted_inconsent_missing / sent.
Marketing automationNeeds consent.marketing: true, or a stored opt-in on the contact.
Transactional, consent mode "implied" (default)Sent unless consent.transactional is false.
Transactional, consent mode "explicit"Needs consent.transactional: true, or a stored opt-in on the contact.

Contact sync is on by default: a customer with a phone number becomes or updates a contact, tagged source:<platform>. A stated marketing opt-in is stored as opted_in for WhatsApp and SMS, and a per-channel opted_out is stored as an unsubscribe. A declined marketing checkbox is not an unsubscribe, and an opt-in from a shop never undoes a STOP the customer sent. Test events never change contacts.

Timing#

  • The run is due at occurred_at plus the automation’s delay (up to 30 days); if that is in the past it is due now. Test events are due now.
  • Quiet hours, per automation, move the send to the next allowed minute in the customer’s time zone (from the contact or customer.country), otherwise in the account’s. Owner messages use the account’s time zone. One-time codes and password resets ignore quiet hours.
  • Each automation lists cancel_on event types. A scheduled run is cancelled when such an event arrives from the same source for the same customer (phone, otherwise email) or, for carts, with the same cart.id or data.cart_id. The default for cart.abandoned is order.created and order.paid.
  • A newer cart.abandoned for the same cart replaces the pending reminder.
  • Switching an automation off, or disabling the source, also stops what it had scheduled.

Sending and fallback#

  • The message is rendered for customer.locale: the exact language tag, then the bare language, then the default variant.
  • It is sent with reference set to the receipt ID (aev_…) and metadata: { source, event, automation }, so it can be found in the message log and in webhooks.
  • Per-recipient protection: at most 20 automation messages to one recipient per source per hour, and 5 runs of one automation per recipient per hour. Beyond that the run is skipped with recipient_rate_limited.
  • A message that is empty after merge tags are filled in is skipped with empty_message.
  • The fallback is sent once, when the first message fails or, if the automation sets a timeout (30 seconds to 24 hours), when it was not delivered in that time. It is a second, separately billed message. A run that failed for insufficient_balance gets no fallback.

Read receipts#

curl https://api.omnimessage.co/v1/automation_events/aev_3kL9pQ2wE5rT8yU1iO4a \
  -H "Authorization: Bearer om_live_xxxxxxxxxxxxxxxxxxxxxxxx"
Response
{
  "id": "aev_3kL9pQ2wE5rT8yU1iO4a",
  "object": "automation_event",
  "mode": "live",
  "source_id": "src_9Kd2mQ5vB8cX1zL0pK3j",
  "event_id": "woocommerce:order:5012:order.paid:1",
  "type": "order.paid",
  "status": "accepted",
  "reason": null,
  "automations_matched": 1,
  "received_at": "2026-10-05T09:30:00.120Z",
  "runs": [
    {
      "automation_id": "aut_5Cf8hK1mP4rT7vY0aD3g",
      "status": "sent",
      "scheduled_for": "2026-10-05T09:30:00.120Z",
      "message_id": "msg_2b1Xw9aQ3rT8yU0pL4kZ",
      "skip_reason": null
    }
  ]
}

GET /v1/automation_events lists the receipts of the key mode, newest first, with cursor pagination. Filters: source, type, status (accepted or ignored), created_after, and test (true or false).

curl "https://api.omnimessage.co/v1/automation_events?source=src_9Kd2mQ5vB8cX1zL0pK3j&status=ignored&limit=20" \
  -H "Authorization: Bearer om_live_xxxxxxxxxxxxxxxxxxxxxxxx"
Run statusMeaning
scheduledWaiting for its time, or being sent.
sentHanded to the channel. message_id is the message; its delivery is tracked like any message.
skippedNot sent, with a skip_reason.
cancelledCancelled by a later event, replaced, or its source or automation was deleted.
failedThe message failed.
ReasonMeaning
event_not_enabledNo automation is switched on for this event type.
no_automation_matchedThe event did not meet the conditions of any automation.
no_recipientNo phone number or address to send to.
consent_missingThe customer has not agreed to this kind of message.
unsubscribedThe contact unsubscribed from the channel or is blocked.
staleoccurred_at is more than 7 days ago.
recipient_rate_limitedThe recipient already received the most messages allowed in an hour.
empty_messageNothing was left after the merge tags were filled in.
source_disabled, automation_disabledThe source was disabled or the automation switched off before the message was due.

Native platform webhooks#

WooCommerce and Shopify can sign webhooks themselves, so they can be pointed straight at OmniMessage without a plugin. Create a source for the shop in the console under Integrations; it shows the delivery URL (ingest_urls) and the signing secret.

POST /v1/ingest/{platform}/{source_id} takes no bearer token. The signature over the raw body, base64(HMAC-SHA256(raw body, signing secret)), is verified before anything else; the delivery is then stored, answered with 200 {"received": true} at once and processed in the background. Normalised events go through the same rules as POST /v1/automation_events and appear in the event log.

AnswerWhen
200 {"received": true}The signature is valid and the delivery is queued, or the same delivery was received before (deduplicated on X-WC-Webhook-Delivery-ID or X-Shopify-Webhook-Id). Topics that map to nothing are acknowledged too, so the platform does not disable the webhook.
401 webhook_signature_invalidThe signature header is missing or does not match the source’s signing secret.
403 source_disabledThe source is disabled.
404 resource_missingNo such source.
400 invalid_jsonThe body is not a JSON object.
429 rate_limit_exceededMore than 50 requests a second for the source.

WooCommerce#

In WordPress, open WooCommerce › Settings › Advanced › Webhooks and add one webhook per topic: Order created, Order updated and Customer created. Status Active, API version WP REST API Integration v3, the delivery URL of the source, and its signing secret as Secret. The unsigned ping WooCommerce sends when a webhook is saved is answered with 200. The WordPress and WooCommerce guide walks through it.

WooCommerceEvent
Topic order.createdorder.created, plus the status event below when the order is created already paid or completed.
Topic order.updated, status changed to processingorder.paid
… to completedorder.shipped
… to cancelled / refundedorder.cancelled / order.refunded
… to any other statusorder.status_changed with previous_status
Topic order.updated without a status changeNothing.
Topic customer.createdcustomer.created
  • Event IDs are woocommerce:order:{id}:{type}:{n}, where n counts how often the order produced that type.
  • The customer comes from the billing address (billing.phone with the billing or shipping country). Consent is read from the order meta _omnimessage_opt_in or _wc_other/omnimessage/opt-in; without it consent is unknown.
  • Tracking is read from _wc_shipment_tracking_items (the last entry) or _omnimessage_tracking. order.status_url is not in the WooCommerce payload and stays empty.

Shopify#

Shopify support is in beta. In the Shopify admin, open Settings › Notifications › Webhooks and create one webhook per event, format JSON, with the delivery URL of the source: Order creation, Order payment, Order fulfillment, Order cancellation, Refund create, Checkout creation, Checkout update and Customer creation. Shopify signs these webhooks with a key of its own, shown under the list of webhooks: paste it into the source’s settings (it replaces the generated signing secret). Until then deliveries are rejected.

Shopify topicEvent
orders/createorder.created
orders/paidorder.paid
orders/fulfilled, fulfillments/createorder.shipped
fulfillment_events/create with status deliveredorder.delivered
orders/cancelledorder.cancelled
refunds/createorder.refunded
checkouts/create, checkouts/updatecart.abandoned; every update replaces the pending reminder. A completed checkout is ignored.
customers/createcustomer.created
  • Event IDs are shopify:{topic}:{resource id}.
  • Consent comes from customer.sms_marketing_consent: subscribed is a marketing opt-in for SMS, unsubscribed an opt-out. Shopify has no transactional flag, so transactional consent is unknown and follows the source’s consent mode.
  • When a store has not granted access to protected customer data, Shopify sends no name, phone or email; such events are ignored with no_recipient.

For plugin authors: discovery#

A plugin learns everything it needs from GET /v1/integration_sources/current:

  1. Which events to send: enabled_events. Do not send others; ["*"] means send everything.
  2. How to behave: event_settings, for example report an abandoned cart no later than cart.abandoned.delay_seconds after the last cart activity. Reporting earlier is fine: the delay is measured from occurred_at.
  3. Where to send the user: console_url for the overview, automation_url_template with {event_type} replaced for the editor of one event.
Read the configuration, cached
curl https://api.omnimessage.co/v1/integration_sources/current \
  -H "Authorization: Bearer om_src_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H 'If-None-Match: "12"'
  • Cache it for about five minutes and send If-None-Match with the ETag; an unchanged configuration answers 304 without a body.
  • On 401 or 403, stop sending, show a reconnect notice and keep the last known configuration.
  • Report a new plugin version or new emits with PATCH /v1/integration_sources/current, or by registering again.

Merge tags#

Texts, template variables and JSON messages of an automation can contain merge tags. The syntax is the same in the WordPress plugin’s local templates.

FormMeaning
{{customer.first_name}}A dot path into the event payload. Whitespace inside the braces is ignored.
{{customer.first_name | default: "there"}}A filter. Available: default, upper, lower, capitalize, truncate, urlencode (for example truncate: 40). Filters chain left to right.
{{#if order.tracking.url}}…{{else}}…{{/if}}Shown when the value is present. Empty strings, null, false, 0, "0", "false" and empty lists count as absent.
{{#unless customer.first_name}}…{{/unless}}The opposite. Blocks nest.
\{{A literal {{.

The tags an automation can use depend on its event type: the customer block, the block of the event family, site, and the free-form data.*, form.fields.* and attributes.* (custom attributes of the synced contact). event.type, event.id and event.occurred_at are available everywhere.

BlockTags
customer{{customer.name}}, {{customer.first_name}}, {{customer.last_name}}, {{customer.phone}}, {{customer.email}}, {{customer.locale}}, {{customer.country}}, {{customer.id}}
order{{order.number}}, {{order.id}}, {{order.status}}, {{order.previous_status}}, {{order.currency}}, {{order.total.formatted}}, {{order.total.amount_minor}}, {{order.total.currency}}, {{order.subtotal.formatted}}, {{order.subtotal.amount_minor}}, {{order.subtotal.currency}}, {{order.shipping_total.formatted}}, {{order.shipping_total.amount_minor}}, {{order.shipping_total.currency}}, {{order.discount_total.formatted}}, {{order.discount_total.amount_minor}}, {{order.discount_total.currency}}, {{order.items_summary}}, {{order.items_count}}, {{order.payment_method}}, {{order.payment_method_title}}, {{order.shipping_method}}, {{order.tracking.number}}, {{order.tracking.url}}, {{order.tracking.carrier}}, {{order.status_url}}, {{order.note}}, {{order.created_at}}
cart{{cart.id}}, {{cart.currency}}, {{cart.total.formatted}}, {{cart.total.amount_minor}}, {{cart.total.currency}}, {{cart.items_summary}}, {{cart.items_count}}, {{cart.recovery_url}}, {{cart.updated_at}}
form{{form.name}}, {{form.id}}, {{form.plugin}}, {{form.fields_summary}}, {{form.page_url}}
appointment{{appointment.id}}, {{appointment.service}}, {{appointment.starts_at}}, {{appointment.ends_at}}, {{appointment.timezone}}, {{appointment.location}}, {{appointment.staff}}, {{appointment.manage_url}}
otp{{otp.code}}, {{otp.expires_in_seconds}}, {{otp.purpose}}
user{{user.id}}, {{user.login}}, {{user.reset_url}}
comment{{comment.id}}, {{comment.post_title}}, {{comment.post_url}}, {{comment.excerpt}}
site{{site.name}}, {{site.url}}
  • The campaign tags {{first_name}}, {{last_name}}, {{full_name}}, {{phone}}, {{email}}, {{locale}} work as aliases of the matching customer.* values.
  • {{site.name}} and {{site.url}} are the name and URL of the source as set in the console.
  • A tag that does not exist for the event type is rejected when the automation is saved. A tag that has no value in a particular event prints nothing and is counted as a missing tag in the automation’s statistics.
  • Output is plain text. Nothing is HTML-escaped, and a value that itself contains {{…}} is printed as is.
  • Lists of plain values are joined with commas; objects print nothing, so use the summaries order.items_summary, cart.items_summary and form.fields_summary.
  • After rendering, runs of spaces collapse and the result is trimmed. In WhatsApp template variables, line breaks and tabs become spaces and an empty value is sent as -, so the template stays deliverable.
  • Limits: 30 different tags per message, 8 KB per variant, rendered text cut at 4,096 characters.

Automations in the console#

Console › Integrations lists the directory of integrations and the connected sources of the current mode, with their platform, health, last event and number of automations. A source has three tabs:

  • Automations: one row per automation with its channel and 30-day statistics (sent, delivered, failed, skipped; test events excluded), a switch to turn it on or off, suggestions for event types the source sends but nothing answers yet, and Send test event, which pushes the built-in sample of an event type through the real pipeline, marked test.
  • The automation editor: event type (fixed once created), channel and fallback channel, message format (text, WhatsApp template with one value per variable, or JSON for any message type), up to 30 language variants, audience, consent class, conditions, delay, quiet hours and cancel_on. Starting points prefill a text for common events. The preview renders against the last real event of the type, or the sample. Send test sends the saved automation to a number you enter; in live mode that is a real, billed message.
  • Event log: every receipt with its result (sent, scheduled, skipped, failed, cancelled, no automation, ignored, duplicate), the customer, the payload with one-time codes masked, and each run with its message. Run again applies the automations that are on now to a stored event, without deduplication.
  • Settings: name and URL, contact sync, consent mode, "Record every event", the source ID and key prefix, the delivery URLs and the webhook signing secret (which can be replaced, for Shopify’s own key), replace key, disable and delete.

Errors and limits#

HTTPCodeWhen
400parameter_missing, parameter_invalidThe body does not match the contract. param is the path, for example customer.phone or events.3.type.
400integration_source_limit_reachedMore than 50 sources in one mode.
401api_key_invalidUnknown source key.
401webhook_signature_invalidNative webhook signature missing or wrong.
403scope_missingA source key outside its endpoints, an API key without the scope, or an API key on /current.
403source_disabledThe source is disabled.
404resource_missingUnknown source or receipt.
413payload_too_largeBody over 256 KB.
429rate_limit_exceededToo many requests; wait for Retry-After.
LimitValue
Body256 KB per request (also for a batch); data 32 KB; 200 items
Batch1 to 100 events, one request for rate limiting
Rate50 requests a second per source with a source key and for native webhooks; an API key has its usual rate limit
Sources50 per account and mode
Automations100 per source, 10 conditions and 30 languages each, 10 owner numbers
Deduplication and receipts30 days
occurred_atNot older than 7 days, not more than 10 minutes ahead
DelayUp to 30 days
Per recipient20 messages per source and 5 per automation, per hour

Events that carry credentials (otp.requested, user.password_reset_requested) skip the consent check and quiet hours, and cannot be run again from the event log once their code or link was removed.

    Loading