quotastack Docs
Docs / API / Events / subscription.contract_ended

subscription.contract_ended

Fired when a postpaid contract_end is reached.

When it fires

QuotaStack sends this when the contract end date arrives. The subscription moves to contract_ended. A QuotaStack job raises it.

When it does not fire

QuotaStack does not send this for a subscription with no contract end date. A contract that ended is not the same as one that ran out. You can bring this one back with the resume call.

data
FieldTypeMeaning
idrequiredstring (uuid)
tenant_idrequiredstring (uuid)
customer_idrequiredstring (uuid)
plan_variant_idrequiredstring (uuid)
environmentrequiredstring
  • live
  • sandbox

Which environment this resource lives in. Determined by the API key prefix used (qs_live_… → live, qs_test_… → sandbox).

statusrequiredstring
  • trialing
  • active
  • cancelling
  • canceled
  • expired
  • paused
  • overdue
  • contract_ended
originoptionalstring
  • api
  • import

How the subscription was created — api (normal create) or import (legacy cutover).

started_atrequiredstring (date-time)
trial_ends_atoptionalstring (date-time)
current_period_startoptionalstring (date-time)
current_period_endoptionalstring (date-time)
billing_anchoroptionalinteger

Day of month (1-28) for monthly renewals.

cancel_at_period_endrequiredboolean
scheduled_variant_idoptionalstring (uuid)
external_subscription_idoptionalstring
paused_atoptionalstring (date-time)
resumed_atoptionalstring (date-time)
expired_atoptionalstring (date-time)
contract_startoptionalstring (date-time)
contract_endoptionalstring (date-time)
contract_ending_soon_daysrequiredinteger
metadatarequiredobject
created_atrequiredstring (date-time)
updated_atrequiredstring (date-time)
Example payload
{
  "event_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a51",
  "event_type": "subscription.contract_ended",
  "tenant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a01",
  "environment": "live",
  "customer_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a05",
  "external_customer_id": "user_42",
  "created_at": "2026-08-31T00:00:00Z",
  "idempotency_key": "sub-contract-end:0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a21",
  "data": {
    "id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a21",
    "plan_variant_id": "0192f5a4-7c31-7b8e-9a2d-4f6c8e1b3a12",
    "status": "contract_ended",
    "contract_end": "2026-08-31T00:00:00Z"
  }
}

What to do

Sign the new contract, then resume. Or close the account.

Which calls fire this

  • QuotaStack's scheduler, on a timer — no API call involved.

See also

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:

AttemptDelay after previous
1Immediate
230 seconds
35 minutes
430 minutes
52 hours
68 hours
724 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.