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.
- The event arrives at
POST /v1/automation_events, or as a native WooCommerce or Shopify webhook atPOST /v1/ingest/{platform}/{source_id}. - It is stored as a receipt and deduplicated on its
id. - The enabled automations of the source for that event type are matched, in the order the console shows them, against their conditions.
- For each match the recipient and the consent are checked and a run is scheduled:
occurred_atplus the delay, moved out of quiet hours. - 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 forPOST /v1/messages. - 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.
{
"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"
}| Field | Description |
|---|---|
platform | woocommerce, wordpress, shopify, salla, zid, ikas, ticimax or custom, or any other lower-case slug of up to 32 characters. |
external_id | The 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. |
emits | The event types the plugin can send. Informational: the console suggests automations for them. |
enabled_events | The 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_settings | Hints for plugins, keyed by event type. Today: cart.abandoned.delay_seconds, the shortest delay of the enabled abandoned-cart automations. |
config_version | Increases whenever enabled_events or event_settings change. It is also the ETag of the source. |
console_url, automation_url_template | Where a plugin sends its user: the automations of the source, and the editor of one event type ({event_type} replaced). |
ingest_urls | The delivery URLs for native WooCommerce and Shopify webhooks. |
source_key, signing_secret | Returned 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.
| Credential | May 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:read | Register, list, read, update, roll and delete sources. |
API key with events:write | Push events. The body must then name the source: "source": "src_…". |
API key with events:read | List and read event receipts. |
| Platform signature | POST /v1/ingest/{platform}/{source_id} only. |
- An unknown source key is
401 api_key_invalid; a disabled source is403 source_disabled; a suspended account is403 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
/currentendpoints: they are for source keys only. GET /v1/melistsautomation_eventsincapabilities. Plugins check that list, not the endpoints, before offering automation events.
Endpoints#
| Method and path | Auth | Purpose |
|---|---|---|
POST /v1/integration_sources | integrations:write | Register 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_sources | integrations:read | List the sources of the key mode. |
GET /v1/integration_sources/{id} | integrations:read | Read one source, with ETag. |
GET /v1/integration_sources/current | Source key | The source of the key. Sends ETag: "<config_version>" and answers 304 to a matching If-None-Match. |
PATCH /v1/integration_sources/current | Source key | Update name, url, plugin_version, platform_version, emits. |
PATCH /v1/integration_sources/{id} | integrations:write | The same fields, plus status (active or disabled), signing_secret and settings (contact_sync, consent_mode, store_all_events). |
POST /v1/integration_sources/{id}/roll_key | integrations:write | New source key and signing secret. |
DELETE /v1/integration_sources/{id} | integrations:write | Delete the source with its automations and receipts; scheduled runs are cancelled. Messages already sent stay. |
POST /v1/automation_events | Source key or events:write | Push one event. |
POST /v1/automation_events/batch | Source key or events:write | Push up to 100 events. |
GET /v1/automation_events | events:read | List receipts, newest first. |
GET /v1/automation_events/{id} | events:read | One receipt with its runs. |
POST /v1/ingest/{platform}/{source_id} | Platform signature | Native 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"
]
}'{
"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:
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"
}'{
"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
202with the receipt.statusisacceptedwhen at least one message is scheduled, otherwiseignoredwith areason. - Sending the same
idagain answers200with the first receipt,status: "duplicate"and the headerIdempotent-Replayed: true, whatever the body. Platforms and queues retry the same occurrence with bodies that are not byte-identical, so the eventidis the idempotency key; anIdempotency-Keyheader is accepted and ignored on this endpoint. - Receipts, and with them the deduplication window, are kept for 30 days.
- Retry only transport errors,
429and5xx. A4xxwill 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:
{
"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#
| Field | Rules |
|---|---|
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. |
source | The src_… ID. Required with an API key, forbidden with a source key. |
test | true 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. |
site | name and url. |
customer | Who the event concerns. Optional for events that only notify the account owner. |
order, cart, form, appointment, otp, user, comment | The block that matches the event family. |
data | Free-form extras up to 32 KB, available as {{data.*}}. |
Event types#
| Type | Block | Typical trigger |
|---|---|---|
order.created | order | Order lifecycle |
order.paid | order | Order lifecycle |
order.shipped | order | Order lifecycle |
order.delivered | order | Order lifecycle |
order.cancelled | order | Order lifecycle |
order.refunded | order | Order lifecycle |
order.status_changed | order (status, previous_status) | Order lifecycle |
order.note_added | order (note) | Order lifecycle |
cart.abandoned | cart | Checkout started, no order followed |
customer.created | customer | A new customer |
user.registered | user | Accounts on the site |
user.password_reset_requested | user | Accounts on the site |
form.submitted | form | Contact and lead forms |
appointment.booked | appointment | Scheduling |
appointment.rescheduled | appointment | Scheduling |
appointment.cancelled | appointment | Scheduling |
appointment.reminder_due | appointment | Scheduling |
otp.requested | otp | One-time passwords |
comment.approved | comment | Blog comments |
custom.<name> | data | Anything 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.phoneshould be E.164; otherwisephone_raw(orphone) is read as a number ofcustomer.country, or of the account country.customer.consent:transactionalandmarketingaretrue,falseornull(unknown);channelsmaps a channel type toopted_in,opted_outorunknownand wins over the two flags for that channel;sourceandcollected_atdescribe where the consent came from.- Money is always
{ "amount_minor": <integer>, "currency": "<ISO 4217>", "formatted": "<as the shop shows it>" }.amount_minoruses 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 plusdata.admin_recipientsof 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
skippedwithno_recipient.
Consent#
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.
| Situation | Result |
|---|---|
Audience owner, or otp.requested / user.password_reset_requested | No consent check. |
| The contact is blocked or unsubscribed from the channel | Never messaged: unsubscribed. Checked again just before sending. |
customer.consent.channels.<type> is opted_out / opted_in | consent_missing / sent. |
| Marketing automation | Needs 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_atplus 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_onevent 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 samecart.idordata.cart_id. The default forcart.abandonedisorder.createdandorder.paid. - A newer
cart.abandonedfor 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
referenceset to the receipt ID (aev_…) andmetadata: { 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
skippedwithrecipient_rate_limited. - A message that is empty after merge tags are filled in is
skippedwithempty_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_balancegets no fallback.
Read receipts#
curl https://api.omnimessage.co/v1/automation_events/aev_3kL9pQ2wE5rT8yU1iO4a \
-H "Authorization: Bearer om_live_xxxxxxxxxxxxxxxxxxxxxxxx"{
"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 status | Meaning |
|---|---|
scheduled | Waiting for its time, or being sent. |
sent | Handed to the channel. message_id is the message; its delivery is tracked like any message. |
skipped | Not sent, with a skip_reason. |
cancelled | Cancelled by a later event, replaced, or its source or automation was deleted. |
failed | The message failed. |
| Reason | Meaning |
|---|---|
event_not_enabled | No automation is switched on for this event type. |
no_automation_matched | The event did not meet the conditions of any automation. |
no_recipient | No phone number or address to send to. |
consent_missing | The customer has not agreed to this kind of message. |
unsubscribed | The contact unsubscribed from the channel or is blocked. |
stale | occurred_at is more than 7 days ago. |
recipient_rate_limited | The recipient already received the most messages allowed in an hour. |
empty_message | Nothing was left after the merge tags were filled in. |
source_disabled, automation_disabled | The 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.
| Answer | When |
|---|---|
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_invalid | The signature header is missing or does not match the source’s signing secret. |
403 source_disabled | The source is disabled. |
404 resource_missing | No such source. |
400 invalid_json | The body is not a JSON object. |
429 rate_limit_exceeded | More 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.
| WooCommerce | Event |
|---|---|
Topic order.created | order.created, plus the status event below when the order is created already paid or completed. |
Topic order.updated, status changed to processing | order.paid |
… to completed | order.shipped |
… to cancelled / refunded | order.cancelled / order.refunded |
| … to any other status | order.status_changed with previous_status |
Topic order.updated without a status change | Nothing. |
Topic customer.created | customer.created |
- Event IDs are
woocommerce:order:{id}:{type}:{n}, wherencounts how often the order produced that type. - The customer comes from the billing address (
billing.phonewith the billing or shipping country). Consent is read from the order meta_omnimessage_opt_inor_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_urlis 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 topic | Event |
|---|---|
orders/create | order.created |
orders/paid | order.paid |
orders/fulfilled, fulfillments/create | order.shipped |
fulfillment_events/create with status delivered | order.delivered |
orders/cancelled | order.cancelled |
refunds/create | order.refunded |
checkouts/create, checkouts/update | cart.abandoned; every update replaces the pending reminder. A completed checkout is ignored. |
customers/create | customer.created |
- Event IDs are
shopify:{topic}:{resource id}. - Consent comes from
customer.sms_marketing_consent:subscribedis a marketing opt-in for SMS,unsubscribedan 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:
- Which events to send:
enabled_events. Do not send others;["*"]means send everything. - How to behave:
event_settings, for example report an abandoned cart no later thancart.abandoned.delay_secondsafter the last cart activity. Reporting earlier is fine: the delay is measured fromoccurred_at. - Where to send the user:
console_urlfor the overview,automation_url_templatewith{event_type}replaced for the editor of one event.
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-Matchwith theETag; an unchanged configuration answers304without a body. - On
401or403, stop sending, show a reconnect notice and keep the last known configuration. - Report a new plugin version or new
emitswithPATCH /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.
| Form | Meaning |
|---|---|
{{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.
| Block | Tags |
|---|---|
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 matchingcustomer.*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_summaryandform.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#
| HTTP | Code | When |
|---|---|---|
| 400 | parameter_missing, parameter_invalid | The body does not match the contract. param is the path, for example customer.phone or events.3.type. |
| 400 | integration_source_limit_reached | More than 50 sources in one mode. |
| 401 | api_key_invalid | Unknown source key. |
| 401 | webhook_signature_invalid | Native webhook signature missing or wrong. |
| 403 | scope_missing | A source key outside its endpoints, an API key without the scope, or an API key on /current. |
| 403 | source_disabled | The source is disabled. |
| 404 | resource_missing | Unknown source or receipt. |
| 413 | payload_too_large | Body over 256 KB. |
| 429 | rate_limit_exceeded | Too many requests; wait for Retry-After. |
| Limit | Value |
|---|---|
| Body | 256 KB per request (also for a batch); data 32 KB; 200 items |
| Batch | 1 to 100 events, one request for rate limiting |
| Rate | 50 requests a second per source with a source key and for native webhooks; an API key has its usual rate limit |
| Sources | 50 per account and mode |
| Automations | 100 per source, 10 conditions and 30 languages each, 10 owner numbers |
| Deduplication and receipts | 30 days |
occurred_at | Not older than 7 days, not more than 10 minutes ahead |
| Delay | Up to 30 days |
| Per recipient | 20 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.