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

> ## Agent Instructions
> Describe only the public 123hub merchant API documented on this site. The primary endpoint is POST /public/api/multihub/v1.
> Preserve API method names, field names, header names, and error codes exactly as documented. Do not invent endpoints or parameters.
> Treat every amount as an integer in minor units unless a page explicitly states otherwise.
> Never expose, request, or fabricate a merchant secret key. The SDK pages contain reference implementations, not official SDK packages.

# Support webhooks

> Receive durable ticket, refund, and chargeback change notifications safely

# Support webhooks

Support webhooks notify your backend when merchant-visible ticket, refund, or chargeback state changes. They use the existing durable webhook delivery, signature, retry, and deduplication pipeline.

<Info>
  **Prerequisites:** configure a public HTTPS webhook URL and explicitly select
  the support events you need. New event selectors are opt-in and are never
  added to an existing subscription automatically.
</Info>

## Events

| Event selector           | Meaning                                                 |
| ------------------------ | ------------------------------------------------------- |
| `ticket.created`         | A public ticket was created                             |
| `ticket.updated`         | Merchant-visible ticket data or comment content changed |
| `ticket.status_changed`  | The public ticket status changed                        |
| `ticket.comment.created` | A merchant-visible comment was added                    |
| `ticket.reopened`        | A resolved/closed ticket was reopened                   |
| `refund.requested`       | A monetary refund request was durably accepted          |
| `refund.status_changed`  | The refund request status changed                       |
| `chargeback.created`     | A chargeback became visible to the merchant             |

Configure support event selectors through Developer Center or during onboarding. Existing payment event selections and per-payment callback URLs are unchanged.

## Payload

The signed wire body contains the durable event ID, occurrence time, resource type, public identifiers, and a small allowlist of state/reference timestamps:

```json theme={null}
{
  "id": "11111111-1111-4111-8111-111111111111",
  "created_at": "2026-07-15T12:00:00.000Z",
  "data": {
    "event_type": "ticket.reopened",
    "type": "ticket",
    "identifiers": {
      "c_id": "merchant-ticket-1",
      "h_id": "22222222-2222-4222-8222-222222222222"
    },
    "status": "open",
    "previous_status": "resolved",
    "payment_h_id": "33333333-3333-4333-8333-333333333333",
    "updated_at": "2026-07-15T12:00:00.000Z"
  }
}
```

`data.event_type` is the subscribed selector and `data.type` is `ticket`, `refund`, or `chargeback`. `c_id` is omitted for resources without a merchant identifier. Depending on the event, the safe optional fields are `status`, `previous_status`, `payment_h_id`, `payment_c_id`, `reference`, `created_at`, `updated_at`, `resolved_at`, `closed_at`, `requested_at`, and `processed_at`.

<Warning>
  Webhooks intentionally omit comment bodies, descriptions, PII, staff
  identities, assignment/team data, internal tags, raw metadata, storage keys,
  and audit payloads. Query the relevant `*.get` or comments method after
  receiving a notification when you need current details.
</Warning>

## Headers and signatures

| Header                   | Description                                                                |
| ------------------------ | -------------------------------------------------------------------------- |
| `X-Webhook-Id`           | Durable event/delivery identifier; matches the webhook event ID            |
| `X-Webhook-Timestamp`    | ISO 8601 delivery timestamp                                                |
| `X-Webhook-Nonce`        | Per-delivery nonce                                                         |
| `X-Webhook-Signature-V3` | Preferred `HMAC-SHA512(timestamp.nonce.webhookId.rawBody, webhook secret)` |
| `X-Webhook-Signature-V2` | Compatibility `SHA512(timestamp + rawBody + webhook secret)`               |
| `X-Data-Hash`            | Compatibility `SHA512(rawBody + webhook secret)`                           |

Verify the exact raw request bytes before JSON parsing. Prefer V3, require a recent timestamp, and remember processed event IDs. The same logical event can be delivered more than once with new delivery metadata.

## Verify a delivery

