Webhooks API
Complete reference for programmatic webhook subscription management. Use these endpoints to register HTTPS endpoints that receive JSON notifications when events occur in your tenant.
Inbound vs outbound
These endpoints configure outbound webhooks (Waqti → your server). They are not the URL that payment providers call into Waqti.
List Webhook Subscriptions
GET /v1/webhooksReturns all webhook subscriptions for the authenticated tenant. Secrets are never included in list responses.
Example Request
curl -X GET "https://api.waqti.sa/v1/webhooks" \
-H "Authorization: Bearer 3|a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0" \
-H "Accept: application/json"Example Response
{
"data": [
{
"id": 7,
"name": "ERP sync",
"url": "https://integrations.example.com/waqti/webhook",
"events": [
"purchase_order.approved",
"invoice.paid",
"vendor.updated"
],
"is_active": true,
"failure_count": 0,
"last_triggered_at": "2026-03-28T09:12:33Z",
"created_at": "2026-01-10T14:20:00Z"
}
]
}Create Webhook
POST /v1/webhooksCreates a new subscription. Provide the URL to receive POST requests and the list of event types to subscribe to.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name (max 100 characters) |
url | string | Yes | HTTPS endpoint that accepts application/json POST (max 500 characters) |
events | array | Yes | One or more event type strings (see Available events) |
The signing secret used for HMAC verification is generated by Waqti and returned only in this response. It cannot be set in the request body and is not shown again on later GET or PUT responses—store it securely when you create the subscription.
Example Request
curl -X POST "https://api.waqti.sa/v1/webhooks" \
-H "Authorization: Bearer 3|a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"name": "Procurement automation",
"url": "https://integrations.example.com/waqti/webhook",
"events": [
"purchase_order.created",
"purchase_order.approved",
"invoice.created",
"vendor.created"
]
}'Example Response
{
"data": {
"id": 8,
"name": "Procurement automation",
"url": "https://integrations.example.com/waqti/webhook",
"events": [
"purchase_order.created",
"purchase_order.approved",
"invoice.created",
"vendor.created"
],
"secret": "k8mN2pQ9rS4tU7vW1xY5zA6bC0dE3fG8hI2jK5lM9nO1pQ4rS7tU0vW3xY6z",
"is_active": true,
"created_at": "2026-04-02T11:05:22Z"
}
}Get Webhook Details
GET /v1/webhooks/{id}Returns one subscription including health fields. The signing secret is not included.
Example Request
curl -X GET "https://api.waqti.sa/v1/webhooks/8" \
-H "Authorization: Bearer 3|a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0" \
-H "Accept: application/json"Example Response
{
"data": {
"id": 8,
"name": "Procurement automation",
"url": "https://integrations.example.com/waqti/webhook",
"events": [
"purchase_order.created",
"purchase_order.approved",
"invoice.created",
"vendor.created"
],
"is_active": true,
"failure_count": 0,
"last_triggered_at": "2026-04-02T14:18:00Z",
"last_failed_at": null,
"last_failure_reason": null,
"created_at": "2026-04-02T11:05:22Z",
"updated_at": "2026-04-02T11:05:22Z"
}
}Update Webhook
PUT /v1/webhooks/{id}Updates an existing subscription. All body fields are optional; omitted fields keep their current values.
Request Body
| Field | Type | Description |
|---|---|---|
name | string | Display name (max 100 characters) |
url | string | Delivery URL (max 500 characters) |
events | array | Replacement list of event types (min 1 if provided) |
is_active | boolean | Enable or pause deliveries |
Example Request
curl -X PUT "https://api.waqti.sa/v1/webhooks/8" \
-H "Authorization: Bearer 3|a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"events": [
"purchase_order.approved",
"purchase_order.rejected",
"purchase_order.cancelled",
"invoice.approved",
"invoice.paid",
"budget.threshold_reached"
],
"is_active": true
}'Example Response
{
"data": {
"id": 8,
"name": "Procurement automation",
"url": "https://integrations.example.com/waqti/webhook",
"events": [
"purchase_order.approved",
"purchase_order.rejected",
"purchase_order.cancelled",
"invoice.approved",
"invoice.paid",
"budget.threshold_reached"
],
"is_active": true,
"updated_at": "2026-04-02T15:40:10Z"
}
}Delete Webhook
DELETE /v1/webhooks/{id}Permanently removes the subscription. No further deliveries are sent.
Example Request
curl -X DELETE "https://api.waqti.sa/v1/webhooks/8" \
-H "Authorization: Bearer 3|a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0" \
-H "Accept: application/json"Example Response
{
"data": null
}Send Test Event
POST /v1/webhooks/{id}/testQueues an immediate test delivery to the configured URL using the same headers and signing rules as real events. The payload uses the synthetic event type webhook.test.
Example Request
curl -X POST "https://api.waqti.sa/v1/webhooks/8/test" \
-H "Authorization: Bearer 3|a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0" \
-H "Accept: application/json"Example Response (success)
{
"data": {
"status_code": 200,
"response": "{\"received\":true}"
}
}If the endpoint does not return a success HTTP status, the API responds with 422 and an error message describing the failure.
Available events
Subscribe using the API event strings in POST / PUT requests. The shorthand column matches common naming (po.*, etc.); only the API value is accepted by the server.
| API event | Shorthand | Description |
|---|---|---|
purchase_order.created | po.created | A new purchase order was created |
purchase_order.approved | po.approved | A purchase order was approved |
purchase_order.rejected | po.rejected | A purchase order was rejected |
purchase_order.cancelled | po.cancelled | A purchase order was cancelled |
invoice.created | invoice.created | A new invoice was created |
invoice.approved | invoice.approved | An invoice was approved |
invoice.paid | invoice.paid | An invoice was marked as paid |
vendor.created | vendor.created | A new vendor was created |
vendor.updated | vendor.updated | Vendor details were updated |
budget.threshold_reached | budget.threshold_reached | A budget crossed its alert threshold |
Additional event types may be available in the product over time; your integration should ignore unknown event values it does not handle.
Webhook payload and signature verification
Each delivery is an HTTP POST with Content-Type: application/json. The JSON body has this shape:
Example delivery body
{
"id": "9f2d5c1e-6b4a-4e8d-9c3f-1a7e2d5b8c60",
"event": "purchase_order.approved",
"created_at": "2026-04-02T16:22:11+00:00",
"tenant_id": "acme-corp",
"data": {
"purchase_order_id": 442,
"po_number": "PO-2026-0442",
"status": "approved",
"total_amount": 128750.5,
"currency": "SAR",
"approved_at": "2026-04-02T16:22:00+00:00"
}
}Headers
| Header | Description |
|---|---|
X-Waqti-Event | Same string as the event field in the body |
X-Waqti-Signature | Hex-encoded HMAC-SHA256 of the raw request body using your subscription secret |
X-Waqti-Delivery | Internal delivery id (useful for support and idempotency logging) |
User-Agent | Waqti-Webhook/1.0 |
Verifying HMAC SHA-256
- Read the raw HTTP body as a string (before parsing JSON).
- Compute
HMAC_SHA256(raw_body, secret)and encode as lowercase hexadecimal. - Compare the result to
X-Waqti-Signatureusing a constant-time comparison (e.g.hash_equalsin PHP).
If the signature does not match, reject the request and do not trust the JSON.
WARNING
Whitespace, key order, and Unicode normalization affect the raw body. Verify against the bytes Waqti actually sent, not a re-serialized copy of parsed JSON, unless your framework guarantees identical output.
Retry policy
Failed deliveries (non-2xx HTTP status, timeouts, or network errors) are retried automatically. Expect up to about three retries after the initial attempt, with exponential backoff between each try. After the maximum attempts are exhausted, the delivery is marked failed.
If failures persist, Waqti may increment a failure counter on the subscription and automatically disable the webhook after many consecutive failures—fix the endpoint and re-enable the subscription via PUT or the admin UI.
Security best practices
- Use HTTPS only for webhook URLs so payloads and metadata are encrypted in transit.
- Verify every request with
X-Waqti-Signaturebefore acting on the payload; treat missing or invalid signatures as attacks or misconfiguration. - Store the secret from the create response in a secrets manager or environment variable, not in source control.
- Respond quickly with
2xxafter validating and enqueueing work; perform heavy processing asynchronously so Waqti does not time out and retry unnecessarily. - Design for duplicates: at-least-once delivery is possible; use
id(payload UUID) or your own idempotency keys to deduplicate.