> ## Documentation Index
> Fetch the complete documentation index at: https://docs.amps.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook Model

> Event taxonomy for action lifecycle. Delayed delivery, retries with backoff, signed payloads. Sandbox vs live timing.

Webhooks are how Amps pushes events to you. When an action reaches a terminal state or a device's connection status changes, a signed HTTP POST lands on your registered endpoint. Delivery is retried with exponential backoff; endpoints are managed from the dashboard. Subscribe to anything you would otherwise poll.

Polling is the right default for dashboards where the user accepts visible cache freshness. Events are the right default for any state change a user is waiting on. Pick once at integration time and the rest of the design follows. Events are also how you give end users fast feedback. A user waiting for a charge to start expects the UI to update when the device confirms; a polling cadence that runs every minute can't deliver that.

## Event taxonomy

Two categories exist. Lifecycle events track device connection state. Push-action events track command outcomes.

| Event                 | Category  | When it fires                                                                                                |
| --------------------- | --------- | ------------------------------------------------------------------------------------------------------------ |
| `device.connected`    | Lifecycle | A new device finishes registration via the Auth Journey.                                                     |
| `device.reconnected`  | Lifecycle | An existing device's credentials are refreshed.                                                              |
| `device.disconnected` | Lifecycle | Stored credentials become invalid. May carry a `reconnectionUrl` when the OEM's auth flow supports re-entry. |
| `push.completed`      | Push      | The OEM accepted the command. The action is in `completed`.                                                  |
| `push.failed`         | Push      | The OEM rejected, or a transient failure exhausted the path. The action is in `failed`.                      |

<Callout icon="clock" color="#ED6D2C">
  **Coming soon.** Schedule lifecycle events (`schedule.created`, `schedule.updated`, `schedule.cancelled`, `schedule.slot_failed`, `schedule.slot_skipped`) ship with the [scheduler](/concepts/scheduling). They will flow through the same delivery path as the events above.
</Callout>

<Note>
  `completed` means the OEM API accepted the command, not that the device verified end-state. Verifying end-state would mean polling the OEM after every command, doubling latency without telling you anything the next pull cannot. Tail the device pull or wait for a state-change webhook if end-state confirmation matters.
</Note>

## Payload shape

Webhook bodies are flat. The `svix-id`, `svix-timestamp`, and `svix-signature` headers carry the delivery ID (use it for idempotency), the send time, and the signature; they do not name the event type. You determine the event from the payload itself: a push-action body carries an `actionId` (with `completedAt` on success or `errorCode` on failure), while a lifecycle body carries `deviceId`/`deviceType`/`timestamp` (with `reconnectionUrl` on a disconnect).

Lifecycle events carry the device identifiers:

```json theme={null}
{
  "deviceId": "device_abc123",
  "deviceType": "battery",
  "timestamp": "2026-06-01T10:14:23Z"
}
```

`device.disconnected` may add a `reconnectionUrl` pointing at the Auth Journey in reconnection mode, when the OEM's auth flow supports re-entry:

```json theme={null}
{
  "deviceId": "device_abc123",
  "deviceType": "battery",
  "timestamp": "2026-06-01T10:14:23Z",
  "reconnectionUrl": "https://auth.amps.ai/?reconnect=true&token=..."
}
```

Push-action events carry the action state loaded fresh at delivery time. The payload mirrors the dispatch and the read response: the canonical `command`, the constraints-only `parameters`, and the clean `deviceType`.

```json theme={null}
{
  "actionId": "action_xyz789",
  "deviceId": "device_abc123",
  "deviceType": "battery",
  "command": "charge",
  "parameters": { "target": { "value": 90, "unit": "percent" } },
  "result": { "success": true, "message": "Mode applied" },
  "completedAt": "2026-06-01T18:00:04Z"
}
```

The payload is loaded fresh from the action record at delivery time, not built from the event that triggered the webhook. The payload always reflects current truth on delivery. Re-fetching the action at `GET /actions/{actionId}` returns the same `command`/`parameters`/`deviceType` shape.

## Delayed delivery

Webhook delivery is delayed by a fixed interval after the trigger:

| Environment | Delay       |
| ----------- | ----------- |
| Live        | 10 seconds  |
| Sandbox     | 180 seconds |

The sandbox delay gives you time to set up a tunnel and an endpoint between submitting the action and the webhook landing. The live delay leaves room for any post-processing that should complete before you receive the event (audit trail written, response cache populated).

