Skip to content

Webhooks overview

How Topiic delivers signed events to your server, and what to do with them.

A webhook is an HTTP POST Topiic makes to a URL you control whenever something interesting happens — checkout outcomes, subscription state changes, payment results. The body is JSON; the headers carry an HMAC signature and an idempotency key.

In the Topiic portal under Settings → API keys, find the row for your API key and:

  1. Set Endpoint URL to an HTTPS URL on your server. (HTTP is rejected outside development.)
  2. Tick the events you want to receive.
  3. Toggle Enabled on.
  4. Save.

You can change the URL or the subscribed events at any time. Changes apply to events produced after the save — events already enqueued retain the URL they were enqueued with.

POST /webhooks/topiic HTTP/1.1
Host: your-app.example.com
Content-Type: application/json
User-Agent: Topiic-Webhooks/1.0
Topiic-Event-Id: 4f8c2a14-7b3d-4e9c-8d6f-1a2b3c4d5e6f
Topiic-Idempotency-Key: 4f8c2a14-7b3d-4e9c-8d6f-1a2b3c4d5e6f
Topiic-Signature: t=1769472312,v1=8f2a1c4b9d6e3a7f0c5b8a1d2e4f6c9b7a3d5e8f0c1a4b7d9e2c6a8b3d5f7e9c
{
"id": "4f8c2a14-7b3d-4e9c-8d6f-1a2b3c4d5e6f",
"type": "checkout.completed",
"createdAt": "2026-05-27T10:25:12.123Z",
"data": {
"sessionId": "",
"externalRef": "cust_42",
"memberId": "",
"paymentMethodId": "",
"subscriptionId": "",
"planId": ""
}
}
HeaderMeaning
Topiic-Event-IdThe event’s stable identifier. Same value across all delivery attempts for this event. Use it as your idempotency key.
Topiic-Idempotency-KeyMirrors Topiic-Event-Id. Provided separately so your generic webhook receiver can pick up “the” idempotency key without knowing the sender.
Topiic-SignatureHMAC-SHA256 over <timestamp>.<body>. See Verifying signatures.

Every event body has the same outer shape:

{
"id": "<uuid>",
"type": "<event.type>",
"createdAt": "<ISO-8601 UTC>",
"data": { /* event-specific */ }
}

The data shape depends on the event type — see the event reference.

TypeFired when
checkout.completedA hosted-checkout session provisioned member + payment method + subscription.
checkout.failedA hosted-checkout session was cancelled, capture failed, or provisioning errored.
subscription.pausedAn active subscription has been paused. Billing is suspended.
subscription.resumedA paused subscription has been resumed. Billing restarts.
subscription.cancelledA subscription has been cancelled. Billing stops permanently.
payment.succeededA recurring or retry charge was approved by the gateway.
payment.failedA recurring or retry charge was declined.
payment.refundedA charge was fully or partially refunded.

More event types will be added over time — subscribe to only the types you handle, and treat unknown types as a no-op (don’t 500).

Checkout events (checkout.*) deliver only to the API key whose signing secret produced the original deep link. Every other event fans out: it’s delivered to every API key on the merchant that has webhooks enabled and is subscribed to that event type. If three integrations all want to know when a subscription is cancelled, they each get their own delivery with their own signature.

  1. Verify the signature — see Verifying signatures. Reject anything that doesn’t verify with 401.
  2. Check the Topiic-Event-Id against your event log. If you’ve seen it before, return 200 OK and stop. Same event id may arrive more than once.
  3. Apply the side effect in a transaction that also records the event id, so step 2 is meaningful on retry.
  4. Return a 2xx response within 10 seconds. Anything else triggers a retry on backoff. Return body is ignored — 200 with empty body is ideal.

The first attempt arrives within a few seconds of the triggering action. Outbound POSTs have a 10-second timeout — if your server doesn’t respond in time the attempt is counted as failed and a retry is scheduled.

A delivery is silently skipped if:

  • The API key has no webhook URL configured.
  • The API key has webhooks disabled.
  • The event type is not in the API key’s subscribed events list.

These are not errors — they’re a configured “I don’t want this”. No retries, no warning.

Topiic stores the full delivery log: one row per attempt with the request body, response status, response body (capped at 8KB), and the next-attempt timestamp. Fetch it via GET /api/Webhooks/deliveries or the portal UI.

A manual replay (re-POST a recorded event) is available from the portal and via POST /api/Webhooks/deliveries/{id}/replay. Use it after fixing a receiver bug.