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

# Webhooks

> Test and verify webhook delivery

# Webhooks

Test your webhook endpoint to verify it can receive notifications from meter.

## Test webhook delivery

Send a sample webhook payload to verify your endpoint is configured correctly.

```http theme={null}
POST /api/webhooks/test
```

### Request body

```json theme={null}
{
  "webhook_url": "https://your-app.com/webhooks/meter"
}
```

| Field         | Type   | Required | Description             |
| ------------- | ------ | -------- | ----------------------- |
| `webhook_url` | string | Yes      | The webhook URL to test |

### Response

```json theme={null}
{
  "success": true,
  "status_code": 200,
  "message": "Webhook delivered successfully"
}
```

| Field         | Type    | Description                                                      |
| ------------- | ------- | ---------------------------------------------------------------- |
| `success`     | boolean | Whether the webhook was delivered successfully (2xx response)    |
| `status_code` | integer | HTTP status code returned by your endpoint (0 if request failed) |
| `message`     | string  | Human-readable result message                                    |

### Example

```bash theme={null}
curl -X POST https://api.meter.sh/api/webhooks/test \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://my-app.com/webhook"
  }'
```

## Webhook payload formats

Meter sends webhooks for both successful and failed jobs. The payload structure differs based on the job status.

### Success payload

Sent when a job completes successfully:

```json theme={null}
{
  "job_id": "660e8400-e29b-41d4-a716-446655440000",
  "schedule_id": "880e8400-e29b-41d4-a716-446655440000",
  "status": "completed",
  "url": "https://example.com/products",
  "results": [
    {"title": "Sample Product A", "price": "$19.99"},
    {"title": "Sample Product B", "price": "$29.99"}
  ],
  "item_count": 2,
  "has_changes": true,
  "content_hash": "7f3d9a2b4c1e5f8a9b0c1d2e3f4a5b6c",
  "completed_at": "2025-01-15T10:30:12Z",
  "delivery_reason": "first_run",
  "metadata": {"project": "my-project"}
}
```

| Field             | Type     | Description                                                                                                                                                                        |
| ----------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `job_id`          | UUID     | ID of the scrape job                                                                                                                                                               |
| `schedule_id`     | UUID     | ID of the schedule that triggered the job                                                                                                                                          |
| `status`          | string   | Always `completed` for success webhooks                                                                                                                                            |
| `url`             | string   | The URL that was scraped                                                                                                                                                           |
| `results`         | array    | Extracted data from the page                                                                                                                                                       |
| `item_count`      | integer  | Number of items extracted                                                                                                                                                          |
| `has_changes`     | boolean  | Whether content changed since last run                                                                                                                                             |
| `content_hash`    | string   | Hash of the extracted content                                                                                                                                                      |
| `completed_at`    | datetime | When the job completed                                                                                                                                                             |
| `delivery_reason` | string   | Why this webhook fired: `first_run` for the first successful delivery for this `schedule + url`, `content_changed` for subsequent deliveries triggered by a `content_hash` change. |
| `metadata`        | object   | Custom JSON metadata from `webhook_metadata` (if configured)                                                                                                                       |

<Note>
  `delivery_reason` is only included in `standard` webhook payloads. The
  `slack`, `slack_workflow`, and `discord` formatters cherry-pick fields for
  human display and do not forward it.
</Note>

### Failure payload

Sent when a job fails:

```json theme={null}
{
  "job_id": "660e8400-e29b-41d4-a716-446655440000",
  "schedule_id": "880e8400-e29b-41d4-a716-446655440000",
  "status": "failed",
  "url": "https://example.com/products",
  "error": "Page not accessible: 404 Not Found",
  "completed_at": "2025-01-15T10:30:12Z",
  "metadata": {"project": "my-project"}
}
```

| Field          | Type     | Description                                                  |
| -------------- | -------- | ------------------------------------------------------------ |
| `job_id`       | UUID     | ID of the scrape job                                         |
| `schedule_id`  | UUID     | ID of the schedule that triggered the job                    |
| `status`       | string   | Always `failed` for failure webhooks                         |
| `url`          | string   | The URL that failed to scrape                                |
| `error`        | string   | Error message describing what went wrong                     |
| `completed_at` | datetime | When the job failed                                          |
| `metadata`     | object   | Custom JSON metadata from `webhook_metadata` (if configured) |

<Note>
  Failure webhooks do not include `results`, `item_count`, `has_changes`, or `content_hash` fields.
</Note>

## Webhook types

Meter supports four webhook formats:

| Type             | Description                              |    Secret required   |
| ---------------- | ---------------------------------------- | :------------------: |
| `standard`       | Full JSON payload (default)              | Yes (auto-generated) |
| `slack`          | Formatted Slack incoming webhook message | Yes (auto-generated) |
| `slack_workflow` | Slack Workflow Builder trigger payload   |          No          |
| `discord`        | Formatted Discord webhook embed          |          No          |

### Auto-detection rules

The webhook type is auto-detected from the URL when not explicitly set:

| URL pattern                                                            | Detected type    |
| ---------------------------------------------------------------------- | ---------------- |
| Contains `hooks.slack.com/services/`                                   | `slack`          |
| Contains `hooks.slack.com/triggers/`                                   | `slack_workflow` |
| Contains `discord.com/api/webhooks/` or `discordapp.com/api/webhooks/` | `discord`        |
| Everything else                                                        | `standard`       |

You can override auto-detection by setting `webhook_type` explicitly when creating a schedule.

<Note>
  Discord and Slack Workflow types do not require a `webhook_secret`. For `standard` and `slack` types, a secret is auto-generated if not provided.
</Note>

## Webhook secrets

When a schedule has a `webhook_url`, Meter auto-generates a secret with a `whsec_` prefix. The secret is sent in the `X-Webhook-Secret` header on every webhook delivery. Verify this header to ensure requests are from Meter.

See the [Webhooks Guide](/guides/webhooks#webhook-secrets) for verification examples.

## Retry behavior

Failed deliveries are retried up to 5 times with exponential backoff: 15 minutes, 30 minutes, 1 hour, 2 hours, 4 hours.

* **2xx**: Success, no retry
* **4xx**: Permanent failure, no retry
* **5xx / timeout / connection error**: Retries with backoff

## Error responses

| Status | Description                            |
| ------ | -------------------------------------- |
| `400`  | Invalid request (missing webhook\_url) |
| `401`  | Invalid or missing API key             |
| `422`  | Invalid URL format                     |
| `500`  | Internal server error                  |
| `503`  | Service temporarily unavailable        |

See [REST API Errors](/api-reference/rest/errors) for detailed error handling.

## Next steps

<CardGroup cols={2}>
  <Card title="Webhooks Guide" icon="webhook" href="/guides/webhooks">
    Learn how to handle webhook notifications
  </Card>

  <Card title="Schedules" icon="clock" href="/api-reference/rest/schedules">
    Set up scheduled scraping with webhooks
  </Card>
</CardGroup>
