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

# Webhooks

> Subscribe to real-time events from the Arcus API. Accounts, products, orders, invoices, payments, inventory, purchasing, fulfillment, returns, and more -- 118 event types across 14 families.

## Overview

Webhooks let your application react to events in real time without polling. When something happens
in Arcus -- an order is confirmed, a payment is received, a shipment is created, an inventory
adjustment is posted -- Arcus sends an HTTP POST to your endpoint with a signed JSON payload.

All webhook requests are signed with HMAC-SHA256. You verify the signature using the signing
secret returned at endpoint creation.

## Event structure

Every webhook payload uses this envelope:

```json theme={null}
{
  "id": "evt_01H...",
  "object": "event",
  "type": "order.confirmed",
  "created_at": "2026-05-01T10:30:00Z",
  "api_version": "2026-05-01",
  "entity_id": "ent_01H...",
  "data": {
    "object": {
      "id": "ord_01H...",
      "object": "order",
      "document_type": "sales_order",
      "status": "confirmed"
    }
  },
  "livemode": true
}
```

| Field         | Type     | Description                                                   |
| ------------- | -------- | ------------------------------------------------------------- |
| `id`          | string   | Unique event ID. Use this for deduplication.                  |
| `type`        | string   | Event name (e.g. `order.confirmed`). See event catalog below. |
| `created_at`  | ISO 8601 | When the event fired.                                         |
| `api_version` | string   | The API version used to serialize `data.object`.              |
| `entity_id`   | string   | The entity this event belongs to.                             |
| `data.object` | object   | The full resource at the time of the event.                   |
| `livemode`    | boolean  | `true` for production events; `false` for test-mode events.   |

## Registering an endpoint

```bash theme={null}
curl -X POST https://api.arcuserp.com/v1/entities/$ARCUS_ENTITY_ID/webhook_endpoints \
  -H "Authorization: Bearer $ARCUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.example.com/arcus-webhook",
    "enabled_events": ["order.confirmed", "payment.succeeded", "fulfillment.shipped"],
    "description": "Production webhook for order pipeline"
  }'
```

The response includes a `secret` in `whsec_<hex>` format. **Store it securely -- it is
shown once and cannot be retrieved again.** Use it to verify every incoming request.

```json theme={null}
{
  "id": "whe_01H...",
  "object": "webhook_endpoint",
  "url": "https://your-server.example.com/arcus-webhook",
  "status": "active",
  "enabled_events": ["order.confirmed", "payment.succeeded", "fulfillment.shipped"],
  "secret": "whsec_...",
  "created_at": "2026-05-01T10:30:00Z"
}
```

<Tip>
  **Field name:** use `enabled_events` (Stripe convention) in all requests and responses.
  The legacy alias `events` is accepted for backward compatibility but is deprecated and will
  be removed in API v2.
</Tip>

### Wildcard subscriptions

Subscribe to an entire event family with `order.*`, or all events with `*`:

```json theme={null}
{ "enabled_events": ["order.*", "payment.*"] }
```

Valid wildcard patterns are `<family>.*` (14 families) or `*` (all 118 events).

### Duplicate endpoint guard

Registering the same URL + mode combination twice returns HTTP 409 with `error: "duplicate_url"`
and the `existing_id` of the already-active endpoint.

## Verifying signatures

Every webhook request includes an `Arcus-Signature` header. Always verify it before processing
the payload. The signature is an HMAC-SHA256 of `<timestamp>.<raw_body>` using your signing
secret, with a 5-minute replay window.

Header format: `Arcus-Signature: t=<unix_epoch>,v1=<hex_hmac_sha256>`

