credit.granted
Fired when credits are granted to a customer (any source).
When it fires
QuotaStack sends this every time credits land in a customer balance. A topup, a plan grant, a renewal, and a grant you make by hand all raise it.
When it does not fire
QuotaStack does not send this for an import, even when you set options.emit_webhooks: true. Import turns the event off in the code, so a cutover cannot flood your endpoint. A grant that fails sends nothing either.
| Field | Type | Meaning |
|---|---|---|
idrequired | string (uuid) | |
tenant_idrequired | string (uuid) | |
customer_idrequired | string (uuid) | |
environmentrequired | string |
Which environment this resource lives in. Determined by the API key prefix used ( |
deltarequired | integer (int64) | Millicredits. Positive for credits, negative for debits. |
typerequired | string |
|
sourceoptional | string |
|
credit_block_idoptional | string (uuid) | |
billable_metric_keyoptional | string | |
cost_unitsoptional | integer (int64) | How many metric units this entry covers. Unit: a count of billable metric units, not credits. |
exchange_rateoptional | integer (int64) | |
idempotency_keyrequired | string | |
reference_idoptional | string (uuid) | |
metadatarequired | object | |
created_atrequired | string (date-time) |
{
"event_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a51",
"event_type": "credit.granted",
"tenant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a01",
"environment": "live",
"customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
"external_customer_id": "user_42",
"created_at": "2026-07-28T12:20:00Z",
"idempotency_key": "topup:pay_xyz789",
"data": {
"transaction_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a32",
"credits": 50000,
"source": "topup",
"reason": "Payment pay_xyz789",
"balance_after": 75000
}
} What to do
Show the customer their new balance. Read balance_after instead of adding credits to a figure you hold. The same event can reach you twice, and balance_after stays right.
Which calls fire this
POST /v1/customers/{customer_id}/credits/adjustPOST /v1/customer-by-external-id/{external_id}/credits/adjustPOST /v1/subscriptionsPOST /v1/customers/{customer_id}/credits/grantPOST /v1/customer-by-external-id/{external_id}/credits/grantPOST /v1/topups/grantPOST /v1/subscriptions/{id}/renewPOST /v1/subscriptions/{id}/resumePOST /v1/subscriptions/{id}/upgrade
Delivery
QuotaStack guarantees at-least-once delivery. An event may be delivered more than once if your endpoint returns a non-2xx response, the connection fails, or the request exceeds the delivery timeout.
Delivery timeout: 5 seconds per attempt. If your endpoint does not return a 2xx within 5 seconds, the attempt is treated as a failure and retried. Not configurable today.
One webhook URL per tenant. Multiple URLs and per-event routing are not supported. Configure the URL via the tenant config endpoint.
Retry schedule
If delivery fails (non-2xx response, timeout, or network error), QuotaStack retries with exponential backoff:
| Attempt | Delay after previous |
|---|---|
| 1 | Immediate |
| 2 | 30 seconds |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 8 hours |
| 7 | 24 hours |
After 7 failed attempts, the event is moved to a dead letter queue. Dead-lettered events are not lost — you can requeue them yourself, from the dashboard (Activity → Webhooks → Redeliver) or the API:
curl -X POST https://api.quotastack.io/v1/webhooks/events/{event_id}/redeliver \
-H "X-API-Key: $QS_KEY" \
-H "Idempotency-Key: redeliver:{event_id}"
Redelivery resets the event to pending with a fresh retry schedule (7
new attempts). The next attempt signs with your current secret —
useful when the event dead-lettered because of a secret rotation or an endpoint
outage you have since fixed. Only dead_letter events can be
redelivered; the call returns 409 for events in any other status.
Handling duplicates
Because delivery is at-least-once, your webhook handler should be idempotent. Use
the webhook-id header for deduplication — if you have already
processed an event with that ID, return 200 and skip processing.
Loading…