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

# Webhooks

> Receive real-time event notifications for asset processing

## Overview

Webhooks let you receive real-time notifications when asset processing finishes. Instead of polling the submission status endpoint, your system is notified via HTTPS POST as soon as an event occurs.

## How webhooks work

<Steps>
  <Step title="Create a subscription">
    Register an HTTPS endpoint URL and the event types you want to receive. The API returns a signing `secret`.
  </Step>

  <Step title="Receive deliveries">
    When an asset finishes processing, the API sends a signed POST to your endpoint.
  </Step>

  <Step title="Verify and acknowledge">
    Verify the signature, respond quickly with a 2xx, and process asynchronously.
  </Step>

  <Step title="Fetch detail">
    The payload does not embed results. Fetch the asset detail, issues, or topics from the REST API.
  </Step>
</Steps>

## Event types

Only two event types exist.

| Event type                   | Trigger                                   |
| ---------------------------- | ----------------------------------------- |
| `asset.processing.completed` | An asset finishes processing successfully |
| `asset.processing.failed`    | An asset fails to process                 |

## Creating a webhook subscription

POST `/api/integrations/webhooks/subscriptions`

The request uses `endpointUrl` (HTTPS required), `eventTypes`, and an optional `description`.

```json theme={null}
{
  "endpointUrl": "https://example.com/webhooks",
  "eventTypes": ["asset.processing.completed", "asset.processing.failed"],
  "description": "Production webhook endpoint"
}
```

Response `201 Created`. The `secret` is returned only on create:

```json theme={null}
{
  "id": "550e8400-...",
  "workspaceId": "660e8400-...",
  "endpointUrl": "https://example.com/webhooks",
  "eventTypes": ["asset.processing.completed", "asset.processing.failed"],
  "description": "Production webhook endpoint",
  "enabled": true,
  "secret": "whsec_AbCdEf...",
  "createdAt": "2026-06-03T12:00:00Z",
  "updatedAt": "2026-06-03T12:00:00Z"
}
```

<Warning>
  Save the `secret` immediately — it is never returned again after create. You use it to verify webhook signatures.
</Warning>

A `422` is returned for a non-HTTPS URL or when the workspace reaches its subscription limit.

## Webhook delivery payload

When an event occurs, the API sends a Standard Webhooks envelope to your endpoint. The outer fields are `id`, `type`, `timestamp`, and `data`. The `data` object is snake\_case.

```json theme={null}
{
  "id": "evt_uuid",
  "type": "asset.processing.completed",
  "timestamp": "2026-06-03T12:05:00Z",
  "data": {
    "asset_id": "a1...",
    "submission_id": "s1...",
    "workspace_id": "w1...",
    "status": "completed",
    "filename": "campaign-video.mp4",
    "issue_count": 3
  }
}
```

A failure delivery uses the same envelope with `data.status` set to `"failed"` and an added `data.error_message`:

```json theme={null}
{
  "id": "evt_uuid",
  "type": "asset.processing.failed",
  "timestamp": "2026-06-03T12:10:00Z",
  "data": {
    "asset_id": "a1...",
    "submission_id": "s1...",
    "workspace_id": "w1...",
    "status": "failed",
    "error_message": "Source media could not be decoded.",
    "filename": "bad.mp4"
  }
}
```

