# Webhooks

`GET | POST | DELETE /v1/webhooks` — Signed events when renders land and flow runs end — what Zapier, Make and n8n subscribe to.

- Base URL: https://eroq.ai/v1
- Auth: `Authorization: Bearer eroq_sk_…` (create keys at https://eroq.ai/dashboard/keys)
- Credits: free

An endpoint gets a signed POST for each event it listens to: `image.generation.succeeded` / `image.generation.failed`, `video.generation.succeeded` / `video.generation.failed`, `flow.run.succeeded` / `flow.run.failed` (a flow run ended) and `flow.run.waiting` (a run waits on a post for someone's click). Create endpoints here or in the dashboard (Developers › Webhooks); `flow_id` narrows the flow events to one flow. Up to 25 per workspace. Managing them takes the `webhooks:manage` role (owners and admins by default).

**REST hooks.** Zapier, Make and n8n subscribe when a Zap, a scenario or a workflow with an eroq trigger is turned on (`POST /v1/webhooks`) and unsubscribe when it is turned off (`DELETE /v1/webhooks/{id}`). An endpoint that answers `410 Gone` is deleted. `GET /v1/webhooks/samples?event=…` gives the last few real events of a type (or one example): the sample a trigger shows before the first real event fires.

Every delivery is `{ "id": "evt_…", "type": "…", "created": <unix>, "data": { … } }` with the header `eroq-signature: t=<unix>,v1=<hex>`, where `v1` = HMAC-SHA256(secret, `<t>.<raw body>`). Rebuild that string from the raw body, compare in constant time, and refuse timestamps older than 5 minutes. One attempt with a 10-second budget: answer 2xx at once and do the work after.

A `flow.run.*` event carries `flow` (`id`, `name`), `run` (`id`, `status`, `source`, `credits`, `startedAt`, `finishedAt`, `inputs`) and `outputs`: each render (`kind`, `url` signed for 7 days, `creationId`) and each text Clap wrote, by step; `failure` (`step`, `nodeId`, `type`, `message`) on a failed run, `waiting` on a run that waits on a post. A `*.generation.*` event carries the job `id`, `model`, `content_type`, `result_url` (signed for 7 days, permanent with `store: true`) and `library_id` (or `error.code` when it failed, already refunded). A render finishes when it is read: poll its job, or put it in a flow, for the event to come at once.

## Operations

| Method | Path | What it does |
| --- | --- | --- |
| `GET` | `/v1/webhooks` | The workspace's endpoints and subscriptions, newest first — never their secrets. |
| `POST` | `/v1/webhooks` | Subscribe an endpoint (201). The signing secret is answered once: store it. |
| `DELETE` | `/v1/webhooks/{id}` | Unsubscribe: no event reaches that URL again. Idempotent: an endpoint already gone answers `deleted: false`. |
| `GET` | `/v1/webhooks/samples` | Up to 3 recent events of a type, exactly as an endpoint would have received them, or one example when there is none yet. |

## GET /v1/webhooks

The workspace's endpoints and subscriptions, newest first — never their secrets.

### Example request

```bash
curl https://eroq.ai/v1/webhooks \
  -H "Authorization: Bearer $EROQ_API_KEY"
```

```javascript
const res = await fetch('https://eroq.ai/v1/webhooks', {
  headers: { Authorization: `Bearer ${process.env.EROQ_API_KEY}` },
})
console.log(await res.json())
```

```python
import os, requests

res = requests.get("https://eroq.ai/v1/webhooks", headers={"Authorization": f"Bearer {os.environ['EROQ_API_KEY']}"})
print(res.json())
```

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	req, _ := http.NewRequest("GET", "https://eroq.ai/v1/webhooks", nil)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("EROQ_API_KEY"))

	res, _ := http.DefaultClient.Do(req)
	defer res.Body.Close()
	out, _ := io.ReadAll(res.Body)
	fmt.Println(string(out))
}
```

### Response


```json
{
  "object": "list",
  "webhooks": [{
    "object": "webhook", "id": "c2f1…", "url": "https://hooks.zapier.com/hooks/standard/…",
    "events": ["flow.run.succeeded"], "flowId": "1e45…", "enabled": true,
    "lastDeliveryAt": "2026-10-09T18:06:02Z", "lastStatus": 200, "createdAt": "2026-10-09T18:02:11Z"
  }]
}
```

## POST /v1/webhooks

Subscribe an endpoint (201). The signing secret is answered once: store it.

### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string | yes | An https URL, not a private or internal host. |
| `events` | array | yes | One or more of `image.generation.succeeded`, `image.generation.failed`, `video.generation.succeeded`, `video.generation.failed`, `flow.run.succeeded`, `flow.run.failed`, `flow.run.waiting`. |
| `flow_id` | string | no | Only this flow's `flow.run.*` events. 404 `flow_not_found` for a flow outside the workspace. |

### Example request

```bash
curl https://eroq.ai/v1/webhooks \
  -H "Authorization: Bearer $EROQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://hooks.example.com/eroq",
  "events": [
    "flow.run.succeeded",
    "flow.run.failed"
  ],
  "flow_id": "FLOW_ID"
}'
```

```javascript
const res = await fetch('https://eroq.ai/v1/webhooks', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.EROQ_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "url": "https://hooks.example.com/eroq",
    "events": [
      "flow.run.succeeded",
      "flow.run.failed"
    ],
    "flow_id": "FLOW_ID"
  }),
})
console.log(await res.json())
```

```python
import os, requests

