API reference

Webhooks

  • 12 endpoints

Get event types#

GET /webhooks/event-types

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

request.sh

curl https://api.qbitflow.app/v2/webhooks/event-types \  -H "X-API-Key: $QBITFLOW_API_KEY"# 200: an array of TypeInfo

Response

A page: { "items": TypeInfo[], "hasMore": …, "nextCursor"?: … }. Pass nextCursor back as cursor for the next page; it is absent when hasMore is false.

  • typestring

    One ofpayment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expired

    • webhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
  • descriptionstring

Errors

List endpoints#

GET /webhooks/endpoints

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

request.sh

curl https://api.qbitflow.app/v2/webhooks/endpoints \  -H "X-API-Key: $QBITFLOW_API_KEY"# 200: an array of Endpoint

Query parameters

  • limitintegerOptional

    Page size: 20 when absent or ≤ 0, at most 100 (a larger one is clamped, not refused)

  • cursorstringOptional

    The 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
  • uuidstring

    Its only id (a UUIDv7)

  • testboolean

    Whether this is test data (testnets) or live data

  • urlstring
  • eventsarray of string

    The event types it receives (GET /webhooks/event-types); empty: every type

    One ofpayment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expired

    • webhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
  • includeMembersboolean

    An endpoint of the organization's shared space that also receives its members' events (userUuid names the member)

  • payloadVersionstring

    The 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.
  • descriptionstring

    A note for the dashboard; absent when none

  • createdAtstring
  • rotatedAtstringOptional

    When its secret last changed

  • disabledAtstringOptional

    Disabled: 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

  • disabledReasonstringOptional

    DisabledReason 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
  • failingSincestringOptional

    FailingSince 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

  • lastDeliveredAtstringOptional

    LastDeliveredAt 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

Create endpoint#