<Tabs>
  <Tab title="SDK (recommended)">
    <CodeGroup>
      ```typescript TypeScript SDK theme={null}
      import { Webhooks } from '@arcuserp/arcus-node';

      // Express example -- use express.raw() to preserve the raw body
      app.post('/arcus-webhook', express.raw({ type: 'application/json' }), (req, res) => {
        try {
          const { timestamp } = Webhooks.verifyWebhookSignature({
            payload: req.body.toString(),
            signatureHeader: req.headers['arcus-signature'] as string,
            secret: process.env.ARCUS_WEBHOOK_SECRET!,
          });
          // timestamp is the verified unix epoch from the signature header
        } catch (err) {
          if (err instanceof Webhooks.WebhookSignatureError) {
            // err.code is one of: 'missing_header' | 'malformed_header' |
            //                     'timestamp_too_old' | 'invalid_signature'
            return res.status(400).json({ error: err.code, message: err.message });
          }
          return res.status(400).send('Webhook verification failed');
        }

        const event = JSON.parse(req.body);
        // Process event...
        res.status(200).send('ok');
      });
      ```

      ```python Python SDK theme={null}
      from arcuserp.webhooks import verify_webhook_signature, WebhookSignatureError

      # Flask example
      from flask import Flask, request, abort
      import os

      app = Flask(__name__)

      @app.route('/arcus-webhook', methods=['POST'])
      def webhook():
          try:
              timestamp = verify_webhook_signature(
                  payload=request.data.decode('utf-8'),
                  signature_header=request.headers.get('Arcus-Signature', ''),
                  secret=os.environ['ARCUS_WEBHOOK_SECRET'],
              )
              # timestamp is the verified unix epoch from the signature header
          except WebhookSignatureError as e:
              # e.code is one of: 'missing_header', 'malformed_header',
              #                    'timestamp_too_old', 'invalid_signature'
              abort(400, str(e))

          event = request.get_json()
          # Process event...
          return '', 200
      ```

      ```go Go SDK theme={null}
      import (
          "errors"
          "net/http"
          "io"
          "os"
          "github.com/arcuserp/arcus-sdk-go/pkg/webhooks"
      )

      func webhookHandler(w http.ResponseWriter, r *http.Request) {
          rawBody, _ := io.ReadAll(r.Body)

          ts, err := webhooks.VerifyWebhookSignature(
              string(rawBody),
              r.Header.Get("Arcus-Signature"),
              os.Getenv("ARCUS_WEBHOOK_SECRET"),
              300, // tolerance in seconds (5 minutes)
          )
          if err != nil {
              var sigErr *webhooks.SignatureError
              if errors.As(err, &sigErr) {
                  // sigErr.Code is one of: "missing_header", "malformed_header",
                  //                        "timestamp_too_old", "invalid_signature"
                  http.Error(w, sigErr.Code, http.StatusBadRequest)
                  return
              }
              http.Error(w, "verification failed", http.StatusBadRequest)
              return
          }
          // ts is the verified unix timestamp from the signature header
          _ = ts

          // Process event...
          w.WriteHeader(http.StatusOK)
      }
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Raw implementation (no SDK)">
    <CodeGroup>
      ```javascript Node.js theme={null}
      import crypto from 'crypto';

      /**
       * Verify an Arcus webhook request.
       * @param {string | Buffer} rawBody  The raw request body (before JSON.parse).
       * @param {string}          signature  The Arcus-Signature header value.
       * @param {string}          secret  The signing secret (whsec_... from endpoint creation).
       * @throws {Error} if the signature is invalid or the timestamp is stale.
       */
      function verifyArcusWebhook(rawBody, signature, secret) {
        const parts = Object.fromEntries(
          signature.split(',').map((p) => p.split('=', 2))
        );
        const timestamp = parts['t'];
        const receivedSig = parts['v1'];

        if (!timestamp || !receivedSig) {
          throw new Error('Malformed Arcus-Signature header');
        }

        const payload = `${timestamp}.${rawBody}`;
        const expected = crypto
          .createHmac('sha256', secret)
          .update(payload)
          .digest('hex');

        const sigBuf = Buffer.from(receivedSig, 'hex');
        const expBuf = Buffer.from(expected, 'hex');

        if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
          throw new Error('Invalid webhook signature');
        }

        // Reject events older than 5 minutes (replay protection)
        const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
        if (age > 300) {
          throw new Error('Webhook timestamp too old (> 5 minutes)');
        }
      }

      // Express example -- use express.raw() to preserve the raw body
      app.post('/arcus-webhook', express.raw({ type: 'application/json' }), (req, res) => {
        try {
          verifyArcusWebhook(req.body, req.headers['arcus-signature'], process.env.ARCUS_WEBHOOK_SECRET);
        } catch (err) {
          return res.status(400).send(err.message);
        }

        const event = JSON.parse(req.body);
        // Process event...
        res.status(200).send('ok');
      });
      ```

      ```python Python theme={null}
      import hashlib
      import hmac
      import time
      import json

      def verify_arcus_webhook(raw_body: bytes, signature: str, secret: str) -> None:
          """
          Verify an Arcus webhook request.

          Args:
              raw_body: The raw request body bytes (before JSON parsing).
              signature: The Arcus-Signature header value.
              secret: The signing secret (whsec_... from endpoint creation).

          Raises:
              ValueError: if the signature is invalid or the timestamp is stale.
          """
          parts = dict(p.split('=', 1) for p in signature.split(','))
          timestamp = parts.get('t')
          received_sig = parts.get('v1')

          if not timestamp or not received_sig:
              raise ValueError('Malformed Arcus-Signature header')

          payload = f"{timestamp}.{raw_body.decode('utf-8')}"
          expected = hmac.new(
              secret.encode('utf-8'),
              payload.encode('utf-8'),
              hashlib.sha256,
          ).hexdigest()

          if not hmac.compare_digest(expected, received_sig):
              raise ValueError('Invalid webhook signature')

          age = time.time() - int(timestamp)
          if age > 300:
              raise ValueError('Webhook timestamp too old (> 5 minutes)')


      # Flask example
      from flask import Flask, request, abort

      app = Flask(__name__)

      @app.route('/arcus-webhook', methods=['POST'])
      def webhook():
          try:
              verify_arcus_webhook(
                  request.data,
                  request.headers.get('Arcus-Signature', ''),
                  os.environ['ARCUS_WEBHOOK_SECRET'],
              )
          except ValueError as e:
              abort(400, str(e))

          event = request.get_json()
          # Process event...
          return '', 200
      ```

      ```go Go theme={null}
      package main

      import (
          "crypto/hmac"
          "crypto/sha256"
          "encoding/hex"
          "errors"
          "net/http"
          "os"
          "strconv"
          "strings"
          "time"
      )

      // VerifyArcusWebhook verifies an Arcus webhook request.
      // rawBody is the raw request body bytes; signature is the Arcus-Signature header value;
      // secret is the whsec_... signing secret from endpoint creation.
      func VerifyArcusWebhook(rawBody []byte, signature, secret string) error {
          parts := make(map[string]string)
          for _, p := range strings.Split(signature, ",") {
              kv := strings.SplitN(p, "=", 2)
              if len(kv) == 2 {
                  parts[kv[0]] = kv[1]
              }
          }

          timestamp := parts["t"]
          receivedSig := parts["v1"]
          if timestamp == "" || receivedSig == "" {
              return errors.New("malformed Arcus-Signature header")
          }

          payload := timestamp + "." + string(rawBody)
          mac := hmac.New(sha256.New, []byte(secret))
          mac.Write([]byte(payload))
          expected := hex.EncodeToString(mac.Sum(nil))

          if !hmac.Equal([]byte(expected), []byte(receivedSig)) {
              return errors.New("invalid webhook signature")
          }

          ts, err := strconv.ParseInt(timestamp, 10, 64)
          if err != nil {
              return errors.New("invalid timestamp in Arcus-Signature header")
          }
          if time.Now().Unix()-ts > 300 {
              return errors.New("webhook timestamp too old (> 5 minutes)")
          }
          return nil
      }

      func webhookHandler(w http.ResponseWriter, r *http.Request) {
          rawBody, _ := io.ReadAll(r.Body)
          if err := VerifyArcusWebhook(rawBody, r.Header.Get("Arcus-Signature"), os.Getenv("ARCUS_WEBHOOK_SECRET")); err != nil {
              http.Error(w, err.Error(), http.StatusBadRequest)
              return
          }
          // Process event...
          w.WriteHeader(http.StatusOK)
      }
      ```
    </CodeGroup>
  </Tab>
</Tabs>

### Webhook request headers

Every delivery includes these headers:

| Header              | Description                                    |
| ------------------- | ---------------------------------------------- |
| `Arcus-Signature`   | `t=<unix_epoch>,v1=<hex_hmac_sha256>`          |
| `Arcus-Webhook-Id`  | The webhook endpoint ID                        |
| `Arcus-Event-Id`    | The event ID (use for deduplication)           |
| `Arcus-Event-Type`  | The event type string (e.g. `order.confirmed`) |
| `Arcus-API-Version` | The API version used to serialize the payload  |
| `Content-Type`      | `application/json`                             |

## Responding to webhooks

Return a `2xx` response within **5 seconds**. If your endpoint takes longer, respond immediately
and process the event asynchronously. Arcus retries on any non-2xx response or connection
failure, with exponential backoff:

| Attempt | Delay after previous |
| ------- | -------------------- |
| 1       | Immediate            |
| 2       | 1 minute             |
| 3       | 5 minutes            |
| 4       | 30 minutes           |
| 5       | 2 hours              |
| 6       | 12 hours             |
| 7       | 24 hours             |

After 6 failed attempts, no more retries are made for that delivery. An endpoint that accumulates
10 consecutive permanent failures is automatically disabled.

## Deduplication

Events may be delivered more than once (network timeouts, retries). Always deduplicate on
`event.id` before processing:

```javascript theme={null}
const eventId = req.headers['arcus-event-id'];
const existing = await db.processedEvents.findUnique({ where: { id: eventId } });
if (existing) return res.status(200).send('already processed');

