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

# Webhooks

> Receive and verify asynchronous model and workflow run notifications.

<Frame caption="Webhooks - Magica pushes events to your server">
  <img className="block dark:hidden" src="https://mintcdn.com/galaxyai-f8150d75/DBhRDarFZNKVqMQi/logo/logo_light_theme.svg?fit=max&auto=format&n=DBhRDarFZNKVqMQi&q=85&s=6f2c433ff16ffb278918ac9ebdf0de20" alt="Diagram of Magica pushing webhook events to a customer server" width="1474" height="411" data-path="logo/logo_light_theme.svg" />

  <img className="hidden dark:block" src="https://mintcdn.com/galaxyai-f8150d75/DBhRDarFZNKVqMQi/logo/logo_dark_theme.svg?fit=max&auto=format&n=DBhRDarFZNKVqMQi&q=85&s=12e176141e9a0793a64292bb91603779" alt="Diagram of Magica pushing webhook events to a customer server" width="1474" height="411" data-path="logo/logo_dark_theme.svg" />
</Frame>

Webhooks let your server receive HTTP POST notifications for asynchronous model and workflow runs. Public REST consumers should verify the signature, respond quickly, and process events idempotently.

<Info>
  Delivery is powered by [Svix](https://www.svix.com): automatic retries,
  HMAC-SHA256 signatures, and delivery logging included.
</Info>

***

## Configure a webhook

Pass a `webhook` object when starting a model run or workflow run.

```bash theme={null}
curl -X POST https://api.magica.com/api/v1/runs \
  -H "Authorization: Bearer $MAGICA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "workflowId": "workflow-or-system-slug",
    "webhook": {
      "url": "https://your-app.com/webhooks/magica",
      "events": ["run.completed", "run.failed"],
      "metadata": { "orderId": "order_456" }
    }
  }'
```

For standalone model runs, include the same `webhook` object in [`POST /v1/nodes/{nodeType}/run`](/api-reference/nodes/run).

***

## Supported events

Model runs and workflow runs support different event sets.

| Event            | Model run | Workflow run | Description                    |
| ---------------- | --------- | ------------ | ------------------------------ |
| `run.started`    | Yes       | Yes          | The run started                |
| `run.completed`  | Yes       | Yes          | The run completed successfully |
| `run.failed`     | Yes       | Yes          | The run failed                 |
| `run.canceled`   | No        | Yes          | The workflow run was canceled  |
| `node.completed` | No        | Yes          | One workflow node completed    |
| `node.failed`    | No        | Yes          | One workflow node failed       |

When you omit `events`, Magica sends every event supported by that run type.

***

## Event payload

```json theme={null}
{
  "success": true,
  "type": "run.completed",
  "runId": "cuid_abc123",
  "workflowId": "cuid_def456",
  "data": { "status": "COMPLETED", "completedNodes": 5, "failedNodes": 0 },
  "metadata": { "orderId": "order_456" },
  "error": null,
  "createdAt": "2026-02-18T12:00:00.000Z"
}
```

| Field        | Type           | Description                                                                   |
| ------------ | -------------- | ----------------------------------------------------------------------------- |
| `success`    | boolean        | `true` for success events, `false` for failures                               |
| `type`       | string         | Event type, such as `run.completed`                                           |
| `runId`      | string         | Execution run ID                                                              |
| `workflowId` | string         | Workflow definition ID for workflow runs; node type for standalone model runs |
| `data`       | object         | Event-specific data                                                           |
| `metadata`   | object \| null | Custom metadata echoed back by the execution surface                          |
| `error`      | string \| null | Error message for failure events                                              |
| `createdAt`  | string         | ISO 8601 timestamp                                                            |

***

## Event types

<CardGroup cols={2}>
  <Card title="run.started" icon="play">
    A run begins executing.
  </Card>

  <Card title="run.completed" icon="circle-check">
    All requested work finished successfully.
  </Card>

  <Card title="run.failed" icon="circle-xmark">
    One or more steps failed.
  </Card>

  <Card title="run.canceled" icon="ban">
    The run was canceled.
  </Card>

  <Card title="node.completed" icon="square-check">
    A single node finished successfully.
  </Card>

  <Card title="node.failed" icon="square-xmark">
    A single node failed.
  </Card>
</CardGroup>

***

## Handler pattern

Always respond `200 OK` immediately and process asynchronously. If your handler takes too long, Svix will retry and you may receive duplicate deliveries.

<Tabs>
  <Tab title="Node.js">
    ```javascript theme={null}
    app.post("/webhooks/magica", (req, res) => {
      res.status(200).send("OK");

      const event = req.body;
      if (event.type === "run.completed") {
        enqueueResultProcessing(event.runId);
      }
      if (event.type === "run.failed") {
        logRunFailure(event.runId, event.error);
      }
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    @app.post("/webhooks/magica")
    async def magica_webhook(request: Request):
        event = await request.json()

        if event["type"] == "run.completed":
            enqueue_result_processing(event["runId"])
        elif event["type"] == "run.failed":
            logger.error("Run failed: %s", event["error"])

        return {"ok": True}
    ```
  </Tab>
</Tabs>

***

## Security

Every webhook is signed with HMAC-SHA256 via Svix. Verify signatures before trusting the payload:

<Tabs>
  <Tab title="Node.js">
    ```javascript theme={null}
    import { Webhook } from "svix";

    app.use("/webhooks/magica", express.raw({ type: "application/json" }));

    const wh = new Webhook("whsec_your_signing_secret");

    app.post("/webhooks/magica", (req, res) => {
      try {
        const payload = wh.verify(req.body, {
          "svix-id": req.headers["svix-id"],
          "svix-timestamp": req.headers["svix-timestamp"],
          "svix-signature": req.headers["svix-signature"],
        });
        res.status(200).send("OK");
        enqueueWebhook(payload);
      } catch (err) {
        res.status(400).send("Invalid signature");
      }
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    from svix.webhooks import Webhook, WebhookVerificationError

    wh = Webhook("whsec_your_signing_secret")

    @app.post("/webhooks/magica")
    async def magica_webhook(request: Request):
        body = await request.body()
        try:
            payload = wh.verify(body, dict(request.headers))
        except WebhookVerificationError:
            return Response(status_code=400, content="Invalid signature")

        enqueue_webhook(payload)
        return Response(status_code=200, content="OK")
    ```
  </Tab>
</Tabs>
