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

# Webhooks

> Start a workflow by calling its URL with a bearer token.

A webhook trigger gives a [workflow](/learn/workflows/overview) its own URL. Any system that can send an HTTP request, such as your backend, a CI job, a form tool, or another SaaS product, can start the workflow by posting JSON to that URL with the trigger's bearer token. The request body becomes the workflow's input.

<Tip>
  Webhook triggers are the secure, preferred way to let an outside system start work in Major. Every call must carry the trigger's credential, and calls without it are rejected. [Webhook routes](/learn/apps/webhook-routes) on an app are public and unauthenticated, so use them only when a service must post raw payloads to your app and can't send a credential.
</Tip>

## Example requests

* "Give our backend a URL it can call with an order id to start the refund review workflow."
* "Add a webhook trigger to the lead-routing workflow that takes an email and a company name."
* "Let our CI pipeline start the release-notes workflow when a deploy finishes."

## How a webhook trigger is defined

A webhook trigger has an `id`, the type `webhook`, and an optional `label` and `input`. It has no `config`, and its URL and credential never appear in the definition.

```jsonc theme={null}
"triggers": [
  { "id": "refund_request", "type": "webhook", "label": "Refund request" }
]
```

## Getting the URL and credential

Each webhook trigger has one URL and one bearer token. The token is shown **once**, so copy both into the system that will call the workflow, such as its secrets manager, before you close the dialog.

<Steps>
  <Step title="Add the trigger">
    In the workflow editor, add a trigger and choose **Webhook**. Major creates the URL right away and shows the URL and token. Copy both, confirm you've saved the token, and click **Done**.

    If the trigger was added in JSON, through the [Platform Agent](/build/platform-agent), [MCP](/build/mcp), or the [CLI](/reference/cli/workflow), its URL is created when you publish. Open the workflow in the editor afterwards and click **View credential** on the trigger to generate and reveal the token.
  </Step>

  <Step title="Publish">
    The URL doesn't accept calls until the workflow is **published**. Before that, calls are rejected.
  </Step>

  <Step title="Call the URL">
    Send a `POST` with the token in the `Authorization` header.
  </Step>
</Steps>

Only people who can edit the workflow can view a credential. The token is never shown again, and Major never includes it in chat, MCP results, or the workflow file. If you lose it, remove the trigger, publish, and add it again. That creates a new URL and token.

## Calling the webhook

```bash theme={null}
curl -X POST "https://go-api.prod.major.build/webhook-triggers/<webhook-id>/events" \
  -H "Authorization: Bearer $MAJOR_WEBHOOK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "order_id": "ord_1042", "amount": 129.99 }'
```

* The body must be JSON, sent with `Content-Type: application/json`, and at most 1 MB.
* A successful call returns `200` after the run has been created.
* An unknown URL, a missing or wrong token, or an unpublished trigger all return `404`, so the response never reveals whether a trigger exists.
* A body that isn't valid JSON returns `400`, and a body over 1 MB returns `413`.

Use the exact URL shown in the editor. The `<webhook-id>` in it is generated by Major and is not the trigger's `id` in the definition. Each call starts one run of the published version, which appears in the workflow's run history.

## Shaping the input

By default, the JSON body becomes `trigger.input` as is, so the call above gives the workflow `trigger.input.order_id` and `trigger.input.amount`. The body must be a JSON object in this case.

To pick or rename fields, set `input` on the trigger. Each value is a JSON literal or a `{ "$expr": "..." }` CEL expression, and expressions can only read the body, as `event.json`:

```jsonc theme={null}
{
  "id": "new_lead",
  "type": "webhook",
  "label": "New lead from the CRM",
  "input": {
    "email": { "$expr": "event.json.contact.email" },
    "company": { "$expr": "coalesce(event.json.contact.company, 'Unknown')" },
    "source": "crm"
  }
}
```

If the workflow has an `input_schema`, the input is validated against it. When the projection fails or the input doesn't validate, the call still returns `200`, and the run is recorded as failed with the reason. See [State and input](/learn/workflows/state#workflow-input).

## Removing a webhook

Removing a published webhook trigger takes effect when you publish: the URL keeps working until then, and publishing retires it and revokes its token. Removing a trigger that was never published retires it immediately.

The credential acts on behalf of the workflow's publisher at the time it was created. If that person loses access to the workflow or leaves the organization, calls are rejected.

## Next steps

<CardGroup cols={2}>
  <Card title="Connector events" icon="plug" href="/learn/triggers/connector-events">
    Start a workflow from events in services you've connected.
  </Card>

  <Card title="State and input" icon="code" href="/learn/workflows/state">
    Validate and use the webhook's input.
  </Card>
</CardGroup>
