Webhooks
Subscribe to platform events on your own HTTPS endpoints. Every payload is signed; replay protection is built in.
Anatomy of a delivery
POST /your/endpoint HTTP/1.1
Content-Type: application/json
Bq-Signature: t=1715792949,v1=4f9c1e6...
Bq-Event-Id: whv_01HZ...
Bq-Delivery-Attempt: 1
{
"id": "whv_01HZ...",
"type": "gift.settled",
"created_at": "2026-05-15T13:45:11Z",
"data": {
"id": "gft_01HZ...",
"status": "settled",
"amount_cents": 2500,
...
}
}
Verifying the signature
The Bq-Signature header carries a timestamp t and one or more v1 hashes. To verify:
- Reject if
tis more than 5 minutes from now (replay window). - Compute
HMAC-SHA256(secret, t + "." + raw_body). - Compare in constant time against any
v1=value in the header.
Our SDKs handle this for you. The raw helper:
// Node
const event = bq.webhooks.constructEvent(
rawBody,
req.headers['bq-signature'],
process.env.BQ_WEBHOOK_SECRET
);
POST
/v1/webhooks
Subscribe an endpoint to a set of events. The signing secret is returned ONCE — store it.
| Field | Type | Description | |
|---|---|---|---|
| url | string | required | HTTPS URL on your side. |
| events | array | required | List of event names to subscribe to. Use ["*"] for all. |
| description | string | optional | Human-readable label. |
Example request
curl -X POST https://api.bequest.org/v1/webhooks \
-H "Authorization: Bearer $BQ_KEY" \
-d '{
"url": "https://app.example.com/webhooks/bequest",
"events": ["gift.settled", "campaign.contribution"]
}'
const endpoint = await bq.webhooks.create({
url: 'https://app.example.com/webhooks/bequest',
events: ['gift.settled', 'campaign.contribution'],
});
// store endpoint.signing_secret — shown only once
endpoint = bq.webhooks.create(
url='https://app.example.com/webhooks/bequest',
events=['gift.settled', 'campaign.contribution'],
)
# store endpoint.signing_secret — shown only once
<?php
$endpoint = $bq->webhooks->create([
'url' => 'https://app.example.com/webhooks/bequest',
'events' => ['gift.settled', 'campaign.contribution'],
]);
// store $endpoint->signing_secret — shown only once
endpoint = bq.webhooks.create(
url: 'https://app.example.com/webhooks/bequest',
events: ['gift.settled', 'campaign.contribution']
)
# store endpoint.signing_secret — shown only once
endpoint, err := bq.Webhooks.Create(ctx, &bequest.WebhookCreateParams{
URL: "https://app.example.com/webhooks/bequest",
Events: []string{"gift.settled", "campaign.contribution"},
})
// store endpoint.SigningSecret — shown only once
Example response
{
"id": "whe_01HZ...",
"url": "https://app.example.com/webhooks/bequest",
"events": ["gift.settled", "campaign.contribution"],
"signing_secret": "whsec_AbCdEf...",
"status": "active"
}
Returns: The WebhookEndpoint with the one-time-visible signing_secret.
GET
/v1/webhooks
List your webhook endpoints.
PATCH
/v1/webhooks/{id}
Update events list or disable an endpoint.
| Field | Type | Description | |
|---|---|---|---|
| events | array | optional | New event subscription set. |
| status | string | optional | active | disabled |
POST
/v1/webhooks/{id}/rotate_secret
Rotate the signing secret. The new value is returned once; the old value is revoked after a 24-hour grace.
GET
/v1/webhooks/events
List recent webhook deliveries, useful for debugging.
| Field | Type | Description | |
|---|---|---|---|
| endpoint_id | string | optional | Filter to one endpoint. |
| status | string | optional | pending|delivered|failed|expired |
Retry policy
A non-2xx response triggers an exponential backoff retry: 1m, 5m, 30m, 2h, 12h, then expired. The same event id is sent every time; idempotency is up to you.
Event index
gift.created,gift.settled,gift.failed,gift.reversedcampaign.created,campaign.published,campaign.contribution,campaign.endedpool.created,pool.committed,pool.disbursement_requested,pool.disbursement_approved,pool.disbursement_executedidentity.kyc_passed,identity.kyc_failedreceipt.issued