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

# Custom Events API

> Full request and response contract for the Custom Events webhook endpoint: methods, auth, payload, query params, errors, and limits.

This page is the dispatcher's wire-level reference for the Custom Events webhook. For setup walkthroughs and recipes, see [Custom Events (Integrations)](/docs/integrations/custom-events). For workflow-side filtering and run access, see [Custom Events in workflows](/docs/workflows/custom-events).

## Endpoint

```
POST https://workflow-dispatcher.overcut.ai/trigger-event/<token>
GET  https://workflow-dispatcher.overcut.ai/trigger-event/<token>
```

`<token>` is an opaque, 32-byte random string. It identifies the Custom Event and, even on its own, acts as the baseline credential: anyone who has the URL can fire the event unless an additional auth header is required.

The exact URL for your event is shown on the event detail page in **Custom Events → (event) → Webhook URL**. Reveal it with **Show** and copy it with **Copy**, or click **Example** to grab a complete curl snippet.

| Method | Body             | Query params              |
| ------ | ---------------- | ------------------------- |
| `POST` | JSON, up to 1 MB | Optional                  |
| `GET`  | (none)           | All data via query params |

A successful call returns **`202 Accepted`** with `{"success":true,"eventId":"<id>"}` immediately. Trigger evaluation and workflow dispatch happen asynchronously: a 202 means "your event was accepted," not "a workflow has finished."

## Authentication

The auth mode is set on the event itself, not per request.

<ParamField path="None" type="auth mode">
  No auth header required. URL token is the only credential. Suitable for low-risk internal sources or prototypes.
</ParamField>

<ParamField path="Bearer token (default)" type="auth mode">
  Standard `Authorization: Bearer <secret>` header. The `Bearer` prefix is matched case-insensitively. The secret is whatever non-whitespace follows.

  ```
  Authorization: Bearer 9bff…c11d
  ```
</ParamField>

<ParamField path="Custom header" type="auth mode">
  Any header name chosen at create time. The header name is matched case-insensitively; the value is compared after trimming whitespace.

  ```
  X-API-Key: 9bff…c11d
  ```
</ParamField>