<CodeGroup>
  ```bash cURL / OpenSSL theme={null}
  # Save the exact body as webhook-body.json and read headers without editing them.
  EXPECTED=$(
    { printf '%s.%s.%s.' "$X_WEBHOOK_TIMESTAMP" "$X_WEBHOOK_NONCE" "$X_WEBHOOK_ID"; cat webhook-body.json; } |
      openssl dgst -sha512 -hmac 'your_webhook_secret' -r | awk '{print $1}'
  )
  test "$EXPECTED" = "$X_WEBHOOK_SIGNATURE_V3" || { echo 'invalid signature'; exit 1; }
  ```

  ```python Python theme={null}
  import hashlib, hmac, json
  from datetime import datetime, timezone

  raw_body = request.get_data()
  timestamp = request.headers["X-Webhook-Timestamp"]
  nonce = request.headers["X-Webhook-Nonce"]
  event_id = request.headers["X-Webhook-Id"]
  received = request.headers["X-Webhook-Signature-V3"]
  message = b".".join([timestamp.encode(), nonce.encode(), event_id.encode(), raw_body])
  expected = hmac.new(b"your_webhook_secret", message, hashlib.sha512).hexdigest()
  if not hmac.compare_digest(expected, received):
      return {"error": "invalid signature"}, 400
  payload = json.loads(raw_body)
  enqueue_once(payload["id"], payload)
  return {"received": True}, 200
  ```

  ```javascript Node.js theme={null}
  import crypto from "node:crypto";

  function validV3(rawBody, headers, secret) {
    const timestamp = headers["x-webhook-timestamp"];
    const nonce = headers["x-webhook-nonce"];
    const eventId = headers["x-webhook-id"];
    const received = headers["x-webhook-signature-v3"] || "";
    const prefix = Buffer.from(`${timestamp}.${nonce}.${eventId}.`, "utf8");
    const expected = crypto
      .createHmac("sha512", secret)
      .update(Buffer.concat([prefix, rawBody]))
      .digest("hex");
    return (
      /^[a-f0-9]{128}$/i.test(received) &&
      crypto.timingSafeEqual(
        Buffer.from(expected, "hex"),
        Buffer.from(received, "hex"),
      )
    );
  }

  // Use express.raw({ type: "application/json" }), validate timestamp skew,
  // then JSON.parse(req.body) only after validV3(req.body, req.headers, secret).
  ```
</CodeGroup>

Also reject missing replay headers and timestamps outside your accepted window
(five minutes is recommended). Rotate an exposed webhook secret immediately;
API-key rotation does not change callback verification.

## Delivery and idempotency

The resource owner commits the merchant-visible change and an outbox event atomically. Gateway then persists one webhook delivery per selected subscription and uses the durable event ID for deduplication. Temporary NATS, process, or HTTP failures are retried; repeated infrastructure failures are retained for operational recovery.

Merchant endpoints must still assume at-least-once, potentially out-of-order delivery:

1. Verify V3 and timestamp before parsing.
2. Insert `payload.id` into a table with a unique constraint.
3. Return HTTP 2xx quickly after durable local enqueue.
4. Fetch current state by `data.identifiers.h_id` when processing asynchronously.
5. Ignore older state notifications when your stored resource version/timestamp is newer.

Do not use `request_id` as webhook dedupe state; use the top-level event `id`.

## Retry behavior

Any HTTP 2xx response completes an attempt. Network errors, timeouts, URL safety failures, circuit-breaker blocks, and non-2xx responses are failures. Retries use exponential backoff with jitter, so delivery order is not guaranteed.

## Error handling

| Symptom                  | Recovery                                                                                                   |
| ------------------------ | ---------------------------------------------------------------------------------------------------------- |
| Signature mismatch       | Verify raw bytes, webhook secret, header casing, and V3 concatenation order                                |
| Duplicate `id`           | Return 2xx without replaying the business side effect                                                      |
| Unknown resource locally | Query the corresponding public `*.get` method; creation webhook can arrive before another local projection |
| Missing optional field   | Fetch current state; event payloads are intentionally minimal                                              |
| Repeated delivery        | Check that your endpoint returns 2xx within its timeout and does not perform slow work inline              |

## Best practices

* Use a dedicated HTTPS route with raw-body capture and strict body limits.
* Store the event `id`, resource public IDs, and received timestamp before acknowledging.
* Keep one handler per event selector even if several selectors share a URL.
* Never log signatures, raw comment content, presigned URLs, API secrets, or webhook secrets.
