Register Your Endpoint
Generate a signing secret
Generate the secret used to sign every event.The response returns the secret:
Event Payload
Quidkey sends a Stripe-styleEvent envelope, so existing Stripe tooling can be reused. The payment object lives at data.object.
On the wire,
amount is a string holding a stringified integer of minor units ("1999" = £19.99), and the fees numeric fields (total_fees, each fee’s amount, rate_value) are strings too. Parse them before doing arithmetic.| Field | Description |
|---|---|
id | Unique event ID (evt_...). Use this to de-duplicate: see below. |
object | Always event. |
created | Event creation time, Unix epoch seconds. |
type | The event type. See the catalog. |
data.object.id | The payment request ID. |
data.object.amount | Amount in minor units, sent as a stringified integer ("1999" = £19.99). See Amounts & Currencies. |
data.object.currency | ISO 4217 currency code. |
data.object.status | Underlying payment status. |
data.object.test | true for sandbox payments. See Testing. |
data.object.metadata | Includes order_id, payment_token, and bank_name. |
data.object.fees | Fee breakdown. Included only on succeeded events. |
Verify Every Event
Quidkey signs each delivery with an HMAC. You must verify the signature before trusting or parsing the payload, otherwise anyone who discovers your URL could forge events.Signature Headers
Each request includes these headers:| Header | Description |
|---|---|
Stripe-Signature | t=<unix-ts>,v1=<hex-hmac> |
X-Signature | Same value as Stripe-Signature (use either) |
X-Timestamp | Unix epoch seconds (the t value) |
X-Client-Id | Your client_id |
Stripe-Signature and X-Signature carry the identical value. Use Stripe-Signature with the Stripe SDK, or either header with a custom verifier.How the Signature Is Computed
The signature is an HMAC SHA-256 over the string`${timestamp}.${rawBody}` (the timestamp, a literal dot, then the raw request body), keyed by your webhook signing secret.
Verification Code
- Stripe SDK (Node.js)
- Plain Node.js (HMAC)
- Plain Python (HMAC)
Quidkey’s envelope and signature scheme are Stripe-compatible, so the Stripe SDK verifies them directly. Pass the raw body and the
Stripe-Signature header, keyed by your full whsec_... secret.Event Catalog
| Event type | When it fires | Notes |
|---|---|---|
quidkey.payment_request.succeeded | The transaction reached completed (or received) | Includes the fees object. Fulfil the order here. |
quidkey.payment_request.failed | The payment attempt failed | No fees. |
quidkey.payment_request.canceled | The payment was cancelled | One l. See the warning below. |
quidkey.payment_request.pending | Payment is in progress | The default in-flight state. |
quidkey.payment_request.reversed | A refund or reversal occurred | Adjust your records accordingly. |
failed and reversed events carry the same envelope, without a fees object:
failed
reversed
Terminal vs transitional states.
pending is transitional and resolves to one of succeeded, failed, or canceled. succeeded is otherwise terminal, but a settled payment can still move to reversed later if it is refunded or reversed. A reversal is identified by the event type (quidkey.payment_request.reversed); there is no distinct reversed status value, so data.object.status stays the underlying transaction status (e.g. completed).Handling Events
Delivery and De-duplication
Quidkey makes a single delivery attempt per event. There is no automatic retry. If your endpoint is down or returns a non-2xx, the event is not retried automatically; you resend it manually.
Manual resend. You can resend a delivery from the Quidkey Console. A resend reuses the same
event.id and is re-signed with a fresh timestamp (so the signature and timestamp tolerance still validate).| Property | Behaviour |
|---|---|
| Delivery attempts | Single attempt; no automatic retry |
| Resend | Manual, from the Console |
Resend event.id | Same as the original → de-duplicate on it |
| Resend signature | Re-signed with a fresh timestamp |
Reconciling Missed Events
If you suspect a webhook was missed (endpoint downtime, etc.), reconcile by querying the payment’s current status directly. This protected merchant endpoint returns the authoritative state:Best Practices
Verify before you parse
Verify before you parse
Capture the raw body, verify the signature against it (full secret, including
whsec_), and only then parse the JSON. Reject anything that fails with a 400.De-duplicate on event.id
De-duplicate on event.id
Persist every processed
event.id. Resends reuse the ID, so this is what keeps fulfilment exactly-once.Respond fast, work async
Respond fast, work async
Return
200 quickly and do heavy work (fulfilment, emails) asynchronously. A slow handler can time out the delivery.Enforce a timestamp tolerance
Enforce a timestamp tolerance
Reject events whose
X-Timestamp is outside ~300 seconds of now to defend against replay attacks.Next Steps
Testing
Trigger and verify test webhooks
Errors
Status codes and the error envelope
Amounts & Currencies
How amounts appear in payloads
Accept a Payment (Embedded)
Build the payment flow that triggers these events