Secrets are stored as SHA-256 hashes. The plaintext is shown one time only, in the **New secret** panel at create or rotate time. There is no API or UI to read it back. If lost, [rotate](/docs/integrations/custom-events#rotate-the-url-token-or-secret).

<Note>
  Custom Events do not verify HMAC body signatures. TLS protects the transport and the Bearer / custom header value protects authenticity. If you need provider-native signatures (`X-Hub-Signature`, `Stripe-Signature`, etc.), use the [native integrations](/docs/integrations/overview) instead.
</Note>

## Payload

The request body is arbitrary JSON. Overcut does not validate its shape: workflow trigger conditions read whatever the sender sends.

```bash theme={null}
curl -X POST 'https://workflow-dispatcher.overcut.ai/trigger-event/<token>' \
  -H 'Authorization: Bearer <secret>' \
  -H 'Content-Type: application/json' \
  -d '{
    "environment": "production",
    "status": "success",
    "service": { "name": "api" }
  }'
```

### Size limit

The body is capped at **1 MB**. Larger requests are rejected with `413 payload_too_large`.

### Query-string merge

Every `key=value` pair on the URL (other than the reserved [`projectId`](#projectid) and [`lockKey`](#lockkey)) is merged into the payload as a top-level string field. This lets GET-only systems send data:

```
GET .../trigger-event/<token>?environment=production&status=success
```

Body values win on conflict: if both the body and the query string set `environment`, the body value is kept.

### Well-known meta fields

Four payload keys are dual-use: they shape how the workflow run appears in Overcut **and** remain available to trigger conditions under `payload.*`.

| Key            | Type   | Sets                                 | Default if absent                              |
| -------------- | ------ | ------------------------------------ | ---------------------------------------------- |
| `objectName`   | string | The run's label in the runs list     | the event's slug (e.g. `deployment_completed`) |
| `objectNumber` | string | The run's object-number badge        | empty                                          |
| `objectUrl`    | string | The click-through link from the run  | empty                                          |
| `actor`        | string | The run's actor (displayed as a Bot) | none                                           |

Accepted in both body and query string. Body wins on conflict. Scalar values are coerced to strings (a JSON number `1234` becomes `"1234"`). Non-scalar shapes (arrays, objects) are ignored for lifting but still pass through under `payload`.

## Query parameters

### `projectId`

<ParamField query="projectId" type="string" required={false}>
  Narrows the call to a single project. The project must exist in the event's workspace and be in the event's allowlist (or the event must be scoped to **All projects**). Reserved: never merged into `payload`.

  Omit to fan out: every project that allows the event receives an independent firing, evaluated against its own workflows.
</ParamField>

### `lockKey`

<ParamField query="lockKey" type="string" required={false}>
  Opt-in serialization scope. Requests sharing the same `(event, lockKey)` pair run one at a time, and within a short dedupe window collapse into a single queued execution per workflow. Reserved: never merged into `payload`.

  Charset and length: `^[A-Za-z0-9_.\-:]{1,128}$`. Invalid values return `400 lockkey_invalid`.

  When `projectId` is also present, the lock scope folds in the project: two calls to **different** projects with the same `lockKey` run in parallel.
</ParamField>

### Lock-key formula

The full key the dispatcher uses internally:

| Call shape                    | Lock key                                                       |
| ----------------------------- | -------------------------------------------------------------- |
| No `lockKey`                  | `custom:<eventId>:<requestId>` (unique per call, never merges) |
| `?lockKey=X`                  | `custom:<eventId>:X`                                           |
| `?projectId=P&lockKey=X`      | `custom:<eventId>:P:X`                                         |
| `?projectId=P` (no `lockKey`) | `custom:<eventId>:P:<requestId>`                               |

The same key formula governs both serialization (one execution at a time per key) and dedupe-window merging (repeats collapse into one queued run per workflow).

## Responses

### Success

```
HTTP/1.1 202 Accepted
Content-Type: application/json

{ "success": true, "eventId": "ckh3z9q8l0001abcd1234efgh" }
```

The `eventId` is the dispatcher's record of the inbound webhook (one row per accepted request). It is useful for correlating against logs but is not the workflow run ID: a single inbound event can produce zero, one, or many workflow runs depending on which workflows match.

### Error envelope

Every error returns JSON with a stable `error` code and a human-readable `message`:

```
HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "error": "project_not_allowed",
  "message": "Project abc123 is not in this event's allowlist."
}
```

### Error codes

| Code                  | HTTP | Cause                                                                                       |
| --------------------- | ---- | ------------------------------------------------------------------------------------------- |
| `token_invalid`       | 404  | Unknown or revoked URL token. Also returned for deleted events.                             |
| `auth_missing`        | 401  | The required `Authorization` or custom header was not sent.                                 |
| `auth_invalid`        | 401  | The secret did not match.                                                                   |
| `project_not_found`   | 404  | `?projectId=` references a project that does not exist or belongs to a different workspace. |
| `project_not_allowed` | 403  | `?projectId=` references a project that is not in the event's allowlist.                    |
| `payload_too_large`   | 413  | Request body exceeded 1 MB.                                                                 |
| `lockkey_invalid`     | 400  | `?lockKey=` violates the charset or length rule.                                            |
| `internal_error`      | 500  | Unexpected dispatcher failure. Safe to retry.                                               |

The dispatcher does **not** auto-retry: how to handle a non-2xx is up to the sending system's webhook framework.

## Operational notes

* **No idempotency key.** There is no `Idempotency-Key` header today. To deduplicate retries from a flaky sender, have the sender pass a stable `?lockKey=` so repeats within the dedupe window collapse onto the same execution.
* **No per-event rate limits.** Standard dispatcher abuse protection applies. There is no `Retry-After` contract.
* **Fan-out.** One inbound webhook produces one dispatcher event row and (after fan-out and matching) zero or more workflow runs.
* **Deleted events.** Deleted events return `token_invalid` (404). Past workflow runs for a deleted event remain in the history and continue to show the event's slug.

## Related

<CardGroup cols={2}>
  <Card title="Custom Events (Integrations)" icon="webhook" href="/docs/integrations/custom-events">
    Setup, auth modes, rotation, project scoping, and four ready-to-paste recipes.
  </Card>

  <Card title="Custom Events in workflows" icon="bolt" href="/docs/workflows/custom-events">
    Pick an event in the trigger picker, filter on `payload.*`, and access event data in steps.
  </Card>

  <Card title="Event Context Reference" icon="brackets-curly" href="/docs/reference/event-context#custom-event-properties">
    The `trigger.*` and `trigger.payload.*` template variables exposed to downstream steps.
  </Card>

  <Card title="Advanced Trigger Execution" icon="gears" href="/docs/reference/trigger-execution">
    Operator list, event merging, priority queuing, and the Custom Event lock-key formula.
  </Card>
</CardGroup>
