quotastack Docs
Docs / API / Webhooks

Webhooks

QuotaStack sends you an event for each change. See the log, send a test, or retry one.

A webhook is an event QuotaStack sends when something changes: a grant, a low balance, a plan renewal.

Each event carries a signature. Your server can check it came from QuotaStack. Set one URL per tenant. QuotaStack retries a failed delivery.

Which call do I want?

3 of 3 operations documented in full.

List webhook events

GET/v1/webhooks/events
Idempotency-KeyNot used. This is a read.
EnvironmentSandbox and Live
AuthTenant API key
WritesNothing. A read has no side effects.
Fires No webhooks.

This endpoint lists every webhook event QuotaStack has queued for your tenant, in Sandbox and Live alike. QuotaStack keeps each one here forever; none of them ever get removed. A failed one stays on this list too, the same as one that worked. One URL and one secret serve both environments, so this list never splits them apart. Filter by status or event_type, if you want a smaller list. A bad cursor returns 400, though openapi.yaml does not say so.

Query parameters
FieldTypeMeaning
statusoptionalstring

Only events in this state come back. An unknown value gives you an empty list, not an error.

If you leave it out: All statuses come back.

  • pendingQueued. QuotaStack has not sent this yet, or a retry is due soon.
  • deliveredYour endpoint said yes. This one is done.
  • failedThe last try failed. QuotaStack tries again soon, up to a point.
  • dead_letterEvery try failed. QuotaStack gave up. Redeliver it once you fix the problem.
event_typeoptionalstring

Only events of this exact type, like credit.granted. QuotaStack matches the full string, with no partial match.

If you leave it out: Every event type comes back.

cursoroptionalstring

Opaque pagination cursor from the previous page's pagination.next_cursor. Omit to start at the first page.

limitoptionalinteger

Page size. Default 20, max 100.

