> ## Documentation Index
> Fetch the complete documentation index at: https://docs.omnicommerce.sg/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Consume product, order, return, settlement, and look events from OmniCommerce.

OmniCommerce receives marketplace-specific pushes, refreshes the full resource,
and publishes one stable integration contract. Consumers such as Spresso.ai do
not need to understand Shopee, Lazada, TikTok Shop, Shopify, Zalora, or Amazon
payload shapes.

<Note>
  Order events use the standardized `2026-08-01` envelope. Return and settlement
  events use standardized `2026-08-29` envelopes. Product and look events use
  the original `eventId` / `eventType` envelope. Each event reference identifies
  its exact contract; do not assume fields from one envelope exist in another.
</Note>

## Create a subscription

Create subscriptions with an organization API key or an OAuth token that has
the `webhooks:manage` scope. The endpoint must be a public HTTP(S) URL.

```bash theme={null}
curl -X POST "https://omnicommerce.sg/api/v1/webhooks" \
  -H "Authorization: Bearer $OMNI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.spresso.ai/webhooks/omnicommerce",
    "description": "Spresso commerce ingestion",
    "events": [
      "product.created",
      "product.updated",
      "product.deleted",
      "product.price.changed",
      "product.inventory.changed",
      "order.created",
      "order.updated",
      "order.status.changed",
      "order.deleted",
      "return.created",
      "return.updated",
      "return.status.changed",
      "settlement.created",
      "settlement.updated",
      "settlement.status.changed",
      "look.created",
      "look.deleted"
    ]
  }'
```

The `201` response contains the subscription and an `omni_whsec_...` signing
secret.

<Warning>
  Store the signing secret when the subscription is created. It is returned only
  once. Do not put it in source control or application logs.
</Warning>

Manage subscriptions with the [Webhooks API](/api-reference). API-key requests
inherit their organization from the key and do not send `organizationId`.

See the [event catalog](/webhooks/event-catalog) for all accepted subscription
keys and event availability.

## Standardized envelope and versions

Every standardized order, return, or settlement event uses these top-level fields:

| Field             | Meaning                                                                                |
| ----------------- | -------------------------------------------------------------------------------------- |
| `schemaVersion`   | Wire-contract version; `2026-08-01` for orders or `2026-08-29` for returns/settlements |
| `id`              | Stable event ID and recipient idempotency key                                          |
| `type`            | Canonical subscription event key                                                       |
| `occurredAt`      | Time the canonical resource change occurred                                            |
| `publishedAt`     | Time the immutable event entered the delivery outbox                                   |
| `organizationId`  | OmniCommerce tenant boundary                                                           |
| `resourceVersion` | Increasing integer for one `subject.id`                                                |
| `source`          | Source system, marketplace, exact connected account, and provider receipt              |
| `subject`         | Stable internal and marketplace resource identity                                      |
| `data`            | Complete resource snapshot plus event-specific context                                 |

Unknown additive fields must be ignored. Breaking changes receive a new
`schemaVersion`; existing versions remain stable during their support window.

Product and look events use `eventId`, `eventType`, `organizationId`, the
resource ID, `changedFields`, `occurredAt`, and `data`. Their reference pages
document the complete legacy envelope.

## Delivery guarantees

Events are written to the delivery outbox with their immutable resource
snapshot.

| Behavior        | Contract                                                                   |
| --------------- | -------------------------------------------------------------------------- |
| Acknowledgement | Any HTTP `2xx` response                                                    |
| Timeout         | 10 seconds per delivery attempt                                            |
| Retries         | A failed delivery is retried up to three times                             |
| Idempotency     | The event body and `X-Omni-Event-Id` remain stable across retries          |
| Ordering        | No global ordering; compare `resourceVersion` within one `subject.id`      |
| Duplicates      | At-least-once delivery means the same event can be received more than once |

Return any `2xx` response only after durably accepting the event.

## Verify signatures

Each POST includes:

```text theme={null}
X-Omni-Event-Id: evt_...
X-Omni-Event-Type: product.updated
X-Omni-Timestamp: 1780000000
X-Omni-Signature: <hex HMAC-SHA256>
```

Calculate HMAC-SHA256 over `{timestamp}.{rawBody}` with the subscription secret.
Compare the hexadecimal digest with `X-Omni-Signature` using a constant-time
comparison before parsing the JSON body.

```ts theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyOmniWebhook(input: {
  secret: string;
  timestamp: string;
  rawBody: string;
  signature: string;
}): boolean {
  const ageSeconds = Math.abs(Date.now() / 1000 - Number(input.timestamp));
  if (!Number.isFinite(ageSeconds) || ageSeconds > 300) return false;

  const expected = Buffer.from(
    createHmac("sha256", input.secret)
      .update(`${input.timestamp}.${input.rawBody}`)
      .digest("hex"),
    "hex",
  );
  const actual = Buffer.from(input.signature, "hex");
  return expected.length === actual.length && timingSafeEqual(expected, actual);
}
```

Keep the request body as raw bytes or text until verification succeeds. Parsing
and re-serializing JSON before verification changes the signed value.

## Process events safely

1. Read the raw request body.
2. Reject missing, stale, or invalid signature headers.
3. Deduplicate by `id`, legacy `eventId`, or `X-Omni-Event-Id` in durable storage.
4. For order, return, and settlement events, compare `resourceVersion` with the last
   applied version for `subject.id`.
5. Apply the complete snapshot in one local transaction.
6. Persist the event ID and, when present, resource version before returning
   `2xx`.

Buyer PII and delivery addresses are excluded from the standardized order
contract. Treat all remaining payload data as organization-confidential.

## Continue

<CardGroup cols={2}>
  <Card title="Event catalog" icon="list" href="/webhooks/event-catalog">
    Every accepted subscription event and current availability.
  </Card>

  <Card title="Product webhooks" icon="box" href="/webhooks/products">
    Catalog lifecycle, price, and inventory events.
  </Card>

  <Card title="Order webhooks" icon="receipt" href="/webhooks/orders">
    Event semantics, field definitions, and payload examples.
  </Card>

  <Card title="Return webhooks" icon="rotate-left" href="/webhooks/returns">
    PII-free returned SKUs, refunds, and lifecycle changes.
  </Card>

  <Card title="Settlement webhooks" icon="money-bill-transfer" href="/webhooks/settlements">
    Marketplace fees, net settlement, and finance-status changes.
  </Card>

  <Card title="Look webhooks" icon="images" href="/webhooks/looks">
    Shoppable-look creation and deletion events.
  </Card>

  <Card title="Marketplace mapping" icon="arrows-rotate" href="/webhooks/marketplace-mapping">
    Provider topics, source identity, and normalization boundaries.
  </Card>
</CardGroup>
