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

# Webhooks

# Webhooks

Receive real-time notifications when events happen in your app. Signed with HMAC-SHA256.

## Setup

```
POST /v1/apps/{app_id}/webhooks
Authorization: Bearer {api_key}
```

```json theme={null}
{
  "webhook": {
    "url": "https://your-server.com/webhooks",
    "event_types": ["verification.sms.verified", "verification.sms.expired"],
    "retry_limit": 3
  }
}
```

| Parameter     | Type      | Required | Description                          |
| ------------- | --------- | -------- | ------------------------------------ |
| `url`         | string    | yes      | HTTPS endpoint                       |
| `event_types` | string\[] | no       | Events to subscribe to. Default: all |
| `retry_limit` | int       | no       | Retry attempts (0-10). Default: 3    |
| `description` | string    | no       | Label                                |
| `metadata`    | object    | no       | Custom metadata                      |
| `enabled`     | bool      | no       | Default: true                        |

***

## Payload

```
Content-Type: application/json
User-Agent: AppName-Webhooks/1.0
X-Webhook-Signature: t=1713232500,v1=5a3c2b...
```

```json theme={null}
{
  "id": "uuid",
  "created_at": "2026-04-16T12:00:00Z",
  "event_type": "verification.sms.verified",
  "app_id": "uuid",
  "data": { ... },
  "api_version": "v1"
}
```

***

## Signature verification

The `X-Webhook-Signature` header contains a timestamp and HMAC:

```
t=1713232500,v1=5a3c2b...
```

Verify:

```javascript theme={null}
const crypto = require("crypto");

function verifyWebhook(payload, header, secret) {
  const [tPart, sigPart] = header.split(",");
  const timestamp = tPart.replace("t=", "");
  const signature = sigPart.replace("v1=", "");

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${payload}`)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}
```

***

## Events

### Verification events

Pattern: `verification.{channel}.{outcome}` — channel is `email` or `sms`.

| Event                            | When                                                                |
| -------------------------------- | ------------------------------------------------------------------- |
| `verification.*.requested`       | Verification sent                                                   |
| `verification.*.verified`        | Contact confirmed (magic link click, OTP code, or consent approval) |
| `verification.*.expired`         | Timed out. Fires even if the link was never opened                  |
| `verification.*.failed`          | OTP max attempts exceeded                                           |
| `verification.*.denied`          | Contact denied the action (consent mode)                            |
| `verification.*.delivery_failed` | SMS/email failed to send (bad number, provider error)               |

Verification payloads include:

```json theme={null}
{
  "data": {
    "verification_id": "uuid",
    "intent": "Password Reset",
    "meta_data": {
      "contact_email": "user@company.com",
      "ticket_id": "12345",
      "delivery_method": "sms"
    },
    "timing": {
      "sent_at": "2026-04-16T01:00:00Z",
      "opened_at": "2026-04-16T01:00:47Z",
      "confirmed_at": "2026-04-16T01:00:52Z",
      "time_to_verify_seconds": 52
    },
    "device": { "os": "iOS", "browser": "Safari" },
    "ip_address": "203.0.113.42"
  }
}
```

Expiration events add `was_opened: true/false`. Consent events add `consent_decision` and `denial_reason`. Delivery failures add `error` and `channel`.

### Auth events

| Event                 | When                         |
| --------------------- | ---------------------------- |
| `auth.login.failed`   | Login attempt failed         |
| `challenge.created`   | Challenge created (any type) |
| `challenge.completed` | Challenge verified           |
| `challenge.failed`    | Challenge failed             |

### User events

| Event                     | When                   |
| ------------------------- | ---------------------- |
| `user.created`            | New user registered    |
| `user.updated`            | User profile updated   |
| `user.identifier_changed` | Email or phone changed |

### System events

| Event          | When                     |
| -------------- | ------------------------ |
| `webhook.test` | Test ping from dashboard |

***

## Managing endpoints

| Action     | Method                                           |
| ---------- | ------------------------------------------------ |
| List       | `GET /v1/apps/{app_id}/webhooks`                 |
| Get        | `GET /v1/apps/{app_id}/webhooks/{id}`            |
| Update     | `PATCH /v1/apps/{app_id}/webhooks/{id}`          |
| Delete     | `DELETE /v1/apps/{app_id}/webhooks/{id}`         |
| Test       | `POST /v1/apps/{app_id}/webhooks/{id}/test`      |
| Deliveries | `GET /v1/apps/{app_id}/webhooks/{id}/deliveries` |

***

## Delivery status

| Status       | Meaning                 |
| ------------ | ----------------------- |
| `pending`    | Queued                  |
| `processing` | In flight               |
| `delivered`  | 2xx response            |
| `failed`     | Will retry if retryable |

***

## Limits

* 10-second timeout per delivery
* Max 10 retries with exponential backoff
* Respond with 2xx within 5 seconds
* 4xx = won't retry. 5xx = will retry.
