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.
Configure your endpoint
Section titled “Configure your endpoint”In the Topiic portal under Settings → API keys, find the row for your API key and:
- Set Endpoint URL to an HTTPS URL on your server. (HTTP is rejected outside development.)
- Tick the events you want to receive.
- Toggle Enabled on.
- 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.
Configure your endpoint via the API
Section titled “Configure your endpoint via the API”The portal is the quickest way to get started, but if you provision many merchants or prefer to keep webhook configuration in code, register it programmatically instead:
PUT /api/ApiKeys/{id}/webhook HTTP/1.1Host: api.topiic.com.auAuthorization: Bearer tpk_…Content-Type: application/json
{ "id": "<your-api-key-id>", "merchantId": "<your-merchant-id>", "webhookUrl": "https://your-app.example.com/webhooks/topiic", "webhookEvents": ["checkout.completed", "checkout.failed"], "webhookEnabled": true}Authenticate with your API key as usual. The {id} path segment is your API key id - the same akid you already embed in deep links, so you don’t need to look it up first.
| Field | Required | Notes |
|---|---|---|
id | yes | Your API key id. Must match the {id} in the path. |
merchantId | yes | The merchant that owns the key (the mid from your deep-link payload). The call 404s if the key/merchant pair doesn’t match. |
webhookUrl | no | HTTPS URL to deliver to. Send null to clear it. |
webhookEvents | no | The full list of event types to subscribe to. This replaces any previously configured list - it is not merged. Unknown types are rejected with 400. |
webhookEnabled | no | Toggles delivery. Ignored (forced off) unless webhookUrl is set. |
The call is idempotent - sending the same configuration again is a no-op, so it’s safe to run on every deploy to keep the endpoint in sync. A 200 returns the key’s current webhook configuration; a 404 means the id/merchantId pair didn’t resolve to one of your keys.
What a delivery looks like
Section titled “What a delivery looks like”POST /webhooks/topiic HTTP/1.1Host: your-app.example.comContent-Type: application/jsonUser-Agent: Topiic-Webhooks/1.0Topiic-Event-Id: 4f8c2a14-7b3d-4e9c-8d6f-1a2b3c4d5e6fTopiic-Idempotency-Key: 4f8c2a14-7b3d-4e9c-8d6f-1a2b3c4d5e6fTopiic-Signature: t=1769472312,v1=8f2a1c4b9d6e3a7f0c5b8a1d2e4f6c9b7a3d5e8f0c1a4b7d9e2c6a8b3d5f7e9c
{ "id": "4f8c2a14-7b3d-4e9c-8d6f-1a2b3c4d5e6f", "type": "checkout.completed", "createdAt": "2026-05-27T10:25:12.123Z", "merchantId": "e7d2f1a8-9c4b-4d62-8a3f-1b5c7e9d0f24", "data": { "sessionId": "…", "externalRef": "cust_42", "memberId": "…", "paymentMethodId": "…", "subscriptionId": "…", "planId": "…" }}Headers
Section titled “Headers”| Header | Meaning |
|---|---|
Topiic-Event-Id | The event’s stable identifier. Same value across all delivery attempts for this event. Use it as your idempotency key. |
Topiic-Idempotency-Key | Mirrors Topiic-Event-Id. Provided separately so your generic webhook receiver can pick up “the” idempotency key without knowing the sender. |
Topiic-Signature | HMAC-SHA256 over <timestamp>.<body>. See Verifying signatures. |
Envelope
Section titled “Envelope”Every event body has the same outer shape:
{ "id": "<uuid>", "type": "<event.type>", "createdAt": "<ISO-8601 UTC>", "merchantId": "<uuid>", "data": { /* event-specific */ }}merchantId is the merchant the event belongs to - useful if a single receiver serves several Topiic merchants. The data shape depends on the event type - see the event reference.
Available event types
Section titled “Available event types”| Type | Fired when |
|---|---|
checkout.completed | A hosted-checkout session provisioned member + payment method + subscription. |
checkout.failed | A hosted-checkout session was cancelled, capture failed, or provisioning errored. |
subscription.paused | An active subscription has been paused. Billing is suspended. |
subscription.resumed | A paused subscription has been resumed. Billing restarts. |
subscription.cancelled | A subscription has been cancelled. Billing stops permanently. |
payment.succeeded | A recurring or retry charge was approved by the gateway. |
payment.failed | A recurring or retry charge was declined. |
payment.refunded | A charge was fully or partially refunded. |
payment_method.updated | An existing member captured a new default payment method via a hosted “update card” checkout. |
invoice.created | An invoice was raised for a member. |
invoice.sent | An invoice was issued/sent to the member. |
invoice.paid | An invoice was settled. |
invoice.voided | Reserved. Subscribable, but not emitted yet - no delivery fires today. |
The full machine-readable list is served by GET /api/Webhooks/event-types. 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).
Fan-out behaviour
Section titled “Fan-out behaviour”Checkout-session events (checkout.completed, checkout.failed, and payment_method.updated) 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.
What your handler must do
Section titled “What your handler must do”- Verify the signature - see Verifying signatures. Reject anything that doesn’t verify with
401. - Check the
Topiic-Event-Idagainst your event log. If you’ve seen it before, return200 OKand stop. Same event id may arrive more than once. - Apply the side effect in a transaction that also records the event id, so step 2 is meaningful on retry.
- Return a 2xx response within 10 seconds. Anything else triggers a retry on backoff. Return body is ignored -
200with empty body is ideal.
When deliveries happen
Section titled “When deliveries happen”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.
When deliveries don’t happen
Section titled “When deliveries don’t happen”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.
Inspecting delivery history
Section titled “Inspecting delivery history”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.
- Verifying webhook signatures - code samples for the algorithm.
- Retries & replay - backoff schedule and how to recover from a downed receiver.
- Event reference - payload shape per event type.