POST /webhooks/endpoints

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

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 CreatedEndpoint
const 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`);
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

  • urlstringRequired

    Where the events are posted: https in live mode, a public address

    http_urlmax=2048

  • eventsarray of stringOptional

    The event types to receive (GET /webhooks/event-types); empty or absent: every type

    max=20

    One ofpayment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expired

    • webhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
  • includeMembersbooleanOptional

    Organization 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

  • descriptionstringOptional

    A note for the dashboard

    textmax=200

Response

A CreatedEndpoint.

14 fields of CreatedEndpoint
  • uuidstring

    Its only id (a UUIDv7)

  • testboolean

    Whether this is test data (testnets) or live data

  • urlstring
  • eventsarray of string

    The event types it receives (GET /webhooks/event-types); empty: every type

    One ofpayment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expired

    • webhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
  • includeMembersboolean

    An endpoint of the organization's shared space that also receives its members' events (userUuid names the member)

  • payloadVersionstring

    The 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.
  • descriptionstring

    A note for the dashboard; absent when none

  • createdAtstring
  • rotatedAtstringOptional

    When its secret last changed

  • disabledAtstringOptional

    Disabled: 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

  • disabledReasonstringOptional

    DisabledReason 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
  • failingSincestringOptional

    FailingSince 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

  • lastDeliveredAtstringOptional

    LastDeliveredAt is when it last received an event (a successful delivery). Omitted if it never did

  • secretstring

    whsec_…: verifies the signature of its deliveries (GET …/secret shows it again in the dashboard)

Errors

Get endpoint#

GET /webhooks/endpoints/:uuid

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

request.sh

curl "https://api.qbitflow.app/v2/webhooks/endpoints/$ENDPOINT_UUID" \  -H "X-API-Key: $QBITFLOW_API_KEY"# 200: an Endpoint

Path parameters

  • uuidstringRequired

Response

A Endpoint.

13 fields of Endpoint
  • uuidstring

    Its only id (a UUIDv7)

  • testboolean

    Whether this is test data (testnets) or live data

  • urlstring
  • eventsarray of string

    The event types it receives (GET /webhooks/event-types); empty: every type

    One ofpayment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expired

    • webhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
  • includeMembersboolean

    An endpoint of the organization's shared space that also receives its members' events (userUuid names the member)

  • payloadVersionstring

    The 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.
  • descriptionstring

    A note for the dashboard; absent when none

  • createdAtstring
  • rotatedAtstringOptional

    When its secret last changed

  • disabledAtstringOptional

    Disabled: 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

  • disabledReasonstringOptional

    DisabledReason 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
  • failingSincestringOptional

    FailingSince 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

  • lastDeliveredAtstringOptional

    LastDeliveredAt 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

Update endpoint#

PATCH /webhooks/endpoints/:uuid

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

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 Endpoint

Path parameters

  • uuidstringRequired

Body

  • urlstringOptional

    http_urlmax=2048

  • eventsarray of stringOptional

    [] for every type

    max=20

    One ofpayment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expired

    • webhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
  • includeMembersbooleanOptional
  • descriptionstringOptional

    "" to clear it

    max=200len=0|text

  • payloadVersionstringOptional

    The payload it receives: v2, the only version today

    oneof=v2

    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.
  • enabledbooleanOptional

    false 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
  • uuidstring

    Its only id (a UUIDv7)

  • testboolean

    Whether this is test data (testnets) or live data

  • urlstring
  • eventsarray of string

    The event types it receives (GET /webhooks/event-types); empty: every type

    One ofpayment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expired

    • webhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
  • includeMembersboolean

    An endpoint of the organization's shared space that also receives its members' events (userUuid names the member)

  • payloadVersionstring

    The 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.
  • descriptionstring

    A note for the dashboard; absent when none

  • createdAtstring
  • rotatedAtstringOptional

    When its secret last changed

  • disabledAtstringOptional

    Disabled: 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

  • disabledReasonstringOptional

    DisabledReason 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
  • failingSincestringOptional

    FailingSince 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

  • lastDeliveredAtstringOptional

    LastDeliveredAt 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

Delete endpoint#

DELETE /webhooks/endpoints/:uuid

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

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

Test endpoint#

POST /webhooks/endpoints/:uuid/test

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

request.sh

curl -X POST "https://api.qbitflow.app/v2/webhooks/endpoints/$ENDPOINT_UUID/test" \  -H "X-API-Key: $QBITFLOW_API_KEY"# 200: a TestResult

Path parameters

  • uuidstringRequired

Response

A TestResult.

  • deliveredboolean

    The endpoint answered 2xx

  • statusCodeintegerOptional

    Its answer's status, if it answered

  • errorstringOptional

    Why 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):

List endpoint deliveries#

GET /webhooks/endpoints/:uuid/deliveries

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

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

  • uuidstringRequired

    The endpoint

Query parameters

  • limitintegerOptional

    Page size: 20 when absent or ≤ 0, at most 100 (a larger one is clamped, not refused)

  • cursorstringOptional

    The previous page's nextCursor, verbatim; omit for the first page

    max=200

  • deliveredbooleanOptional

    true: 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
  • uuidstring
  • eventIdstring
  • eventTypestring

    One ofpayment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expired

    • webhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
  • endpointUuidstring
  • attemptinteger

    1 for the first, then one more per retry or resend

  • deliveredboolean

    The endpoint answered with a 2xx

  • skippedbooleanOptional

    Not 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)

  • statusCodeintegerOptional

    The endpoint's answer; absent when it didn't answer (timeout, connection refused)

  • errorstringOptional

    Why it wasn't delivered: the endpoint's status and the start of its answer, or the connection error

  • durationMsinteger
  • attemptedAtstring

Errors

  • 403policy_disabled

    a member doing what their organization's policy doesn't allow them, reads of what the policy governs included

List events#

GET /webhooks/events

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

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}
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 id
page, 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

  • limitintegerOptional

    Page size: 20 when absent or ≤ 0, at most 100 (a larger one is clamped, not refused)

  • cursorstringOptional

    The previous page's nextCursor, verbatim; omit for the first page

    max=200

  • typestringOptional

    Only the events of this type (GET /webhooks/event-types)

    max=64

    One ofpayment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expired

    • webhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
  • includeMembersbooleanOptional

    The 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.

  • idstring

    evt_…: the same for every endpoint and every retry of the event, and if it is ever published again (deduplicate on it)

  • typestring

    One ofpayment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expired

    • webhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
  • versionstring

    v2

    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.
  • createdAtstring
  • testboolean

    Whether it happened in test mode

  • userUuidstringOptional

    The member whose space it happened in; omitted for the organization's own

  • dataany JSON value

    Depends 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

Get event#

GET /webhooks/events/:id

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

request.sh

curl "https://api.qbitflow.app/v2/webhooks/events/$EVENT_ID" \  -H "X-API-Key: $QBITFLOW_API_KEY"# 200: an EventDetail

Path parameters

  • idstringRequired

    The event's id (evt_…, its envelope's id)

    max=64

Response

A EventDetail.

  • idstring

    evt_…: the same for every endpoint and every retry of the event, and if it is ever published again (deduplicate on it)

  • typestring

    One ofpayment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expired

    • webhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
  • versionstring

    v2

    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.
  • createdAtstring
  • testboolean

    Whether it happened in test mode

  • userUuidstringOptional

    The member whose space it happened in; omitted for the organization's own

  • dataany JSON value

    Depends on the type (GET /webhooks/event-types)

  • deliveriesarray of EndpointDelivery

    Its 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 EndpointDelivery
    • endpointUuidstring
    • urlstring

      The endpoint's URL now

    • deliveredboolean

      One of its attempts was answered with a 2xx

    • attemptsarray of DeliveryAttempt

      Oldest first; empty while the event waits for its first attempt (pending)

      11 fields of each DeliveryAttempt
      • uuidstring
      • eventIdstring
      • eventTypestring

        One ofpayment.completedsubscription.createdsubscription.billedsubscription.statusChangedsubscription.actionRequiredChangedsubscription.billingFailedsubscription.upcomingBillrefund.requestedrefund.approvedrefund.rejectedmember.joinedmember.removedheldFunds.releasedcheckout.expired

        • webhook.testWebhookTest is the event POST /webhooks/endpoints/:uuid/test sends; no endpoint subscribes to it.
      • endpointUuidstring
      • attemptinteger

        1 for the first, then one more per retry or resend

      • deliveredboolean

        The endpoint answered with a 2xx

      • skippedbooleanOptional

        Not 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)

      • statusCodeintegerOptional

        The endpoint's answer; absent when it didn't answer (timeout, connection refused)

      • errorstringOptional

        Why it wasn't delivered: the endpoint's status and the start of its answer, or the connection error

      • durationMsinteger
      • attemptedAtstring
    • pendingboolean

      Pending: 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)

    • nextAttemptAtstringOptional

      While pending after a failed attempt: when the next one comes

    • waitingForstringOptional

      While 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

Resend event#

POST /webhooks/events/:id/resend

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

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 TestResult

Path parameters

  • idstringRequired

    The event's id (evt_…, its envelope's id)

    max=64

Body

  • endpointUuidstringRequired

    An endpoint of the request's space that receives the event

Response

A TestResult.

  • deliveredboolean

    The endpoint answered 2xx

  • statusCodeintegerOptional

    Its answer's status, if it answered

  • errorstringOptional

    Why 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):

Verify webhook#

POST /webhooks/verify

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

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

  • endpointUuidstringRequired

    The endpoint that received it

  • bodystringRequired

    The raw body, exactly as received

    max=1048576

  • signaturestringRequired

    The 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 /webhooks/verify: 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)

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.webhooks policy (off by default): otherwise 403 policy_disabled (details.policy: "members.webhooks"). The team always can, acting in the member's space (switch, or an organization key with On-Behalf-Of).
  • The policy applies at delivery too. While members.webhooks is 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 with skipped: true and delivered: false (its error says 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 with includeMembers keep receiving the member's events.

Endpoints

  • GET /webhooks/event-types → 200 the event types, as a bounded list: the {items, hasMore, nextCursor} envelope in one page (hasMore: false, no nextCursor).
  • GET /webhooks/endpoints → 200 a page of the space's endpoints (pagination): newest first (by creation), limit 20 by default (at most 100, clamped), cursor = the previous page's nextCursor (opaque; one the API didn't give → 400 validation_failed on cursor). A space has at most 10 endpoints, so one page usually holds them all; read hasMore all the same.
  • POST /webhooks/endpoints → 201 CreatedEndpoint: always payloadVersion: "v2", with its secret (whsec_…) in this response only (an API integration stores it now; the dashboard can show it again with GET …/:uuid/secret). The list and the other routes never return it.
  • url is required, a valid URL (400 validation_failed on url), https in live mode on port 443 (none given), 8443 or 80 (any other port → 400 on url; 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 a PATCH with url): an endpoint's other fields can be changed whatever its URL (an endpoint migrated from v1 may keep v1's http:// URL in live mode until its URL changes).
  • events: event types from GET /webhooks/event-types, stored sorted and without duplicates; empty or absent = every type, including types added later. An unknown type, or webhook.test → 400 on events[i].
  • includeMembers: the endpoint also receives the events of every member's space of the same mode (the envelope's userUuid names the member): every event of a member's space, each seller's payment.completed, subscription.created / billed / statusChanged / actionRequiredChanged / billingFailed / upcomingBill, refund.*, heldFunds.released, checkout.expired.
    • Absent, it is true for an organization endpoint (the organization's shared space): a marketplace hears of each seller's sale without asking. Send includeMembers: false to receive the organization's own events only (which include member.joined / member.removed). Endpoints created before this default keep their value; PATCH changes it.
    • Absent, it is false for a member's endpoint; true there → 400 on includeMembers (a member's space has no members).
  • At most 10 endpoints per space and mode: the 11th → 409 conflict.
  • Idempotency-Key (idempotency): a create retried with the same key answers the first CreatedEndpoint again (the same uuid and secret, for 24 hours) and creates no second endpoint.
  • PATCH /webhooks/endpoints/:uuid changes the fields sent, the others stay (events: [] = every type, description: "" clears it: description is then absent from the endpoint). PUT is not a route (405 method_not_allowed). payloadVersion takes v2 only, the only payload version (it changes nothing today): any other value, v1 included → 400 validation_failed on payloadVersion.
  • enabled: false disables the endpoint (disabledAt is 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: true enables it again, also after QBitFlow disabled it for failing for 3 days (live mode only, Webhook events § Failing endpoints): disabledAt is 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), and GET …/:uuid/deliveries answers 404. Disable it instead (PATCH { "enabled": false }) to keep that history. Deleted, it answers 404 (a second DELETE too).
  • POST …/:uuid/rotate → 200 EndpointSecret, from the dashboard only (like GET …/secret; an API key → 404 and 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 second v1= in QBitFlow-Signature, after the new one), so the receiver can switch secrets without losing deliveries. rotatedAt records when.

Endpoints migrated from v1

  • v1's webhook URLs became endpoints of their space and mode (v1's transaction URL receives payment.completed and subscription.created, v1's recurring URL subscription.billed and subscription.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 → 200 TestResult: posts one event, signed like any delivery, once (no retry), and answers how the endpoint answered: delivered (a 2xx), statusCode, and error when 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 still 200. 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.webhooks off for that member) → 403 policy_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 → 429 rate_limit_exceeded, nothing sent (each test may hold the request up to 30 seconds).
  • The endpoint receives a webhook.test event (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 → 200 a 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's nextCursor, 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 → 400 validation_failed on cursor (never an empty page).
    • :id is an event's id (evt_…, at most 64 characters: 400 on id otherwise).
    • type keeps one event type (GET /webhooks/event-types); an unknown type → 400 on type.
    • includeMembers=true, for the organization's space only, adds its members' events of the same mode (userUuid names the member); from a member's space → 400 on includeMembers.
    • A member sees their own space's events: member.joined / member.removed are the organization's. Nothing of another organization or mode, ever.
  • GET /webhooks/events/:id → 200 EventDetail: the envelope and deliveries, 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), with delivered (one attempt answered 2xx) and every attempts[] entry oldest first (attempt, delivered, statusCode when 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 with includeMembers=true, then GET /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). true while 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 → 200 a 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 opaque nextCursor), each with its eventId and eventType. delivered=false keeps the failed attempts (what to look at when the endpoint is reported failing), delivered=true the delivered ones. Another space's endpoint → 404.
  • POST /webhooks/events/:id/resend {endpointUuid} → 200 TestResult: posts the event to that endpoint once, now, signed like any delivery (the same event id, a new t), and answers how the endpoint answered (a failed attempt is still 200). It is sent even if the event was already delivered. It is logged as the next attempt (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, includeMembers for a member's event): otherwise 409 conflict; a disabled endpoint → 409 conflict (enable it first). An event or endpoint the request's space doesn't see → 404; endpointUuid missing → 400.
  • A resend to a member's endpoint while the organization's members.webhooks policy is off for them (the team resending in the member's space; the member themselves gets 403 policy_disabled) posts nothing: 200 TestResult with delivered: false, no statusCode, and error saying 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)) → 429 rate_limit_exceeded, nothing sent. A resend refused for another reason (404, 409) still counts.

Verify

  • POST /webhooks/verify checks a delivery an endpoint of the request's space received: endpointUuid, the raw body exactly as received, and the QBitFlow-Signature header as signature. All three required (400 validation_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) → 400 invalid_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 /webhooks/verify: 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)

  • 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 Allow header lists its methods. Answered before any credential is read, like the 404 of a path no route has