res = requests.post(
    "https://eroq.ai/v1/webhooks",
    headers={"Authorization": f"Bearer {os.environ['EROQ_API_KEY']}"},
    json={
        "url": "https://hooks.example.com/eroq",
        "events": [
            "flow.run.succeeded",
            "flow.run.failed"
        ],
        "flow_id": "FLOW_ID"
    },
)
print(res.json())
```

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
)

func main() {
	body := strings.NewReader(`{
  "url": "https://hooks.example.com/eroq",
  "events": [
    "flow.run.succeeded",
    "flow.run.failed"
  ],
  "flow_id": "FLOW_ID"
}`)

	req, _ := http.NewRequest("POST", "https://eroq.ai/v1/webhooks", body)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("EROQ_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)
	defer res.Body.Close()
	out, _ := io.ReadAll(res.Body)
	fmt.Println(string(out))
}
```

### Response


```json
{
  "object": "webhook", "id": "c2f1…", "url": "https://hooks.example.com/eroq",
  "events": ["flow.run.succeeded", "flow.run.failed"], "flowId": "1e45…",
  "enabled": true, "lastDeliveryAt": null, "lastStatus": null, "createdAt": "2026-10-09T18:02:11Z",
  "secret": "whsec_…"
}
```

## DELETE /v1/webhooks/{id}

Unsubscribe: no event reaches that URL again. Idempotent: an endpoint already gone answers `deleted: false`.

### Example request

```bash
curl -X DELETE https://eroq.ai/v1/webhooks/WEBHOOK_ID \
  -H "Authorization: Bearer $EROQ_API_KEY"
```

```javascript
const res = await fetch('https://eroq.ai/v1/webhooks/WEBHOOK_ID', {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${process.env.EROQ_API_KEY}` },
})
console.log(await res.json())
```

```python
import os, requests

res = requests.delete("https://eroq.ai/v1/webhooks/WEBHOOK_ID", headers={"Authorization": f"Bearer {os.environ['EROQ_API_KEY']}"})
print(res.json())
```

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	req, _ := http.NewRequest("DELETE", "https://eroq.ai/v1/webhooks/WEBHOOK_ID", nil)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("EROQ_API_KEY"))

	res, _ := http.DefaultClient.Do(req)
	defer res.Body.Close()
	out, _ := io.ReadAll(res.Body)
	fmt.Println(string(out))
}
```

### Response


```json
{ "ok": true, "id": "c2f1…", "deleted": true }
```

## GET /v1/webhooks/samples

Up to 3 recent events of a type, exactly as an endpoint would have received them, or one example when there is none yet.

### Query parameters

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `event` | string | yes | The event type. |
| `flow_id` | string | no | For `flow.run.*`: one flow's runs only. |

### Example request

```bash
curl "https://eroq.ai/v1/webhooks/samples?event=value&flow_id=flow.run.*" \
  -H "Authorization: Bearer $EROQ_API_KEY"
```

```javascript
const res = await fetch('https://eroq.ai/v1/webhooks/samples?event=value&flow_id=flow.run.*', {
  headers: { Authorization: `Bearer ${process.env.EROQ_API_KEY}` },
})
console.log(await res.json())
```

```python
import os, requests

res = requests.get("https://eroq.ai/v1/webhooks/samples?event=value&flow_id=flow.run.*", headers={"Authorization": f"Bearer {os.environ['EROQ_API_KEY']}"})
print(res.json())
```

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	req, _ := http.NewRequest("GET", "https://eroq.ai/v1/webhooks/samples?event=value&flow_id=flow.run.*", nil)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("EROQ_API_KEY"))

	res, _ := http.DefaultClient.Do(req)
	defer res.Body.Close()
	out, _ := io.ReadAll(res.Body)
	fmt.Println(string(out))
}
```

### Response


```json
{
  "object": "list",
  "events": [{
    "id": "evt_sample_e285…", "type": "flow.run.succeeded", "created": 1760033161,
    "data": {
      "flow": { "id": "1e45…", "name": "A clip for every webhook" },
      "run": { "id": "e285…", "status": "done", "source": "webhook", "credits": 45, "startedAt": "2026-10-09T18:04:00Z", "finishedAt": "2026-10-09T18:06:01Z", "inputs": { "prompt": "…" } },
      "outputs": [
        { "step": 2, "nodeId": "n2", "type": "clap.write", "kind": "text", "text": "A barista slides a latte across the counter…" },
        { "step": 3, "nodeId": "n3", "type": "generate.video", "kind": "video", "creationId": "4a17…", "url": "https://cdn.eroq.ai/…/clip.mp4?token=…" }
      ]
    }
  }]
}
```

---
Canonical: https://eroq.ai/docs/webhooks · Index for agents: https://eroq.ai/llms.txt · OpenAPI: https://eroq.ai/openapi.json