await db.processedEvents.create({ data: { id: eventId } });
await processEvent(event);
```

## Rotating signing secrets

```bash theme={null}
curl -X POST https://api.arcuserp.com/v1/entities/$ARCUS_ENTITY_ID/webhook_endpoints/$WEBHOOK_ID/rotate_secret \
  -H "Authorization: Bearer $ARCUS_API_KEY"
```

The response returns a new `signing_secret`. Update your environment variable immediately. The
old secret is invalidated at rotation -- there is no grace period.

## Sending a test event

Send a `webhook.test` event to verify your endpoint is reachable:

```bash theme={null}
curl -X POST https://api.arcuserp.com/v1/entities/$ARCUS_ENTITY_ID/webhook_endpoints/$WEBHOOK_ID/test \
  -H "Authorization: Bearer $ARCUS_API_KEY"
```

## Event catalog

Arcus emits 118 event types across 14 families. Subscribe to individual events, a whole family
(`order.*`), or everything (`*`).

### account (12 events)

| Event                          | Description                               |
| ------------------------------ | ----------------------------------------- |
| `account.created`              | A new account is created                  |
| `account.updated`              | Account fields are updated                |
| `account.deleted`              | An account is soft-deleted                |
| `account.merged`               | Two accounts are merged                   |
| `account.address.added`        | An address is added to an account         |
| `account.address.updated`      | An account address is updated             |
| `account.address.removed`      | An account address is removed             |
| `account.contact.added`        | A contact is added to an account          |
| `account.contact.updated`      | An account contact is updated             |
| `account.contact.removed`      | An account contact is removed             |
| `account.credit_limit.updated` | The credit limit on an account is changed |
| `account.terms.updated`        | Payment terms on an account are changed   |

### product (21 events)

| Event                           | Description                              |
| ------------------------------- | ---------------------------------------- |
| `product.created`               | A new product is created                 |
| `product.updated`               | Product fields are updated               |
| `product.deleted`               | A product is soft-deleted                |
| `product.restored`              | A deleted product is restored            |
| `product.variant.created`       | A product variant is created             |
| `product.variant.updated`       | A product variant is updated             |
| `product.variant.deleted`       | A product variant is deleted             |
| `product.kit.component.added`   | A component is added to a kit            |
| `product.kit.component.updated` | A kit component is updated               |
| `product.kit.component.removed` | A kit component is removed               |
| `product.pricing.added`         | A pricing policy is added to a product   |
| `product.pricing.updated`       | A product pricing policy is updated      |
| `product.pricing.removed`       | A product pricing policy is removed      |
| `product.vendor.added`          | A vendor is linked to a product          |
| `product.vendor.updated`        | A product-vendor relationship is updated |
| `product.vendor.removed`        | A vendor is unlinked from a product      |
| `product.listing.created`       | A marketplace listing is created         |
| `product.listing.updated`       | A marketplace listing is updated         |
| `product.listing.removed`       | A marketplace listing is removed         |
| `product.listing.paused`        | A marketplace listing is paused          |
| `product.listing.resumed`       | A paused marketplace listing is resumed  |

### order (16 events)

| Event                                      | Description                                                                                                                                                                                                                                                                                            |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `order.created`                            | A quote or sales order is created                                                                                                                                                                                                                                                                      |
| `order.updated`                            | Order fields are updated                                                                                                                                                                                                                                                                               |
| `order.confirmed`                          | A quote is confirmed as a sales order                                                                                                                                                                                                                                                                  |
| `order.cancelled`                          | An order is cancelled                                                                                                                                                                                                                                                                                  |
| `order.shipped`                            | An order is marked shipped                                                                                                                                                                                                                                                                             |
| `order.invoiced`                           | A sales order is converted to an invoice                                                                                                                                                                                                                                                               |
| `order.partially_fulfilled`                | Some but not all order lines are fulfilled                                                                                                                                                                                                                                                             |
| `order.fulfilled`                          | All order lines are fulfilled                                                                                                                                                                                                                                                                          |
| `order.line_item.added`                    | A line item is added to an order                                                                                                                                                                                                                                                                       |
| `order.line_item.updated`                  | An order line item is updated                                                                                                                                                                                                                                                                          |
| `order.line_item.removed`                  | An order line item is removed                                                                                                                                                                                                                                                                          |
| `order.tax.recalculated`                   | Tax is recalculated for an order                                                                                                                                                                                                                                                                       |
| `order.revised`                            | A SENT invoice or sales order is revised (a customer-visible field changed, or a line item was added/updated/removed after the document was sent). Payload: `order_id`, `order_number`, `document_type`, `revision_count`, `revised_at`, `revised_by_user_id`, `revision_summary`, `changed_fields[]`. |
| `order.credit_hold_overridden`             | A permissioned operator pushed an order past a credit-hold gate with a typed reason                                                                                                                                                                                                                    |
| `order.credit_hold.override_notified`      | Finance-team notification fanout succeeded after a credit-hold override (at least one recipient was emailed)                                                                                                                                                                                           |
| `order.credit_hold.override_notify_failed` | The credit-hold override notification fanout failed for every configured recipient (Postmark / DNS / config issue)                                                                                                                                                                                     |

### invoice (9 events)

| Event                        | Description                                                               |
| ---------------------------- | ------------------------------------------------------------------------- |
| `invoice.created`            | An invoice is created                                                     |
| `invoice.updated`            | Invoice fields are updated                                                |
| `invoice.paid`               | An invoice is fully paid                                                  |
| `invoice.partially_paid`     | A partial payment is applied to an invoice                                |
| `invoice.voided`             | An invoice is voided                                                      |
| `invoice.refunded`           | An invoice is refunded                                                    |
| `invoice.overdue`            | An invoice becomes past its due date                                      |
| `invoice.sent`               | An invoice is emailed to the customer                                     |
| `invoice.viewed_by_customer` | A customer views the invoice portal page (future: requires portal beacon) |

### payment (8 events)

| Event                        | Description                              |
| ---------------------------- | ---------------------------------------- |
| `payment.created`            | A payment record is created              |
| `payment.succeeded`          | A payment is successfully processed      |
| `payment.failed`             | A payment attempt fails                  |
| `payment.refunded`           | A payment is fully refunded              |
| `payment.partially_refunded` | A partial refund is issued               |
| `payment.disputed`           | A payment dispute (chargeback) is opened |
| `payment.dispute.won`        | A dispute is resolved in your favor      |
| `payment.dispute.lost`       | A dispute is resolved against you        |

### inventory (7 events)

| Event                             | Description                                                                                                                                      |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `inventory.adjusted`              | An inventory adjustment is posted                                                                                                                |
| `inventory.transferred`           | Inventory is transferred between locations                                                                                                       |
| `inventory.reclassified`          | Inventory is reclassified                                                                                                                        |
| `inventory.low_stock`             | On-hand falls to at or below the product's reorder point (and is still above zero). Debounced to at most once per product per location per hour. |
| `inventory.out_of_stock`          | On-hand reaches zero (or below). Debounced to at most once per product per location per hour.                                                    |
| `inventory.back_in_stock`         | A product that was at zero on-hand is replenished above zero. Debounced to at most once per product per location per hour.                       |
| `inventory.cycle_count.completed` | A cycle count is completed                                                                                                                       |

### purchase\_order (8 events)

| Event                               | Description                       |
| ----------------------------------- | --------------------------------- |
| `purchase_order.created`            | A purchase order is created       |
| `purchase_order.updated`            | Purchase order fields are updated |
| `purchase_order.approved`           | A purchase order is approved      |
| `purchase_order.rejected`           | A purchase order is rejected      |
| `purchase_order.received`           | All items on a PO are received    |
| `purchase_order.partially_received` | Some items on a PO are received   |
| `purchase_order.cancelled`          | A purchase order is cancelled     |
| `purchase_order.closed`             | A purchase order is closed        |

### vendor\_bill (8 events)

| Event                     | Description                                                                                                                                       |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `vendor_bill.created`     | A vendor bill is created                                                                                                                          |
| `vendor_bill.updated`     | Vendor bill fields are updated                                                                                                                    |
| `vendor_bill.approved`    | A vendor bill is approved                                                                                                                         |
| `vendor_bill.rejected`    | A vendor bill is rejected                                                                                                                         |
| `vendor_bill.posted`      | A vendor bill is posted to the GL                                                                                                                 |
| `vendor_bill.paid`        | A vendor bill is paid                                                                                                                             |
| `vendor_bill.voided`      | A vendor bill is voided                                                                                                                           |
| `vendor_bill.written_off` | A vendor bill balance is written off as income recognition (GAAP: FASB ASC 405-20). Payload includes `amount_written_off` and `journal_entry_id`. |

### journal\_entry and period (5 events)

| Event                           | Description                                                                                                                                                                                                                                                                                                                                                                      |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `journal_entry.posted`          | A journal entry is posted to the GL                                                                                                                                                                                                                                                                                                                                              |
| `journal_entry.reversed`        | A journal entry is reversed                                                                                                                                                                                                                                                                                                                                                      |
| `journal_entry.source_relinked` | A migrated journal entry's `source_id` has been corrected from a source-system integer to the Arcus order/invoice UUID (MIGRATION-GL-DIMENSIONS-REPORT-READY 2026-05-21; extended 2026-05-27 to also fire when `source_type` is upgraded from VERSA\_MIGRATION to a canonical value or `description` is enriched with the order number). Payload includes the journal\_entry id. |
| `period.closed`                 | An accounting period is closed                                                                                                                                                                                                                                                                                                                                                   |
| `period.reopened`               | A closed accounting period is reopened                                                                                                                                                                                                                                                                                                                                           |

### fulfillment (7 events)

| Event                         | Description                               |
| ----------------------------- | ----------------------------------------- |
| `fulfillment.created`         | A fulfillment package is created          |
| `fulfillment.label_purchased` | A shipping label is purchased             |
| `fulfillment.label_voided`    | A shipping label is voided                |
| `fulfillment.shipped`         | A package is marked shipped               |
| `fulfillment.in_transit`      | Carrier reports the package is in transit |
| `fulfillment.delivered`       | Carrier reports delivery                  |
| `fulfillment.exception`       | A carrier delivery exception occurs       |

### return (5 events)

| Event              | Description                    |
| ------------------ | ------------------------------ |
| `return.created`   | An RMA is created              |
| `return.received`  | Return items are received back |
| `return.disposed`  | Returned items are disposed    |
| `return.refunded`  | A return is refunded           |
| `return.cancelled` | An RMA is cancelled            |

### connector (7 events)

| Event                       | Description                                      |
| --------------------------- | ------------------------------------------------ |
| `connector.connected`       | A connector (Shopify, Amazon, etc.) is connected |
| `connector.disconnected`    | A connector is disconnected                      |
| `connector.sync_started`    | A connector sync run starts                      |
| `connector.sync_completed`  | A connector sync run completes successfully      |
| `connector.sync_failed`     | A connector sync run fails                       |
| `connector.token_refreshed` | A connector OAuth token is refreshed             |
| `connector.token_expired`   | A connector OAuth token expires                  |

### migration (10 events)

<Note>
  Migration events are registered but only fire when the `API-RESOURCE-MIGRATION` feature is
  complete. Until then, subscribers see zero `migration.*` deliveries.
</Note>

| Event                           | Description                               |
| ------------------------------- | ----------------------------------------- |
| `migration.batch_started`       | A migration batch import starts           |
| `migration.batch_completed`     | A migration batch import completes        |
| `migration.batch_failed`        | A migration batch import fails            |
| `migration.cutover_initiated`   | A migration cutover is initiated          |
| `migration.cutover_verified`    | A cutover verification step passes        |
| `migration.cutover_completed`   | Cutover completes successfully            |
| `migration.cutover_rolled_back` | A cutover is rolled back                  |
| `migration.snapshot_taken`      | A pre-cutover data snapshot is taken      |
| `migration.freeze_engaged`      | The source system write freeze is engaged |
| `migration.unfreeze_engaged`    | The write freeze is lifted                |

### webhook (1 event)

| Event          | Description                                       |
| -------------- | ------------------------------------------------- |
| `webhook.test` | Synthetic test event sent by the `/test` endpoint |