<Note>
  The payload does not embed results. To get issues, topics, or full asset detail, fetch them from the REST API: `GET /api/integrations/submissions/{submission_id}/assets/{asset_id}`, its `/issues`, or its `/topics`. See [Assets](/concepts/assets#asset-results).
</Note>

## Verifying webhook signatures

Deliveries are signed using the Standard Webhooks format. Always verify the signature using your subscription `secret`. After verification, read the event type from `event["type"]`.

<CodeGroup>
  ```python theme={null}
  from standardwebhooks.webhooks import Webhook

  wh = Webhook("whsec_AbCdEf...")

  try:
      event = wh.verify(
          payload=request.body,
          headers=dict(request.headers),
      )
      print("Signature valid:", event["type"])
  except Exception as e:
      print("Invalid signature:", e)
      return {"error": "Unauthorized"}, 401
  ```

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

  const wh = new Webhook("whsec_AbCdEf...");

  try {
    const event = wh.verify(payload, headers);
    console.log("Signature valid:", event.type);
  } catch (e) {
    console.log("Invalid signature:", e);
    return res.status(401).json({ error: "Unauthorized" });
  }
  ```
</CodeGroup>

## Handling webhooks

Your endpoint should:

1. **Verify the signature** (see above).
2. **Respond quickly** — return a 2xx immediately, then process in the background.
3. **Process idempotently** — handle the same event `id` being delivered more than once.
4. **Fetch detail** — load issues or topics from the REST API as needed.

<CodeGroup>
  ```python fastapi theme={null}
  from fastapi import FastAPI, Request
  from standardwebhooks.webhooks import Webhook

  app = FastAPI()
  wh = Webhook("whsec_AbCdEf...")

  @app.post("/webhooks")
  async def handle_webhook(request: Request):
      try:
          payload = await request.body()
          event = wh.verify(payload, dict(request.headers))
      except Exception:
          return {"error": "Unauthorized"}, 401

      event_id = event["id"]
      event_type = event["type"]
      print(f"Received {event_type} (ID: {event_id})")

      # Queue for background processing; fetch asset detail from the REST API.
      return {"status": "received"}
  ```

  ```javascript express theme={null}
  const express = require("express");
  const { Webhook } = require("standardwebhooks");

  const app = express();
  const wh = new Webhook("whsec_AbCdEf...");

  app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => {
    try {
      const event = wh.verify(req.body, req.headers);
      console.log(`Received ${event.type} (ID: ${event.id})`);

      // Queue for background processing; fetch asset detail from the REST API.
      res.json({ status: "received" });
    } catch (e) {
      console.log("Invalid signature:", e);
      res.status(401).json({ error: "Unauthorized" });
    }
  });
  ```
</CodeGroup>

## Delivery and retries

Webhook delivery is handled by a delivery provider. If your endpoint returns a non-2xx status, the delivery is retried automatically. Review attempt history through the deliveries endpoint or in the control plane rather than relying on a fixed retry schedule.

GET `/api/integrations/webhooks/subscriptions/{subscription_id}/deliveries`

Query parameters: `status` (optional filter) and `limit` (1–200, default 50).

## Testing webhooks

### Using ngrok

For local testing, expose your local server publicly:

```bash theme={null}
ngrok http 3000
```

This gives you a public HTTPS URL like `https://abc123.ngrok.io`. Create a subscription pointing to it.

## Managing subscriptions

### List subscriptions

GET `/api/integrations/webhooks/subscriptions`

Returns a paginated envelope. The `secret` is never included after create.

```json theme={null}
{
  "items": [
    {
      "id": "550e8400-...",
      "workspaceId": "660e8400-...",
      "endpointUrl": "https://example.com/webhooks",
      "eventTypes": ["asset.processing.completed", "asset.processing.failed"],
      "description": "Production webhook endpoint",
      "enabled": true,
      "createdAt": "2026-06-03T12:00:00Z",
      "updatedAt": "2026-06-03T12:00:00Z"
    }
  ],
  "page": 1,
  "pageSize": 10,
  "total": 3,
  "totalPages": 1
}
```

Query parameters: `page` (≥1), `pageSize` (1–100), `sortBy` (`endpoint_url` | `created_at` | `updated_at`), `sortOrder` (`asc` | `desc`), `enabled` (bool), and `search`.

### Get a subscription

GET `/api/integrations/webhooks/subscriptions/{subscription_id}`

Returns a single subscription (without `secret`).

### Update a subscription

PATCH `/api/integrations/webhooks/subscriptions/{subscription_id}`

Only enabling or disabling is supported:

```json theme={null}
{ "enabled": false }
```

### Delete a subscription

DELETE `/api/integrations/webhooks/subscriptions/{subscription_id}`

Returns `204 No Content`.

## Webhook best practices

* **Verify signatures** — always validate the signature using your subscription `secret`.
* **Respond quickly** — return a 2xx promptly and process asynchronously.
* **Process idempotently** — handle duplicate event IDs gracefully.
* **Fetch detail on demand** — the payload does not embed results; load them from the REST API.
* **Store the secret securely** — keep it in environment variables or a secret manager.
