# Events

Jobs and events

Every event type, its data, and how to list and replay events: what webhooks deliver, kept for a month.

An event records something that happened in your account, such as a job that ended or a balance that ran low. Each one is delivered to the [webhook endpoints](https://socialtap.dev/docs/webhooks/index.md) subscribed to its type, and `GET /v1/events` lists every event for 30 days, whether or not a webhook endpoint received it.

| Route | What it does | Key scope |
| --- | --- | --- |
| `GET /v1/events` | List events, newest first | `account:read` |
| `GET /v1/events/{id}` | Get an event | `account:read` |
| `POST /v1/events/{id}/redeliver` | Deliver an event again | `admin` |

## The event object

The body of a webhook delivery is the event object, exactly as `GET /v1/events/{id}` returns it:

| Field | Type | Description |
| --- | --- | --- |
| `object` | `string` | Always event.<br>One of `event` |
| `id` | `string` | The event ID (evt_…): the webhook-id header of every delivery, the same on retries. Deduplicate on it. |
| `type` | `string` | The event type (event types table); new types may be added. |
| `timestamp` | `string` (date-time) | When it happened. |
| `data` | `any` | The type's data object (small: a summary plus links). |

## Event types

| Type | When | data |
| --- | --- | --- |
| `job.completed` | A job ended with every item ended (check request_counts: items fail individually). | `JobEventData` |
| `job.canceled` | A canceled job ended: its running items finished. | `JobEventData` |
| `job.expired` | A job reached its deadline; its unfinished items expired. | `JobEventData` |
| `credits.low` | A balance change took the balance from above the account's low_balance_threshold to at or below it (once per crossing; the owner is emailed too). | `CreditsLowData` |
| `credits.depleted` | The balance reached 0 (once, until credits are added). The owner is also emailed, at most once a day, whatever the notification settings say: it is how a leaked key is noticed. | `CreditsDepletedData` |
| `auto_topup.succeeded` | Auto top-up bought its pack: the charge was paid and the pack's credits added (once). | `AutoTopupCharge` |
| `auto_topup.failed` | An auto top-up charge failed (declined, or the bank asked the cardholder to confirm it): nothing was charged, auto top-up turned off and the owner was emailed why. It never retries: a person turns it back on. | `AutoTopupCharge` |
| `auto_topup.skipped` | The balance crossed the auto top-up threshold, but buying the pack would have passed the monthly cap (the owner is emailed, once a month) or the account's limit per {trust_window_days} days (the owner is emailed how to lift it, once per {trust_window_days} days): nothing was charged. Auto top-up stays on. | `AutoTopupCharge` |
| `plan.renewed` | A monthly plan's payment went through: its credits were added and what was left of last month's ended (the first payment of a plan too). | `PlanEventData` |
| `plan.changed` | An upgrade was paid: the new plan and a new month started now, and its credits were added. | `PlanEventData` |
| `plan.payment_failed` | A monthly plan's renewal payment failed: no new credits were added and the owner was emailed. Stripe retries it; the plan ends if the retries fail. | `PlanEventData` |
| `plan.ended` | A monthly plan ended (canceled, or its payment retries failed): the monthly credits left ended with it, pack and top-up credits stay, and the account is pay as you go. | `PlanEventData` |
| `api_key.spend_limit_reached` | An API key's request (or job) was refused for its spend limit, the first time in the limit's interval. | `SpendLimitReachedData` |
| `api_key.created` | An API key was created (not an account's first keys). | `APIKeyEventData` |
| `api_key.revoked` | An API key was revoked. | `APIKeyEventData` |
| `api_key.secret_rotated` | An API key's secret was rotated: the key has a new secret (prefix), the old one works until its overlap ends. | `APIKeyEventData` |
| `webhook.test` | Sent by POST /v1/webhook_endpoints/{id}/test to that endpoint only. | `WebhookTestData` |

New types may be added; ignore the ones you don't handle. Subscribe an endpoint to a family with a wildcard (`job.*`) or to everything with `*`, and filter `GET /v1/events` the same way with its `type` parameter.

## Job events

`job.completed`, `job.canceled` and `job.expired` carry the job's summary and links, never its results:

| Field | Type | Description |
| --- | --- | --- |
| `object` | `string` | One of `job` |
| `id` | `string` |  |
| `status` | `string` | One of `queued`, `running`, `canceling`, `completed`, `canceled`, `expired` |
| `request_counts` | `object` |  |
| `request_counts.total` | `integer` |  |
| `request_counts.queued` | `integer` | Not started. |
| `request_counts.running` | `integer` | Started (fetching, waiting for a retry or walking pages). |
| `request_counts.succeeded` | `integer` |  |
| `request_counts.failed` | `integer` |  |
| `request_counts.canceled` | `integer` |  |
| `request_counts.expired` | `integer` |  |
| `credits` | `object` |  |
| `credits.max` | `integer` | max_credits: the most the job may spend. |
| `credits.reserved` | `integer` | Credits its running pages may still use (0 once the job ends). |
| `credits.charged` | `integer` | Credits charged so far, each page once (items' prices; failed items are free). |
| `metadata` | `object` |  |
| `created_at` | `string` (date-time) |  |
| `finished_at` | `string` (date-time) or `null` |  |
| `url` | `string` | GET this (with your API key) for the job. |
| `results_url` | `string` | Every result as JSON Lines, the job's signed results_url: it works without the API key for 1 hour after the event; GET url for a fresh one. Reading the event with a key without jobs:read, this is url instead: the link is a job's results, whatever key holds it. |

`url` is the job (GET it with your API key); `results_url` is the job's own signed JSON Lines link to every result, which works without the key until it expires (GET `url` for a fresh one). `GET /v1/events` and `GET /v1/events/{id}` answer it only to a key that can read jobs (`jobs:read`); for any other it holds `url`.

## Account events

The balance and spend events come from the account itself, `api_key.created`, `api_key.revoked` and `api_key.secret_rotated` from managing keys, and `webhook.test` from a test delivery. Their data:

### `credits.low`

| Field | Type | Description |
| --- | --- | --- |
| `balance` | `integer` | The balance after the change that crossed the threshold. |
| `low_balance_threshold` | `integer` | The account's low_balance_threshold at the time. |

### `credits.depleted`

| Field | Type | Description |
| --- | --- | --- |
| `balance` | `integer` | The balance: 0. |

### `auto_topup.succeeded`, `auto_topup.failed`, `auto_topup.skipped`

| Field | Type | Description |
| --- | --- | --- |
| `object` | `string` | Always auto_topup_charge.<br>One of `auto_topup_charge` |
| `id` | `string` | Top-up ID (atu_…): the ref of its credit_transaction, and on the Stripe payment's metadata. |
| `status` | `string` | processing (the charge is under way), succeeded (paid: its credits were added, once), failed (nothing charged: auto top-up turned off, see reason) or skipped (it would have passed the monthly cap or the 7-day limit: nothing charged, auto top-up stays on).<br>One of `processing`, `succeeded`, `failed`, `skipped` |
| `pack` | `string` | The credit pack (catalog pack ID). |
| `credits` | `integer` | Credits the pack adds. |
| `amount_total` | `integer` or `null` | Charged, tax included, in cents of currency; null when nothing was charged or before it is known. |
| `amount_tax` | `integer` or `null` | The tax in amount_total (Stripe Tax: only where SocialTap collects it); null when amount_total is. |
| `currency` | `string` | amount_total's currency (ISO 4217).<br>One of `usd` |
| `reason` | `string` or `null` | Why it failed (the off_reason it set) or was skipped (monthly_cap, trust_limit: it would have passed rolling_limit_cents); null otherwise.<br>One of `card_declined`, `authentication_required`, `no_card`, `billing_address_required`, `payment_failed`, `disputed`, `monthly_cap`, `trust_limit` |
| `message` | `string` or `null` | What the bank or Stripe said, for people; null when reason is. |
| `created_at` | `string` (date-time) | When the balance crossed the threshold, RFC 3339 UTC. |

### `plan.renewed`, `plan.changed`, `plan.payment_failed`, `plan.ended`

| Field | Type | Description |
| --- | --- | --- |
| `plan` | `string` | The monthly plan (catalog plan ID). |
| `credits` | `integer` | Credits this event added; 0 when it added none. |
| `current_period_end` | `string` (date-time) or `null` | When the plan renews next, RFC 3339 UTC; null when it has ended. |

### `api_key.spend_limit_reached`

| Field | Type | Description |
| --- | --- | --- |
| `api_key_id` | `string` | The key (key_…) whose spend limit was reached. |
| `api_key_name?` | `string` or `null` | Its name (null if unreadable; absent from events sent before 2026-10-31). |
| `spend_limit` | `object` | Its spend limit. |
| `spend_limit.amount` | `integer` | Most credits the key may spend per interval. |
| `spend_limit.interval` | `string` | Calendar day or month, UTC.<br>One of `day`, `month` |
| `used` | `integer` | Credits the key had used this interval (spent plus held by its requests in flight). |
| `resets_at` | `string` (date-time) | Start of the next interval, RFC 3339 UTC: the key can spend again then. |

### `api_key.created`, `api_key.revoked`, `api_key.secret_rotated`

| Field | Type | Description |
| --- | --- | --- |
| `api_key_id` | `string` | The key (key_…). |
| `name` | `string` | Its label. |
| `prefix` | `string` | The secret's first characters, to recognize it. |

### `webhook.test`

| Field | Type | Description |
| --- | --- | --- |
| `webhook_endpoint_id` | `string` |  |

---

[HTML version](https://socialtap.dev/docs/events/) · [Docs index for AI agents](https://socialtap.dev/llms.txt) · [All docs in one file](https://socialtap.dev/llms-full.txt)
