Webhooks
- 12 endpoints
Get event types#
GET /webhooks/
Lists the event types an endpoint can receive, in one page.
- Auth
- X-API-Key: any key
- On-Behalf-Of
- Honored: acts in the member's space
- Idempotency-Key
- Ignored
- Success
- 200
request.sh
curl https://api.qbitflow.app/v2/webhooks/event-types \ -H "X-API-Key: $QBITFLOW_API_KEY"# 200: an array of TypeInfoResponse
A page: { "items": TypeInfo[], "hasMore": …, "nextCursor"?: … }. Pass nextCursor back as cursor for the next page; it is absent when hasMore is false.
typestringOne of
payment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expiredwebhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
descriptionstring
Errors
The common codes only, as any route (what each means):
- 401unauthorized
- 403forbidden
- 403policy_disabled
- 404not_found
- 429rate_limit_exceeded
- 500internal
- 504timeout
List endpoints#
GET /webhooks/
A page of the webhook endpoints of the request's space (at most 10), newest first, with their health.
- Auth
- X-API-Key: any keyA member's key needs the members.webhooks policy
- On-Behalf-Of
- Honored: acts in the member's space
- Idempotency-Key
- Ignored
- Success
- 200
request.sh
curl https://api.qbitflow.app/v2/webhooks/endpoints \ -H "X-API-Key: $QBITFLOW_API_KEY"# 200: an array of EndpointQuery parameters
limitintegerOptionalPage size: 20 when absent or ≤ 0, at most 100 (a larger one is clamped, not refused)
cursorstringOptionalThe previous page's nextCursor, verbatim; omit for the first page
max=200
Response
A page: { "items": Endpoint[], "hasMore": …, "nextCursor"?: … }. Pass nextCursor back as cursor for the next page; it is absent when hasMore is false.
13 fields of each Endpoint
uuidstringIts only id (a UUIDv7)
testbooleanWhether this is test data (testnets) or live data
urlstringeventsarray of stringThe event types it receives (GET /webhooks/event-types); empty: every type
One of
payment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expiredwebhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
includeMembersbooleanAn endpoint of the organization's shared space that also receives its members' events (userUuid names the member)
payloadVersionstringThe payload it receives: v2 (the event envelope), the only version today
One of:
v2V2 is the event envelope: {id, type, version, createdAt, test, userUuid, data}. The only version today: the field exists so that a future payload change is a new version an endpoint opts into.
descriptionstringA note for the dashboard; absent when none
createdAtstringrotatedAtstringOptionalWhen its secret last changed
disabledAtstringOptionalDisabled: it receives nothing. Set after 3 days without a successful delivery (an email tells the space's team; live mode only: a test-mode endpoint is never disabled for failing), or by its owner; PATCH {"enabled": true} enables it again
disabledReasonstringOptionalDisabledReason is who disabled it: failing (QBitFlow, every delivery failing for too long), owner (PATCH {"enabled": false}) or closed (its member removed, its organization closed). Omitted while enabled, and for endpoints disabled before it was recorded
One of:
failingQBitFlow: every delivery failing for too long (config.WebhookHealth)ownerIts owner: PATCH {"enabled": false}closedIts member removed, or its organization closed
failingSincestringOptionalFailingSince is, for an enabled endpoint whose deliveries fail, its first failed attempt since its last successful one (or since it was enabled again): failing for a day, its team is emailed; for 3, it is disabled (live mode only). Omitted while it delivers
lastDeliveredAtstringOptionalLastDeliveredAt is when it last received an event (a successful delivery). Omitted if it never did
Errors
- 403policy_
disabled a member doing what their organization's policy doesn't allow them, reads of what the policy governs included
Also the common codes, as any route (what each means):
- 400validation_failed
- 401unauthorized
- 403forbidden
- 404not_found
- 429rate_limit_exceeded
- 500internal
- 504timeout
Create endpoint#
POST /webhooks/
Adds a webhook endpoint to the request's space (at most 10, 409 beyond). Its secret is only in this response (the dashboard can show it again).
- Auth
- X-API-Key: any keyA member's key needs the members.webhooks policy
- On-Behalf-Of
- Honored: acts in the member's space
- Idempotency-Key
- Accepted: send one per operation
- Success
- 201
request.sh
curl https://api.qbitflow.app/v2/webhooks/endpoints \ -H "X-API-Key: $QBITFLOW_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "url": "https://shop.example.com/webhooks/qbitflow", "events": [ "payment.completed", "checkout.expired", "subscription.statusChanged" ], "description": "Order fulfilment" }'# 201: a CreatedEndpointconst endpoint = await client. webhooks. endpoints. create( { url: 'https://shop.example.com/webhooks/qbitflow', // Left out: every type, including the ones added later. events: [ EventType.PaymentCompleted, EventType.CheckoutExpired, EventType. SubscriptionStatusChanged, ], description: 'Order fulfilment',});// The whsec_… signing secret is returned only this once: store it now.await storeSecret( 'QBITFLOW_WEBHOOK_SECRET', endpoint.secret);console.log(`endpoint ${endpoint.uuid} created`);webhook_endpoints.py
created = client. webhooks. endpoints. create( url="https://shop.example.com/webhooks/qbitflow", events=[ # None: every type, including the ones added later qbitflow. EventType. PAYMENT_COMPLETED, qbitflow. EventType. CHECKOUT_EXPIRED, qbitflow. EventType. SUBSCRIPTION_STATUS_CHANGED, ], description="Order fulfilment",)# The whsec_… secret is returned only this once: put it in your secret store now# (QBITFLOW_WEBHOOK_SECRET for your webhook handler).print(created.uuid, created.secret)created, err := client. Webhooks. Endpoints. Create( ctx, &qbitflow. CreateWebhookEndpointParams{ URL: "https://shop.example.com/webhooks/qbitflow", Events: []qbitflow.EventType{ // nil: every type, including the ones added later qbitflow. EventTypePaymentCompleted, qbitflow. EventTypeCheckoutExpired, qbitflow. EventTypeSubscriptionStatusChanged, },})if err != nil { log.Fatal(err)}// The whsec_… secret is returned only this once: put it in your secret store now, and give// it to your webhook handler as QBITFLOW_WEBHOOK_SECRET.fmt.Println("endpoint", created.UUID, "secret", created.Secret)$endpoint = $client->webhooks->endpoints->create( new \QBitFlow\Params\CreateWebhookEndpointParams( url: 'https://shop.example.com/webhooks/qbitflow', events: [ // null: every type, including the ones added later \QBitFlow\Enums\EventType::PAYMENT_COMPLETED, \QBitFlow\Enums\EventType::CHECKOUT_EXPIRED, \QBitFlow\Enums\EventType::SUBSCRIPTION_STATUS_CHANGED, ],));// The whsec_… secret is returned only this once: put it in your secret store now.echo "Endpoint {$endpoint->uuid}: QBITFLOW_WEBHOOK_SECRET={$endpoint->secret}\n";Body
urlstringRequiredWhere the events are posted: https in live mode, a public address
http_urlmax=2048eventsarray of stringOptionalThe event types to receive (GET /webhooks/event-types); empty or absent: every type
max=20One of
payment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expiredwebhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
includeMembersbooleanOptionalOrganization endpoints only: also receive the members' events (userUuid names the member). Absent: true for an organization endpoint, false for a member's; false to receive only the organization's own events
descriptionstringOptionalA note for the dashboard
textmax=200
Response
A CreatedEndpoint.
14 fields of CreatedEndpoint
uuidstringIts only id (a UUIDv7)
testbooleanWhether this is test data (testnets) or live data
urlstringeventsarray of stringThe event types it receives (GET /webhooks/event-types); empty: every type
One of
payment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expiredwebhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
includeMembersbooleanAn endpoint of the organization's shared space that also receives its members' events (userUuid names the member)
payloadVersionstringThe payload it receives: v2 (the event envelope), the only version today
One of:
v2V2 is the event envelope: {id, type, version, createdAt, test, userUuid, data}. The only version today: the field exists so that a future payload change is a new version an endpoint opts into.
descriptionstringA note for the dashboard; absent when none
createdAtstringrotatedAtstringOptionalWhen its secret last changed
disabledAtstringOptionalDisabled: it receives nothing. Set after 3 days without a successful delivery (an email tells the space's team; live mode only: a test-mode endpoint is never disabled for failing), or by its owner; PATCH {"enabled": true} enables it again
disabledReasonstringOptionalDisabledReason is who disabled it: failing (QBitFlow, every delivery failing for too long), owner (PATCH {"enabled": false}) or closed (its member removed, its organization closed). Omitted while enabled, and for endpoints disabled before it was recorded
One of:
failingQBitFlow: every delivery failing for too long (config.WebhookHealth)ownerIts owner: PATCH {"enabled": false}closedIts member removed, or its organization closed
failingSincestringOptionalFailingSince is, for an enabled endpoint whose deliveries fail, its first failed attempt since its last successful one (or since it was enabled again): failing for a day, its team is emailed; for 3, it is disabled (live mode only). Omitted while it delivers
lastDeliveredAtstringOptionalLastDeliveredAt is when it last received an event (a successful delivery). Omitted if it never did
secretstringwhsec_…: verifies the signature of its deliveries (GET …/secret shows it again in the dashboard)
Errors
- 403policy_
disabled a member doing what their organization's policy doesn't allow them, reads of what the policy governs included
- 409idempotency_
key_ in_ use the
Idempotency-Key's first request is still running (idempotency): retry once it answered - 422idempotency_
key_ reused the
Idempotency-Keywas used for another request (idempotency): send a new key - 413request_
too_ large an
Idempotentroute's body over 1 MiB (idempotency)
Also the common codes, as any route (what each means):
- 400validation_failed
- 401unauthorized
- 403forbidden
- 404not_found
- 429rate_limit_exceeded
- 500internal
- 504timeout
Get endpoint#
GET /webhooks/
An endpoint of the request's space (another space's is not found).
- Auth
- X-API-Key: any keyA member's key needs the members.webhooks policy
- On-Behalf-Of
- Honored: acts in the member's space
- Idempotency-Key
- Ignored
- Success
- 200
request.sh
curl "https://api.qbitflow.app/v2/webhooks/endpoints/$ENDPOINT_UUID" \ -H "X-API-Key: $QBITFLOW_API_KEY"# 200: an EndpointPath parameters
uuidstringRequired
Response
A Endpoint.
13 fields of Endpoint
uuidstringIts only id (a UUIDv7)
testbooleanWhether this is test data (testnets) or live data
urlstringeventsarray of stringThe event types it receives (GET /webhooks/event-types); empty: every type
One of
payment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expiredwebhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
includeMembersbooleanAn endpoint of the organization's shared space that also receives its members' events (userUuid names the member)
payloadVersionstringThe payload it receives: v2 (the event envelope), the only version today
One of:
v2V2 is the event envelope: {id, type, version, createdAt, test, userUuid, data}. The only version today: the field exists so that a future payload change is a new version an endpoint opts into.
descriptionstringA note for the dashboard; absent when none
createdAtstringrotatedAtstringOptionalWhen its secret last changed
disabledAtstringOptionalDisabled: it receives nothing. Set after 3 days without a successful delivery (an email tells the space's team; live mode only: a test-mode endpoint is never disabled for failing), or by its owner; PATCH {"enabled": true} enables it again
disabledReasonstringOptionalDisabledReason is who disabled it: failing (QBitFlow, every delivery failing for too long), owner (PATCH {"enabled": false}) or closed (its member removed, its organization closed). Omitted while enabled, and for endpoints disabled before it was recorded
One of:
failingQBitFlow: every delivery failing for too long (config.WebhookHealth)ownerIts owner: PATCH {"enabled": false}closedIts member removed, or its organization closed
failingSincestringOptionalFailingSince is, for an enabled endpoint whose deliveries fail, its first failed attempt since its last successful one (or since it was enabled again): failing for a day, its team is emailed; for 3, it is disabled (live mode only). Omitted while it delivers
lastDeliveredAtstringOptionalLastDeliveredAt is when it last received an event (a successful delivery). Omitted if it never did
Errors
- 403policy_
disabled a member doing what their organization's policy doesn't allow them, reads of what the policy governs included
Also the common codes, as any route (what each means):
- 400validation_failed
- 401unauthorized
- 403forbidden
- 404not_found
- 429rate_limit_exceeded
- 500internal
- 504timeout
Update endpoint#
PATCH /webhooks/
Changes an endpoint of the request's space; absent fields stay as they are.
- Auth
- X-API-Key: any keyA member's key needs the members.webhooks policy
- On-Behalf-Of
- Honored: acts in the member's space
- Idempotency-Key
- Ignored
- Success
- 200
request.sh
curl -X PATCH "https://api.qbitflow.app/v2/webhooks/endpoints/$ENDPOINT_UUID" \ -H "X-API-Key: $QBITFLOW_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "enabled": true }'# 200: an EndpointPath parameters
uuidstringRequired
Body
urlstringOptionalhttp_urlmax=2048eventsarray of stringOptional[] for every type
max=20One of
payment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expiredwebhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
includeMembersbooleanOptionaldescriptionstringOptional"" to clear it
max=200len=0|textpayloadVersionstringOptionalThe payload it receives: v2, the only version today
oneof=v2One of:
v2V2 is the event envelope: {id, type, version, createdAt, test, userUuid, data}. The only version today: the field exists so that a future payload change is a new version an endpoint opts into.
enabledbooleanOptionalfalse pauses the endpoint (it receives nothing, deliveries in progress stop); true enables it again, also after it was disabled for failing
Response
A Endpoint.
13 fields of Endpoint
uuidstringIts only id (a UUIDv7)
testbooleanWhether this is test data (testnets) or live data
urlstringeventsarray of stringThe event types it receives (GET /webhooks/event-types); empty: every type
One of
payment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expiredwebhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
includeMembersbooleanAn endpoint of the organization's shared space that also receives its members' events (userUuid names the member)
payloadVersionstringThe payload it receives: v2 (the event envelope), the only version today
One of:
v2V2 is the event envelope: {id, type, version, createdAt, test, userUuid, data}. The only version today: the field exists so that a future payload change is a new version an endpoint opts into.
descriptionstringA note for the dashboard; absent when none
createdAtstringrotatedAtstringOptionalWhen its secret last changed
disabledAtstringOptionalDisabled: it receives nothing. Set after 3 days without a successful delivery (an email tells the space's team; live mode only: a test-mode endpoint is never disabled for failing), or by its owner; PATCH {"enabled": true} enables it again
disabledReasonstringOptionalDisabledReason is who disabled it: failing (QBitFlow, every delivery failing for too long), owner (PATCH {"enabled": false}) or closed (its member removed, its organization closed). Omitted while enabled, and for endpoints disabled before it was recorded
One of:
failingQBitFlow: every delivery failing for too long (config.WebhookHealth)ownerIts owner: PATCH {"enabled": false}closedIts member removed, or its organization closed
failingSincestringOptionalFailingSince is, for an enabled endpoint whose deliveries fail, its first failed attempt since its last successful one (or since it was enabled again): failing for a day, its team is emailed; for 3, it is disabled (live mode only). Omitted while it delivers
lastDeliveredAtstringOptionalLastDeliveredAt is when it last received an event (a successful delivery). Omitted if it never did
Errors
- 403policy_
disabled a member doing what their organization's policy doesn't allow them, reads of what the policy governs included
Also the common codes, as any route (what each means):
- 400validation_failed
- 401unauthorized
- 403forbidden
- 404not_found
- 429rate_limit_exceeded
- 500internal
- 504timeout
Delete endpoint#
DELETE /webhooks/
Removes an endpoint of the request's space (204, no body): its pending retries stop.
- Auth
- X-API-Key: any keyA member's key needs the members.webhooks policy
- On-Behalf-Of
- Honored: acts in the member's space
- Idempotency-Key
- Ignored
- Success
- 204
request.sh
curl -X DELETE "https://api.qbitflow.app/v2/webhooks/endpoints/$ENDPOINT_UUID" \ -H "X-API-Key: $QBITFLOW_API_KEY"# 200: { "message": … }Path parameters
uuidstringRequired
Response
No JSON body: the edge cases below say what it answers.
Errors
- 403policy_
disabled a member doing what their organization's policy doesn't allow them, reads of what the policy governs included
Also the common codes, as any route (what each means):
- 400validation_failed
- 401unauthorized
- 403forbidden
- 404not_found
- 429rate_limit_exceeded
- 500internal
- 504timeout
Test endpoint#
POST /webhooks/
Sends a webhook.test event to an endpoint, once, and answers how it answered. A space sends at most 30 tests an hour (429 rate_limit_exceeded).
- Auth
- X-API-Key: any keyA member's key needs the members.webhooks policy
- On-Behalf-Of
- Honored: acts in the member's space
- Idempotency-Key
- Ignored
- Success
- 200
request.sh
curl -X POST "https://api.qbitflow.app/v2/webhooks/endpoints/$ENDPOINT_UUID/test" \ -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a TestResultPath parameters
uuidstringRequired
Response
A TestResult.
deliveredbooleanThe endpoint answered 2xx
statusCodeintegerOptionalIts answer's status, if it answered
errorstringOptionalWhy it wasn't delivered
Errors
- 403policy_
disabled a member doing what their organization's policy doesn't allow them, reads of what the policy governs included
- 429rate_
limit_ exceeded rate limit hit (see below)
Also the common codes, as any route (what each means):
- 400validation_failed
- 401unauthorized
- 403forbidden
- 404not_found
- 500internal
- 504timeout
List endpoint deliveries#
GET /webhooks/
A page of the attempts to deliver events to an endpoint of the request's space, newest first, optionally only the delivered or the failed ones.
- Auth
- X-API-Key: any keyA member's key needs the members.webhooks policy
- On-Behalf-Of
- Honored: acts in the member's space
- Idempotency-Key
- Ignored
- Success
- 200
request.sh
curl "https://api.qbitflow.app/v2/webhooks/endpoints/$ENDPOINT_UUID/deliveries" \ -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a page of DeliveryAttempt ({ items, hasMore, nextCursor? })Path parameters
uuidstringRequiredThe endpoint
Query parameters
limitintegerOptionalPage size: 20 when absent or ≤ 0, at most 100 (a larger one is clamped, not refused)
cursorstringOptionalThe previous page's nextCursor, verbatim; omit for the first page
max=200deliveredbooleanOptionaltrue: only the delivered attempts; false: only the failed ones
Response
A page: { "items": DeliveryAttempt[], "hasMore": …, "nextCursor"?: … }. Pass nextCursor back as cursor for the next page; it is absent when hasMore is false.
11 fields of each DeliveryAttempt
uuidstringeventIdstringeventTypestringOne of
payment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expiredwebhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
endpointUuidstringattemptinteger1 for the first, then one more per retry or resend
deliveredbooleanThe endpoint answered with a 2xx
skippedbooleanOptionalNot posted: the endpoint is a member's, and the organization's members.webhooks policy was off for them (never retried; it doesn't count against the endpoint's health)
statusCodeintegerOptionalThe endpoint's answer; absent when it didn't answer (timeout, connection refused)
errorstringOptionalWhy it wasn't delivered: the endpoint's status and the start of its answer, or the connection error
durationMsintegerattemptedAtstring
Errors
- 403policy_
disabled a member doing what their organization's policy doesn't allow them, reads of what the policy governs included
Also the common codes, as any route (what each means):
- 400validation_failed
- 401unauthorized
- 403forbidden
- 404not_found
- 429rate_limit_exceeded
- 500internal
- 504timeout
List events#
GET /webhooks/
A page of the request's space's events, newest first, as they were delivered. The organization's space can include its members' events.
- Auth
- X-API-Key: any keyA member's key needs the members.webhooks policy
- On-Behalf-Of
- Honored: acts in the member's space
- Idempotency-Key
- Ignored
- Success
- 200
request.sh
curl https://api.qbitflow.app/v2/webhooks/events \ -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a page of Envelope ({ items, hasMore, nextCursor? })// Newest first, each exactly as delivered; one page (iterate walks them all).const page = await client.webhooks.events.list({ type: EventType.PaymentCompleted, limit: 10 });for (const event of page.items) { console.log(`${event.id} ${event.type} at ${event.createdAt}`);}if (page.hasMore) { console.log(`more: pass ${page.nextCursor} as cursor`); // opaque, never an event id}webhook_endpoints.py
page = client. webhooks. events. list( type=qbitflow. EventType. PAYMENT_COMPLETED, limit=20)for event in page.items: # newest first, each exactly as delivered print(event.id, event.type, event.created_at)if page.has_more: print("more: pass", page.next_cursor, "as cursor=") # opaque, never an event idpage, err := client.Webhooks.Events.List(ctx, &qbitflow.EventListParams{ Type: qbitflow. EventTypePaymentCompleted, Limit: 20,})if err != nil { log.Fatal(err)}for _, e := range page.Items { // newest first, each exactly as delivered fmt.Println(e.ID, e.Type, e. CreatedAt. Format( time. RFC3339))}if page.HasMore { fmt.Println("more: pass", page.NextCursor, "as Cursor") // opaque, never an event id}$page = $client->webhooks->events->list( new \QBitFlow\Params\EventListParams( type: \QBitFlow\Enums\EventType::PAYMENT_COMPLETED, limit: 20,));foreach ($page->items as $event) { // newest first, each exactly as delivered echo "{$event->id} {$event->type} {$event->createdAt->format(DATE_ATOM)}\n";}if ($page->hasMore) { echo "More: pass {$page->nextCursor} as cursor\n"; // opaque, never an event id}Query parameters
limitintegerOptionalPage size: 20 when absent or ≤ 0, at most 100 (a larger one is clamped, not refused)
cursorstringOptionalThe previous page's nextCursor, verbatim; omit for the first page
max=200typestringOptionalOnly the events of this type (GET /webhooks/event-types)
max=64One of
payment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expiredwebhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
includeMembersbooleanOptionalThe organization's space only: its members' events too
Response
A page: { "items": Envelope[], "hasMore": …, "nextCursor"?: … }. Pass nextCursor back as cursor for the next page; it is absent when hasMore is false.
idstringevt_…: the same for every endpoint and every retry of the event, and if it is ever published again (deduplicate on it)
typestringOne of
payment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expiredwebhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
versionstringv2
One of:
v2V2 is the event envelope: {id, type, version, createdAt, test, userUuid, data}. The only version today: the field exists so that a future payload change is a new version an endpoint opts into.
createdAtstringtestbooleanWhether it happened in test mode
userUuidstringOptionalThe member whose space it happened in; omitted for the organization's own
dataany JSON valueDepends on the type (GET /webhooks/event-types)
Errors
- 403policy_
disabled a member doing what their organization's policy doesn't allow them, reads of what the policy governs included
Also the common codes, as any route (what each means):
- 400validation_failed
- 401unauthorized
- 403forbidden
- 404not_found
- 429rate_limit_exceeded
- 500internal
- 504timeout
Get event#
GET /webhooks/
An event of the request's space (from the organization's space, of its members' too) and its deliveries to the space's endpoints.
- Auth
- X-API-Key: any keyA member's key needs the members.webhooks policy
- On-Behalf-Of
- Honored: acts in the member's space
- Idempotency-Key
- Ignored
- Success
- 200
request.sh
curl "https://api.qbitflow.app/v2/webhooks/events/$EVENT_ID" \ -H "X-API-Key: $QBITFLOW_API_KEY"# 200: an EventDetailPath parameters
idstringRequiredThe event's id (evt_…, its envelope's id)
max=64
Response
A EventDetail.
idstringevt_…: the same for every endpoint and every retry of the event, and if it is ever published again (deduplicate on it)
typestringOne of
payment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expiredwebhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
versionstringv2
One of:
v2V2 is the event envelope: {id, type, version, createdAt, test, userUuid, data}. The only version today: the field exists so that a future payload change is a new version an endpoint opts into.
createdAtstringtestbooleanWhether it happened in test mode
userUuidstringOptionalThe member whose space it happened in; omitted for the organization's own
dataany JSON valueDepends on the type (GET /webhooks/event-types)
deliveriesarray of EndpointDeliveryIts deliveries to the request's space's endpoints: attempted, or queued (pending); an endpoint it never went to and won't go to isn't listed
7 fields of each
EndpointDeliveryendpointUuidstringurlstringThe endpoint's URL now
deliveredbooleanOne of its attempts was answered with a 2xx
attemptsarray of DeliveryAttemptOldest first; empty while the event waits for its first attempt (pending)
11 fields of each
DeliveryAttemptuuidstringeventIdstringeventTypestringOne of
payment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expiredwebhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
endpointUuidstringattemptinteger1 for the first, then one more per retry or resend
deliveredbooleanThe endpoint answered with a 2xx
skippedbooleanOptionalNot posted: the endpoint is a member's, and the organization's members.webhooks policy was off for them (never retried; it doesn't count against the endpoint's health)
statusCodeintegerOptionalThe endpoint's answer; absent when it didn't answer (timeout, connection refused)
errorstringOptionalWhy it wasn't delivered: the endpoint's status and the start of its answer, or the connection error
durationMsintegerattemptedAtstring
pendingbooleanPending: QBitFlow still attempts it on its own. It is being retried (nextAttemptAt), waits for an earlier event of the same publication (waitingFor), or its delivery is about to start. false once delivered, abandoned after its retries, or when the endpoint was disabled (a deleted endpoint's deliveries are deleted with it: no longer listed)
nextAttemptAtstringOptionalWhile pending after a failed attempt: when the next one comes
waitingForstringOptionalWhile pending behind an earlier event of the same publication (evt_…): an endpoint gets a publication's events in order, each once the previous one is delivered or abandoned
Errors
- 403policy_
disabled a member doing what their organization's policy doesn't allow them, reads of what the policy governs included
Also the common codes, as any route (what each means):
- 400validation_failed
- 401unauthorized
- 403forbidden
- 404not_found
- 429rate_limit_exceeded
- 500internal
- 504timeout
Resend event#
POST /webhooks/
Sends an event to one of the space's endpoints that receives it, once, now, and answers how the endpoint answered (logged like any attempt). A space resends at most 100 events an hour (429 rate_limit_exceeded).
- Auth
- X-API-Key: any keyA member's key needs the members.webhooks policy
- On-Behalf-Of
- Honored: acts in the member's space
- Idempotency-Key
- Ignored
- Success
- 200
request.sh
curl "https://api.qbitflow.app/v2/webhooks/events/$EVENT_ID/resend" \ -H "X-API-Key: $QBITFLOW_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "endpointUuid": "'"$ENDPOINT_UUID"'" }'# 200: a TestResultPath parameters
idstringRequiredThe event's id (evt_…, its envelope's id)
max=64
Body
endpointUuidstringRequiredAn endpoint of the request's space that receives the event
Response
A TestResult.
deliveredbooleanThe endpoint answered 2xx
statusCodeintegerOptionalIts answer's status, if it answered
errorstringOptionalWhy it wasn't delivered
Errors
- 403policy_
disabled a member doing what their organization's policy doesn't allow them, reads of what the policy governs included
- 429rate_
limit_ exceeded rate limit hit (see below)
Also the common codes, as any route (what each means):
- 400validation_failed
- 401unauthorized
- 403forbidden
- 404not_found
- 500internal
- 504timeout
Verify webhook#
POST /webhooks/
Checks the signature of a delivery an endpoint of the request's space received (400 invalid_signature otherwise, also when older than 5 minutes). The SDKs verify locally with the same rule.
- Auth
- X-API-Key: any keyA member's key needs the members.webhooks policy
- On-Behalf-Of
- Honored: acts in the member's space
- Idempotency-Key
- Ignored
- Success
- 200
request.sh
# $RAW_BODY: the delivery as received (jq escapes its quotes); $SIGNATURE: its# QBitFlow-Signature header.jq -n \ --arg endpoint "$ENDPOINT_UUID" \ --arg body "$RAW_BODY" \ --arg signature "$SIGNATURE" \ '{endpointUuid: $endpoint, body: $body, signature: $signature}' |curl https://api.qbitflow.app/v2/webhooks/verify \ -H "X-API-Key: $QBITFLOW_API_KEY" \ -H "Content-Type: application/json" \ -d @-# 200: { "message": … }Body
endpointUuidstringRequiredThe endpoint that received it
bodystringRequiredThe raw body, exactly as received
max=1048576signaturestringRequiredThe QBitFlow-Signature header: t=<unix seconds>,v1=<hex>
max=512
Response
{ "message": string }
Errors
- 403policy_
disabled a member doing what their organization's policy doesn't allow them, reads of what the policy governs included
- 400invalid_
signature POST /: the signature doesn't match, or is older than 5 minutes; a send route: the transaction isn't the payer's signature over what the intent prepared (Payments and transactions § Sending a signed transaction)webhooks/ verify
Also the common codes, as any route (what each means):
- 400validation_failed
- 401unauthorized
- 403forbidden
- 404not_found
- 429rate_limit_exceeded
- 500internal
- 504timeout
Edge cases
The contract's rules for these routes: Access · Endpoints · Endpoints migrated from v1 · Test event · Event log · Verify.
Read all 6
Access
/webhooks is the integrators' namespace only: the hook QBitFlow's mail provider posts inbound emails to moved out of it (to an internal path, not part of this contract).
- Endpoints are the request's space's (spaces): the team's in the organization's space, a member's in their own; each mode has its own. An endpoint of another space, mode or organization answers
404. - A member manages their endpoints only with the organization's
members.webhookspolicy (off by default): otherwise403policy_disabled(details.policy: "members.webhooks"). The team always can, acting in the member's space (switch, or an organization key withOn-Behalf-Of). - The policy applies at delivery too. While
members.webhooksis off for a member, the endpoints of their space (whoever created them) receive nothing: each event is skipped, logged in the delivery log as an attempt withskipped: trueanddelivered: false(itserrorsays why), never retried, and not counted against the endpoint's health. Once the policy is on again, the events published from then on are delivered; skipped ones aren't resent (resend one from the event log if needed). The organization's endpoints withincludeMemberskeep receiving the member's events.
Endpoints
GET /webhooks/event-types→200the event types, as a bounded list: the{items, hasMore, nextCursor}envelope in one page (hasMore: false, nonextCursor).GET /webhooks/endpoints→200a page of the space's endpoints (pagination): newest first (by creation),limit20 by default (at most 100, clamped),cursor= the previous page'snextCursor(opaque; one the API didn't give →400validation_failedoncursor). A space has at most 10 endpoints, so one page usually holds them all; readhasMoreall the same.POST /webhooks/endpoints→201CreatedEndpoint: alwayspayloadVersion: "v2", with itssecret(whsec_…) in this response only (an API integration stores it now; the dashboard can show it again withGET …/:uuid/secret). The list and the other routes never return it.urlis required, a valid URL (400validation_failedonurl), https in live mode on port 443 (none given), 8443 or 80 (any other port →400onurl; test mode takes any port), and a public address:localhost,*.internal, and private, loopback, link-local, carrier-grade NAT, reserved and other non-public IPs are refused (url); the delivery also refuses them at connection time, whatever the DNS answers. These rules apply to a URL being set (POST, or aPATCHwithurl): an endpoint's other fields can be changed whatever its URL (an endpoint migrated from v1 may keep v1'shttp://URL in live mode until its URL changes).events: event types fromGET /webhooks/event-types, stored sorted and without duplicates; empty or absent = every type, including types added later. An unknown type, orwebhook.test→400onevents[i].includeMembers: the endpoint also receives the events of every member's space of the same mode (the envelope'suserUuidnames the member): every event of a member's space, each seller'spayment.completed,subscription.created/billed/statusChanged/actionRequiredChanged/billingFailed/upcomingBill,refund.*,heldFunds.released,checkout.expired.- Absent, it is
truefor an organization endpoint (the organization's shared space): a marketplace hears of each seller's sale without asking. SendincludeMembers: falseto receive the organization's own events only (which includemember.joined/member.removed). Endpoints created before this default keep their value;PATCHchanges it. - Absent, it is
falsefor a member's endpoint;truethere →400onincludeMembers(a member's space has no members).
- Absent, it is
- At most 10 endpoints per space and mode: the 11th →
409conflict. Idempotency-Key(idempotency): a create retried with the same key answers the firstCreatedEndpointagain (the sameuuidandsecret, for 24 hours) and creates no second endpoint.PATCH /webhooks/endpoints/:uuidchanges the fields sent, the others stay (events: []= every type,description: ""clears it:descriptionis then absent from the endpoint).PUTis not a route (405method_not_allowed).payloadVersiontakesv2only, the only payload version (it changes nothing today): any other value,v1included →400validation_failedonpayloadVersion.enabled: falsedisables the endpoint (disabledAtis set): it receives nothing, each delivery in progress stops at its next attempt (unless the endpoint is enabled again before it), and the events published while it is disabled are never sent to it.enabled: trueenables it again, also after QBitFlow disabled it for failing for 3 days (live mode only, Webhook events § Failing endpoints):disabledAtis removed and the failures before don't count toward its health. Sending the state it already has changes nothing.- How an endpoint stands (every endpoint read, and an update's answer):
disabledReason, while disabled:failing(QBitFlow: every delivery failed for 3 days),owner(enabled: false),closed(its member removed, its organization closed). Absent for an endpoint disabled before it was recorded, and once enabled again;failingSince, while enabled and failing: its first failed attempt since its last successful one (or since it was enabled again). After a day its team is emailed and the dashboard raises it; after 3 it is disabled (Webhook events § Failing endpoints; a test-mode endpoint is never emailed about nor disabled for failing). Absent while it delivers;lastDeliveredAt: when it last received an event. Absent if it never did.
DELETE /webhooks/endpoints/:uuid→204(no body): the endpoint receives nothing more, pending retries included. Its delivery history is deleted with it: the event log no longer lists its deliveries (GET /webhooks/events/:id), andGET …/:uuid/deliveriesanswers404. Disable it instead (PATCH{ "enabled": false }) to keep that history. Deleted, it answers404(a secondDELETEtoo).POST …/:uuid/rotate→200EndpointSecret, from the dashboard only (likeGET …/secret; an API key →404and nothing changes: a leaked key must not be able to read a new secret and sign deliveries the integration would accept). The new secret signs every delivery from now on (retries included), and for 24 hours the previous secret signs them too (a secondv1=inQBitFlow-Signature, after the new one), so the receiver can switch secrets without losing deliveries.rotatedAtrecords when.
Endpoints migrated from v1
- v1's webhook URLs became endpoints of their space and mode (v1's transaction URL receives
payment.completedandsubscription.created, v1's recurring URLsubscription.billedandsubscription.statusChanged). The v2 launch (v1 retired, 2026-10-24) moved them all to the envelope (payloadVersion: "v2") with the secret they signed with: v1's secret of their space (one shared by both modes' endpoints; an owner v1 gave none got a new one). From then on they are endpoints like any other: their own secret, rotated with the 24-hour overlap, any event type.
Test event
POST …/:uuid/test→200TestResult: posts one event, signed like any delivery, once (no retry), and answers how the endpoint answered:delivered(a2xx),statusCode, anderrorwhen it wasn't delivered (the endpoint's status and the start of its answer, or why it got no answer: a fixed reason such as "the endpoint refused the connection", never the address QBitFlow dialed). A failed test is still200. A disabled endpoint can be tested (to check it before enabling it again); a test never counts toward its health. A member's endpoint whose events are skipped (members.webhooksoff for that member) →403policy_disabled: a test must not succeed where the real events don't go out.- At most 30 tests an hour per space (
LimitPerSpace(webhookTests)), every endpoint and credential together →429rate_limit_exceeded, nothing sent (each test may hold the request up to 30 seconds). - The endpoint receives a
webhook.testevent (data: {endpointUuid, message}).
Event log
The space's events as they were delivered, and how each delivery went (Stripe's event list and delivery log). Events are kept at least 90 days.
GET /webhooks/events→200a page of the request's space's events, newest first (the events of one publication latest first), each its envelope exactly as delivered (Webhook events § The envelope). Cursor pagination (pagination):limit(default 20, at most 100),cursor= the previous page'snextCursor, opaque. The log pages on each event's creation time, its position in its publication and its id, so the events of one publication (created together) are each listed once across pages, in order. A cursor the API didn't give →400validation_failedoncursor(never an empty page).:idis an event's id (evt_…, at most 64 characters:400onidotherwise).typekeeps one event type (GET /webhooks/event-types); an unknown type →400ontype.includeMembers=true, for the organization's space only, adds its members' events of the same mode (userUuidnames the member); from a member's space →400onincludeMembers.- A member sees their own space's events:
member.joined/member.removedare the organization's. Nothing of another organization or mode, ever.
GET /webhooks/events/:id→200EventDetail: the envelope anddeliveries, one per endpoint of the request's space the event went to or is queued for (an endpoint of another space isn't listed: the organization doesn't see a member's endpoints there, nor a member the organization's), withdelivered(one attempt answered2xx) and everyattempts[]entry oldest first (attempt,delivered,statusCodewhen the endpoint answered,error,durationMs,attemptedAt). The organization's space reads its members' events too; any other event →404.- Where a member's deliveries show: a member's event goes to their own endpoints and to the organization's endpoints with
includeMembers. Read its deliveries to the organization's endpoints from the organization's space (list it withincludeMembers=true, thenGET /webhooks/events/:id); read as the member (On-Behalf-Of), the same event lists only the member's own endpoints:deliveries: []when they have none, although the organization's endpoint got it. - Whether a delivery goes on:
pending(always present).truewhile QBitFlow still attempts the event on its own; then exactly one of these describes it:
| The delivery | attempts | delivered | pending | nextAttemptAt | waitingFor | |---|---|---|---|---|---| | Delivered | ≥ 1 | true | false | absent | absent | | Retried after a failed attempt | ≥ 1 | false | true | the next attempt | absent | | Queued behind an earlier event of its publication (§ Delivery and retries in Webhook events: an endpoint gets a publication's events in order) | [] | false | true | absent | that event's evt_… | | Not started yet (QBitFlow starts it within a minute) | [] | false | true | absent | absent | | Abandoned after its retries (3 days live, 3 hours in test mode) | ≥ 1 | false | false | absent | absent | | The endpoint was disabled meanwhile | as they were | false | false | absent | absent |
A deleted endpoint's deliveries are deleted with it: the event no longer lists them (its successful ones included), as if it never had that endpoint. To keep an endpoint's history, disable it (PATCH …/:uuid { "enabled": false }) rather than delete it.
So an entry can have attempts: []: a queued event is listed as soon as it is queued, never shown as unsent. An event a queued one waits for is in the same publication (the same createdAt): subscription.billed waits for its subscription.created, never for an earlier publication. If QBitFlow can't read a delivery's progress at that moment, the event still answers 200, and pending comes from the attempts: true while the retry schedule still attempts the event again (so a delivery being retried never reads as abandoned), without nextAttemptAt; a delivery queued with no attempt yet isn't listed until its progress can be read: read it again a moment later.
GET /webhooks/endpoints/:uuid/deliveries→200a page of the attempts to one endpoint of the request's space, newest first by when they were attempted (default 20, at most 100;cursor= the previous page's opaquenextCursor), each with itseventIdandeventType.delivered=falsekeeps the failed attempts (what to look at when the endpoint is reported failing),delivered=truethe delivered ones. Another space's endpoint →404.POST /webhooks/events/:id/resend{endpointUuid}→200TestResult: posts the event to that endpoint once, now, signed like any delivery (the same event id, a newt), and answers how the endpoint answered (a failed attempt is still200). It is sent even if the event was already delivered. It is logged as the nextattempt(resends and automatic retries share one sequence per event and endpoint) and counts for the endpoint's health (a success ends a failing streak). A delivery still in progress goes on while the event fails; once a resend delivered it, the delivery's next retry sends nothing and the retries stop (nothing more is logged). The endpoint must be one of the request's space's that receives the event (its types, its payload version,includeMembersfor a member's event): otherwise409conflict; a disabled endpoint →409conflict(enable it first). An event or endpoint the request's space doesn't see →404;endpointUuidmissing →400.- A resend to a member's endpoint while the organization's
members.webhookspolicy is off for them (the team resending in the member's space; the member themselves gets403policy_disabled) posts nothing:200TestResultwithdelivered: false, nostatusCode, anderrorsaying the policy is off. It is logged as a skipped attempt (skipped: true), and doesn't count for the endpoint's health. - At most 100 resends an hour per space (
LimitPerSpace(webhookResends)) →429rate_limit_exceeded, nothing sent. A resend refused for another reason (404,409) still counts.
Verify
POST /webhooks/verifychecks a delivery an endpoint of the request's space received:endpointUuid, the rawbodyexactly as received, and theQBitFlow-Signatureheader assignature. All three required (400validation_failed).- Valid →
200. A body, signature or secret that doesn't match, a malformed header, or a timestamp more than 5 minutes from the server clock (either way: replays) →400invalid_signature. Another space's endpoint →404. - The SDKs verify locally with the same rule (no call needed): see Webhook events.
The codes named here
- 400validation_
failed path/query/body failed binding or validation, or a business rule about one input (a field named in
errors) - 403policy_
disabled a member doing what their organization's policy doesn't allow them, reads of what the policy governs included
- 400invalid_
signature POST /: the signature doesn't match, or is older than 5 minutes; a send route: the transaction isn't the payer's signature over what the intent prepared (Payments and transactions § Sending a signed transaction)webhooks/ verify - 409conflict
a state conflict no more specific code covers
- 429rate_
limit_ exceeded rate limit hit (see below)
- 405method_
not_ allowed the path exists, not with this method; the
Allowheader lists its methods. Answered before any credential is read, like the404of a path no route has