Webhooks
Quota POSTs JSON to your endpoint when balances change, users connect, or usage completes. Every delivery is signed with HMAC-SHA256 over the raw body so you can verify authenticity.
Event types#
| user.connectedevent | A user linked their Quota account to your app via OAuth. |
| user.disconnectedevent | A user unlinked their account. |
| balance.updatedevent | A user's credit balance changed after an API request. |
| balance.lowevent | A user's balance dropped below the configured low_balance_threshold. |
| usage.completedevent | An API request completed and credits were deducted. |
Event payloads#
Every delivery sends a JSON body with id, type, created_at, and an event-specific data object.
balance.updated
{
"id": "evt_abc123",
"type": "balance.updated",
"created_at": "2026-01-15T12:00:00.000Z",
"data": {
"user_id": "usr_123",
"new_balance": 999950,
"amount_spent": 50,
"model": "gpt-4o",
"endpoint": "/v1/chat/completions"
}
}balance.low
{
"id": "evt_def456",
"type": "balance.low",
"created_at": "2026-01-15T12:05:00.000Z",
"data": {
"user_id": "usr_123",
"current_balance": 950000,
"threshold": 1000000
}
}usage.completed
{
"id": "evt_ghi789",
"type": "usage.completed",
"created_at": "2026-01-15T12:00:01.000Z",
"data": {
"user_id": "usr_123",
"model": "gpt-4o",
"prompt_tokens": 150,
"completion_tokens": 80,
"total_tokens": 230,
"credits_used": 1175
}
}user.connected
{
"id": "evt_jkl012",
"type": "user.connected",
"created_at": "2026-01-15T10:00:00.000Z",
"data": {
"user_id": "usr_123",
"connected_at": "2026-01-15T10:00:00.000Z"
}
}user.disconnected
{
"id": "evt_mno345",
"type": "user.disconnected",
"created_at": "2026-01-15T14:00:00.000Z",
"data": {
"user_id": "usr_123",
"disconnected_at": "2026-01-15T14:00:00.000Z"
}
}Signature verification#
Every delivery includes an X-Quota-Signature header containing an HMAC-SHA256 hex digest of the raw request body, signed with your webhook secret.
import { verifyWebhookSignature } from "@usequota/nextjs";
const rawBody = await request.text();
const signature = request.headers.get("X-Quota-Signature");
if (!signature) {
return new Response("Missing signature", { status: 401 });
}
// Note the await — verifyWebhookSignature is async.
const isValid = await verifyWebhookSignature({
payload: rawBody,
signature,
secret: process.env.QUOTA_WEBHOOK_SECRET!,
});
if (!isValid) {
return new Response("Invalid signature", { status: 401 });
}The Core SDK exposes verifyWebhookSignature and parseWebhook; the Next.js SDK adds createWebhookHandler, which verifies signatures and routes events for you.
Delivery headers#
| X-Quota-Signatureheader | HMAC-SHA256 hex digest of the raw body, signed with the webhook secret. |
| X-Quota-Eventheader | The event type (e.g. balance.updated). |
| Content-Typeheader | Always application/json. |
| User-Agentheader | Always Quota-Webhooks/1.0. |
Create a webhook#
curl -X POST https://api.usequota.ai/v1/webhooks \
-H "Authorization: Bearer sess_your_session_token" \
-H "Content-Type: application/json" \
-d '{
"client_id": "your_client_id",
"url": "https://yourapp.com/api/webhooks/quota",
"events": ["balance.updated", "balance.low", "usage.completed"],
"low_balance_threshold": 1000000
}'{
"id": "wh_abc123",
"url": "https://yourapp.com/api/webhooks/quota",
"events": ["balance.updated", "balance.low", "usage.completed"],
"secret": "whsec_abc123...",
"low_balance_threshold": 1000000,
"active": true,
"created_at": "2026-01-15T12:00:00.000Z"
}Create body
| client_idstringrequired | OAuth client the webhook is scoped to. Must belong to the authenticated developer. |
| urlstringrequired | HTTPS endpoint Quota will POST to. http:// is allowed only for localhost during development. |
| eventsstring[]required | Non-empty array of event types to subscribe to. |
| low_balance_thresholdnumber | Credit threshold that fires balance.low. Optional; omit to disable the event. |
List, get, update, delete#
List
curl "https://api.usequota.ai/v1/webhooks?client_id=your_client_id" \
-H "Authorization: Bearer sess_your_session_token"Get one
GET /v1/webhooks/:id returns one webhook's configuration (without the secret).
Update
PATCH /v1/webhooks/:id updates URL, events, threshold, or active status. Pass any subset.
curl -X PATCH https://api.usequota.ai/v1/webhooks/wh_abc123 \
-H "Authorization: Bearer sess_your_session_token" \
-H "Content-Type: application/json" \
-d '{"active": false}'Delete
DELETE /v1/webhooks/:id removes the subscription. No body. Returns { "success": true }.
Delivery history#
GET /v1/webhooks/:id/deliveries returns recent attempts (default 50, max 100 via ?limit=) including the response status and body returned by your endpoint.
{
"deliveries": [
{
"id": "del_xyz",
"event_type": "balance.updated",
"payload": { "...": "..." },
"response_status": 200,
"response_body": "OK",
"delivered_at": "2026-01-15T12:00:01.000Z",
"created_at": "2026-01-15T12:00:00.000Z"
}
]
}Retries & URL rules#
Failed deliveries (non-2xx responses or network errors) are retried up to 3 times within 24 hours of creation.
- Production webhooks must use
https://. http://is allowed forlocalhostduring development.- Private and link-local IPs are blocked at the SSRF layer (
10.0.0.0/8,192.168.0.0/16, etc.).
Error responses#
client_id, url, or events; events empty or contains an unknown type.http://localhost for dev).sess_… session token. API keys are not accepted on management endpoints.