Webhook delivery is isolated from the dispatch path, so webhook volume cannot affect command latency.

## Retries

A 2xx from your endpoint marks the delivery successful. Anything else triggers a retry on a backoff schedule. The retry policy and delivery history are configurable from the dashboard.

Idempotency on your side is mandatory. Treat each event as potentially deliverable more than once. Dedupe by `actionId` for push events and by the `svix-id` header (the per-delivery idempotency key, stable across retries) for lifecycle events.

## Signature verification

Every webhook carries three headers (`svix-id`, `svix-timestamp`, `svix-signature`). Verify the signature before processing; the SDK reads all three. The signature is computed over the raw request body; verifying after JSON parsing or whitespace normalisation will fail.

```javascript theme={null}
import { Webhook } from 'svix';

const webhook = new Webhook(process.env.WEBHOOK_SECRET);

app.post('/webhooks', (req, res) => {
  try {
    const verified = webhook.verify(req.rawBody, req.headers);
    handleEvent(verified);
    res.status(200).send('OK');
  } catch {
    res.status(400).send('Invalid signature');
  }
});
```

The webhook secret is per-endpoint and visible in the dashboard. Rotate it from the dashboard; rotation does not affect your API key. The full reference, including language-specific examples, is at [webhook security](/guides/webhooks/verify-signatures).

<Warning>
  Never process webhooks without verifying signatures. An unverified endpoint accepts forged events from anyone who can reach it.
</Warning>

## Endpoint configuration

You get a separate webhook consumer per environment. Webhook delivery targets the consumer matching the API key that produced the event. Sandbox events go to your sandbox consumer; live events go to your live consumer. Configure endpoints from the dashboard's webhook settings.

Multiple endpoints per environment are supported. Each endpoint is delivered independently, so failing one does not affect the others.

## Operational behaviour

A few specifics worth knowing:

| Scenario                                                  | Behaviour                                                                                                 |
| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| Push action delivered while still in a non-terminal state | Skipped at the delivery step; the task is acknowledged.                                                   |
| Action ID present in the task but absent from storage     | Same as above. Logged and acknowledged.                                                                   |
| Pull failure with `INVALID_CREDENTIALS`                   | Emits a `device.disconnected` lifecycle webhook in addition to whatever the operation produced.           |
| Push failure with `INVALID_CREDENTIALS`                   | Emits both a push-action webhook and a `device.disconnected` lifecycle webhook.                           |
| Webhook delivery URL not configured                       | Push and pull operations complete without error; no webhook is scheduled. The misconfiguration is logged. |

## Webhook boundaries

* No per-event-type filtering at the subscription level. Every endpoint receives every event in scope. Dispatch from your handler.
* No batch delivery. Each event is one HTTP request.
* Replay is exposed through the dashboard rather than an in-product UI.

## Sandbox vs live

Sandbox webhooks let you test your handler before you have a live device. Simulated devices emit realistic action lifecycle events. The 3-minute sandbox delay covers initial wiring; the 10-second live delay matches production once you switch keys.

The same delivery infrastructure backs both environments: same signature scheme, same retry behaviour, same payload shape. Switch from sandbox to live by switching the API key and updating the consumer endpoint. Your handler logic does not need to change.

## Frequently asked questions

### How quickly are webhooks delivered after an action completes?

Live webhooks land roughly 10 seconds after the action reaches its terminal state. Sandbox webhooks land after 180 seconds. Webhook delivery is isolated from the dispatch path, so webhook volume does not affect command latency. The sandbox delay exists for developer ergonomics: it gives you time to set up a tunnel and an endpoint between submitting the action and the webhook landing.

### What happens if my webhook endpoint is down?

The platform retries on a backoff schedule. A 2xx from your endpoint marks the delivery successful; anything else triggers retry. The retry policy and visible delivery history are configurable from the dashboard. Persistent failures eventually exhaust the retry budget and stop. Idempotency on your side is mandatory: treat each event as potentially deliverable more than once. Dedupe by `actionId` for push events and by the `svix-id` header (the per-delivery idempotency key, stable across retries) for lifecycle events.

### How do I verify a webhook signature?

Every webhook carries an `svix-signature` header computed over the raw request body. Verify before processing using the Svix SDK or its equivalent in your language. The signature is computed over raw bytes, so verifying after JSON parsing or whitespace normalisation will fail. The webhook secret is per-endpoint and surfaced in the dashboard. Rotate it from the dashboard; rotation does not affect your API key. Never process webhooks without verifying signatures.