Response 200
{
  "data": [
    {
      "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a51",
      "environment": "live",
      "event_type": "subscription.renewed",
      "payload": {
        "event_type": "subscription.renewed",
        "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
        "data": {
          "subscription_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a21"
        }
      },
      "status": "delivered",
      "attempts": 1,
      "next_retry_at": null,
      "last_error": null,
      "created_at": "2026-07-27T10:00:00Z",
      "delivered_at": "2026-07-27T10:00:01Z",
      "deliveries": [
        {
          "status_code": 200,
          "response_body": "OK",
          "error": null,
          "duration_ms": 118,
          "created_at": "2026-07-27T10:00:01Z"
        }
      ]
    },
    {
      "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a65",
      "environment": "live",
      "event_type": "credit.low_balance",
      "payload": {
        "event_type": "credit.low_balance",
        "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
        "data": {
          "balance_mc": 2000
        }
      },
      "status": "dead_letter",
      "attempts": 7,
      "next_retry_at": null,
      "last_error": "webhook returned status 500",
      "created_at": "2026-07-26T04:00:00Z",
      "delivered_at": null,
      "deliveries": [
        {
          "status_code": 500,
          "response_body": "{\"error\":\"internal server error\"}",
          "error": null,
          "duration_ms": 340,
          "created_at": "2026-07-26T04:33:00Z"
        }
      ]
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null
  }
}
Response fields
FieldTypeMeaning
datarequiredarray

One row per event, newest first.

data.idrequiredstring (uuid)

This event’s own id. QuotaStack repeats it in the webhook-id header, and redeliverWebhookEvent’s id param takes it too.

data.environmentrequiredstring

Sandbox or live. QuotaStack never filters this list by it — expect to see both.

  • live
  • sandbox
data.event_typerequiredstring

This event’s own type, like credit.granted or subscription.renewed.

data.payloadrequiredobject

The exact JSON body QuotaStack sends your endpoint for this one.

data.statusrequiredstring

What state this event is in right now.

  • pendingQueued. QuotaStack has not sent this yet, or a retry is due soon.
  • deliveredYour endpoint said yes. This one is done.
  • failedThe last try failed. QuotaStack tries again soon, up to a point.
  • dead_letterEvery try failed. QuotaStack gave up. Redeliver it once you fix the problem.
data.attemptsrequiredinteger

How many times QuotaStack has tried to send this. Goes back to 0 when you redeliver it.

data.next_retry_atoptionalstring (date-time)

When QuotaStack tries again next. Empty once the event is delivered or dead_letter.

data.last_erroroptionalstring

Why the last try failed, in plain words. Empty until the first one fails.

data.delivered_atoptionalstring (date-time)

When your endpoint said yes. Empty until then.

data.deliveriesrequiredarray

One row per try QuotaStack has made to send this, oldest first.

data.deliveries.status_codeoptionalinteger

The HTTP status your endpoint sent back. Empty when the request never got that far at all.

data.deliveries.response_bodyoptionalstring

The first 4096 bytes of what your endpoint sent back. QuotaStack cuts off anything longer.

data.deliveries.erroroptionalstring

The network error, when QuotaStack got no answer at all. Empty on a real answer, even a bad one.

data.deliveries.duration_msrequiredinteger

How long this one try took, start to end.

Unit: milliseconds.

paginationrequiredobject

Cursor-based pagination envelope. Returned on every list endpoint. No total — cursor-only.

See also

Redeliver a dead-lettered webhook event

POST/v1/webhooks/events/{id}/redeliver
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesSets attempts back to 0 and status back to pending. QuotaStack tries to send it again on its usual schedule.
Fires No webhooks.

This endpoint sends one webhook event again, if its status is dead_letter right now. Anything else returns 409, and nothing about the event changes. QuotaStack looks up your URL again, fresh, every time it tries. A URL you fixed only a moment ago is the one it will use next. attempts goes back to 0 too, and QuotaStack starts the whole schedule over from the top.

Header parameters
FieldTypeMeaning
Idempotency-Keyrequiredstring

Required idempotency key for this mutating admin/public operation. Reusing the same key with a different body returns 409 Conflict; successful 2xx and client 4xx responses are cached for 24 hours.

Path parameters
FieldTypeMeaning
idrequiredstring (uuid)

This event’s own id. The delivery log shows the same value, and so did the webhook-id header your endpoint got.

If you leave it out: QuotaStack rejects the call. This path segment is required.

Response 200
{
  "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a65",
  "environment": "live",
  "event_type": "credit.low_balance",
  "payload": {
    "event_type": "credit.low_balance",
    "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
    "data": {
      "balance_mc": 2000
    }
  },
  "status": "pending",
  "attempts": 0,
  "next_retry_at": "2026-07-27T12:00:00Z",
  "last_error": null,
  "created_at": "2026-07-26T04:00:00Z",
  "delivered_at": null
}

Errors

  • 404No event here has this id. not-found
  • 409QuotaStack returns this for two reasons. The event is not dead_letter — it may be pending, delivered, or still trying again as failed. Reusing the same Idempotency-Key with a different body returns this too. Calling this twice on the same event is safe either way: the second call also gets 409, since the event is pending by then. conflict
  • 422The Idempotency-Key header is missing, or id is not a real UUID. The reply names the field. validation-error

Send a test webhook

POST/v1/webhooks/test
Idempotency-KeyRequired. Returns 422 without it.
EnvironmentSandbox and Live
AuthTenant API key
WritesQueues one new test.ping event. QuotaStack sends it the same way it sends a real webhook event.
Fires test.ping

This endpoint sends a test.ping webhook event to your own endpoint. Sending one is how you prove your endpoint works, before you write any real code for it. QuotaStack needs a URL and a secret set up first; without both, this call returns 409 and sends nothing. Check the result with listWebhookEvents, filtered to event_type=test.ping. See test.ping for the shape of the event itself.

Header parameters
FieldTypeMeaning
Idempotency-Keyrequiredstring

Required idempotency key for this mutating admin/public operation. Reusing the same key with a different body returns 409 Conflict; successful 2xx and client 4xx responses are cached for 24 hours.

Response 202
{
  "event_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a51",
  "event_type": "test.ping",
  "status": "pending"
}
Response fields
FieldTypeMeaning
event_idrequiredstring (uuid)

The new event’s own id. Look it up in listWebhookEvents, or wait for it at your endpoint.

event_typerequiredstring

Always test.ping. This call only ever queues this one type.

statusrequiredstring

Always pending. QuotaStack queued the event, and has not sent it yet.

Errors

  • 409QuotaStack returns this when nothing is set up to send to: no webhook URL, or no secret. Reusing the same Idempotency-Key with a different body returns this too. conflict
  • 422The Idempotency-Key header is missing. QuotaStack asks for it on every write. validation-error