### What does completed mean in a push.completed webhook?

It means the OEM API accepted the command, not that the device verified end-state. Verifying end-state would require polling the OEM after every command, doubling latency without adding value the next pull cannot deliver. Tail the device pull or wait for a state-change webhook if end-state confirmation matters. The webhook payload is loaded fresh at delivery time, so it reflects the latest truth on delivery.

### Can I filter which events my endpoint receives?

Not at the subscription level. Every endpoint receives every event in scope. Use your handler to dispatch on the body shape: a push event with `errorCode` is `push.failed`, a push event with only `completedAt` is `push.completed`, a lifecycle body without `actionId` is one of the `device.*` events. Multiple endpoints per environment are supported and each is delivered independently; failing one endpoint does not affect delivery to the others. Filtering, batching, and replay are exposed through the dashboard.

## Related concepts

<CardGroup cols={2}>
  <Card title="Scheduling" icon="calendar" href="/concepts/scheduling">
    Action lifecycle states that produce webhooks.
  </Card>

  <Card title="Auth Journey" icon="user-plus" href="/concepts/auth-model">
    `device.connected` fires when registration completes.
  </Card>

  <Card title="Webhook Security" icon="shield" href="/guides/webhooks/verify-signatures">
    Signature verification reference.
  </Card>

  <Card title="Device State" icon="gauge" href="/concepts/device-state">
    Polling versus webhook patterns for state changes.
  </Card>
</CardGroup>

For a worked walkthrough wiring a webhook handler, see the cookbook: [subscribe to webhooks](/guides/cookbook/subscribe-webhooks).

<script
  type="application/ld+json"
  dangerouslySetInnerHTML={{
__html: JSON.stringify({
  "@context": "https://schema.org",
  "@type": "FAQPage",
  mainEntity: [
    {
      "@type": "Question",
      name: "How quickly are webhooks delivered after an action completes?",
      acceptedAnswer: {
        "@type": "Answer",
        text: "Live webhooks land roughly 10 seconds after the action reaches its terminal state. Sandbox webhooks land after 180 seconds. Webhook delivery is isolated from the dispatch path, so webhook volume does not affect command latency. The sandbox delay exists for developer ergonomics: it gives you time to set up a tunnel and an endpoint between submitting the action and the webhook landing."
      }
    },
    {
      "@type": "Question",
      name: "What happens if my webhook endpoint is down?",
      acceptedAnswer: {
        "@type": "Answer",
        text: "The platform retries on a backoff schedule. A 2xx from your endpoint marks the delivery successful; anything else triggers retry. The retry policy and visible delivery history are configurable from the dashboard. Persistent failures eventually exhaust the retry budget and stop. Idempotency on your side is mandatory: treat each event as potentially deliverable more than once. Dedupe by actionId for push events and by the svix-id header (the per-delivery idempotency key, stable across retries) for lifecycle events."
      }
    },
    {
      "@type": "Question",
      name: "How do I verify a webhook signature?",
      acceptedAnswer: {
        "@type": "Answer",
        text: "Every webhook carries an svix-signature header computed over the raw request body. Verify before processing using the Svix SDK or its equivalent in your language. The signature is computed over raw bytes, so verifying after JSON parsing or whitespace normalisation will fail. The webhook secret is per-endpoint and surfaced in the dashboard. Rotate it from the dashboard; rotation does not affect your API key. Never process webhooks without verifying signatures."
      }
    },
    {
      "@type": "Question",
      name: "What does completed mean in a push.completed webhook?",
      acceptedAnswer: {
        "@type": "Answer",
        text: "It means the OEM API accepted the command, not that the device verified end-state. Verifying end-state would require polling the OEM after every command, doubling latency without adding value the next pull cannot deliver. Tail the device pull or wait for a state-change webhook if end-state confirmation matters. The webhook payload is loaded fresh at delivery time, so it reflects the latest truth on delivery."
      }
    },
    {
      "@type": "Question",
      name: "Can I filter which events my endpoint receives?",
      acceptedAnswer: {
        "@type": "Answer",
        text: "Not at the subscription level. Every endpoint receives every event in scope. Use your handler to dispatch on the body shape: a push event with errorCode is push.failed, a push event with only completedAt is push.completed, a lifecycle body without actionId is one of the device.* events. Multiple endpoints per environment are supported and each is delivered independently; failing one endpoint does not affect delivery to the others. Filtering, batching, and replay are exposed through the dashboard."
      }
    }
  ]
})
}}
/>
