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

# Webhooks

> Start a workflow from any outside app or website by sending data to a Custom Webhook URL.

A **webhook** is a private web address that another app can send data to. Think of it as a letterbox for your workflow: when a website form, a payment app or your own code posts a message to the address, the workflow runs and can use everything in that message. In Connie, this is the **Custom Webhook** trigger.

**Where:** open a workflow → **Build** → trigger card → **Event** → **Webhooks** tab → **Custom Webhook** · `https://connie.ai/<workspace-id>/workflows/<workflow-id>`

## Set up a webhook trigger

<Steps>
  <Step title="Add the Custom Webhook trigger">
    Create a workflow with **New** → **New workflow** → **Event**, then choose **Custom Webhook** on the **Webhooks** tab. (In an existing workflow: trigger card **⋯** → **Change**.)
  </Step>

  <Step title="Copy the Webhook URL">
    The trigger panel shows your **Webhook URL** with a copy button. It looks like `https://connie.ai/api/webhooks/` followed by a long ID. Each Custom Webhook trigger has its own URL.
  </Step>

  <Step title="Send a test request">
    Open the trigger's **Test** panel and click **Run Test** ("Send a POST request to the webhook URL to capture test payloads."). Then send a request from your app, or copy the **Example request** shown in the panel with **Copy command** and run it.
  </Step>

  <Step title="Pick the test event">
    The captured request appears in the list. Click **Use as test event for Run Once**. Its fields now appear under **Custom Webhook** in the [variable picker](/automations/workflows/variables).
  </Step>

  <Step title="Build the steps, then set it live">
    Add your steps, then **Run** → **Set live**. Until the workflow is live, requests are saved as test events but don't start runs.
  </Step>
</Steps>

## Send data to the webhook

* **Method:** `POST`.
* **Body:** valid JSON. Send the header `Content-Type: application/json`. A body that isn't valid JSON (form-encoded, XML, empty) is refused with `400 {"error":"Invalid JSON body"}`.
* **Address:** always use the `https://` URL exactly as copied.
* **Authentication:** none needed. The URL itself is the key (see [Security](#security)).

```bash theme={null}
curl -X POST "https://connie.ai/api/webhooks/YOUR-WEBHOOK-ID" \
  -H "Content-Type: application/json" \
  -d '{"email": "ana@acme.com", "name": "Ana Lopez", "company": {"name": "Acme", "size": 120}}'
```

Each request starts one run, and each run adds a row to the **Runs** tab.

## Use the payload in your steps

Every key in the JSON you send becomes a [variable](/automations/workflows/variables) under the **Custom Webhook** group. With the example above:

| In the payload | Variable |
| - | - |
| `"email"` | `{{Custom Webhook.email}}` |
| `"name"` | `{{Custom Webhook.name}}` |
| `"company"` → `"name"` | `{{Custom Webhook.company.name}}` |

Insert them from the picker rather than typing them. Fields only appear in the picker after you've captured a test event, so capture one with the same shape your app will really send.

**Example:** a website "Book a demo" form posts name, email and company. The workflow uses **Search records in a list or object** to look up `{{Custom Webhook.email}}`, **Create or update a record in a list** to add the lead to your Inbound list, and **Send Slack Message** to tell your team.

## Responses

| Status | Body | Meaning |
| - | - | - |
| `200` | `{"success": true, "workflowId": "…", "eventId": "…", "triggered": 1, "skipped": 0}` | Received and started. |
| `200` | `{"success": true, "message": "Webhook is inactive. Test payload saved.", "workflowId": "…", "triggered": 0}` | The workflow is in **draft**. The request was saved as a test event, but nothing ran. |
| `400` | `{"error": "Invalid JSON body"}` | The body isn't valid JSON. |
| `402` | `{"code": "INSUFFICIENT_CREDITS", "error": "…", "message": "…"}` | Not enough credits. The run didn't start. The message is "Your credit balance is depleted. Upgrade your plan to continue running flows." when the balance is zero or below, or "Insufficient credits to run this flow. Upgrade your plan to continue." when it's too low for this workflow's steps. |
| `404` | `{"error": "Webhook not found"}` | The URL is wrong or no longer exists. |
| `410` | `{"error": "Workspace has been deleted"}` | The workspace has been deleted. |

If **Custom Response** is on, your custom response replaces both `200` bodies above (including the draft one). Error responses are never replaced.

A `GET` request to the URL returns a short description of how to call it; it doesn't start a run.

### Send your own response

Turn on **Custom Response** ("Return a custom JSON body to the webhook caller"). The default is `{"status":"ok"}`. You can echo values from the incoming request, for example `{{body.email}}` ("Use `{{body.field}}` to echo payload values"). If the text isn't JSON, it's returned as plain text, which is useful for apps (such as Slack) that verify a webhook by expecting a value echoed back.

If any value in your custom response can't be filled from the request, the default response is sent instead.

## Duplicate requests

Many apps retry a webhook if they don't hear back quickly. To stop the same event running twice, have the sending app include one of these headers with a unique value per event: `Idempotency-Key`, `X-Idempotency-Key`, `X-Webhook-Id`, `X-Delivery-Id`, `X-Event-Id`, `X-Message-Id` or `X-Request-Id`. A repeat with the same value is skipped (`"skipped": 1`).

Without one of those headers, an identical request is only skipped while the previous identical run is still running. After that, it runs again.

## Security

<Warning>
  **Anyone who has the URL can start your workflow.** Connie doesn't check signatures, and signature headers from the sending app are ignored. The panel reminds you: "Anyone with this URL can trigger this flow. Keep it private." Don't put the URL in public web pages or client-side code, and share it only with the systems that need it.
</Warning>

* Add a **Router** or **Filter Records by condition** step early in the workflow to ignore requests that don't look right (for example, missing a field only your app sends).
* **Duplicating a workflow** gives the copy a new webhook URL. The original keeps its URL.

## Good to know

* **Every request is saved as a test event, even in draft.** That's how capturing a test event works.
* **Running hours** in [Workflow settings](/automations/workflows/test-and-run#workflow-settings) apply to webhook runs. Outside those hours, requests wait (or are dropped if **Queue triggers outside of window** is off).
* **Credits** are checked before every webhook run. Each run is charged for the paid steps it uses. See [Credits](/automations/workflows/test-and-run#credits).
* The webhook is the only way to start a workflow from outside Connie, apart from [MCP](/mcp/overview). See [Developers](/developers/overview).

## Troubleshooting

<AccordionGroup>
  <Accordion title="Webhook is inactive. Test payload saved.">
    **Why:** The workflow is in draft, so the request was stored as a test event only.
    **Fix:** **Run** → **Set live**.
  </Accordion>

  <Accordion title="400 Invalid JSON body">
    **Why:** The body isn't JSON (for example, it's form-encoded or XML).
    **Fix:** Send a JSON body with the header `Content-Type: application/json`.
  </Accordion>

  <Accordion title="404 Webhook not found">
    **Why:** The URL is mistyped, or the Custom Webhook trigger it belonged to has been removed. A duplicated workflow has its own, different URL.
    **Fix:** Copy the **Webhook URL** again from the trigger panel and update the sending app.
  </Accordion>

  <Accordion title="402 Your credit balance is depleted / Insufficient credits to run this flow">
    **Why:** The workspace is out of credits, or its balance is below the price of this workflow's paid steps, so the run didn't start.
    **Fix:** Add credits. See [Top-ups](/billing/top-ups).
  </Accordion>

  <Accordion title="The request returned 200 but nothing ran">
    **Why:** Either the workflow is in draft (the response says Webhook is inactive, or your custom response hides that), or the request was a duplicate (the response shows skipped 1).
    **Fix:** Set the workflow live. For duplicates, make sure each event has its own idempotency value.
  </Accordion>

  <Accordion title="Some events never ran">
    **Why:** The sending app reused the same idempotency or delivery ID, or the events arrived outside running hours with queueing off.
    **Fix:** Use a unique ID per event. Check **Running hours** in Workflow settings.
  </Accordion>

  <Accordion title="The same request ran twice">
    **Why:** Without an idempotency header, identical requests are treated as separate events once the first run has finished.
    **Fix:** Have the sending app include an `Idempotency-Key` header.
  </Accordion>

  <Accordion title="My custom response came back as the default">
    **Why:** A value in the custom response couldn't be filled from this request.
    **Fix:** Only echo keys that are present in every request.
  </Accordion>

  <Accordion title="The payload fields don't show in the variable picker">
    **Why:** No test event has been captured yet.
    **Fix:** Open the trigger → **Test** → **Run Test**, send a request, then choose **Use as test event for Run Once**.
  </Accordion>

  <Accordion title="My app says the webhook redirected">
    **Why:** The URL was entered with `http://`.
    **Fix:** Use the `https://` URL exactly as copied.
  </Accordion>
</AccordionGroup>

## Related

* [Triggers](/automations/workflows/triggers) — all trigger types
* [Variables](/automations/workflows/variables) — using payload fields in steps
* [Test and run](/automations/workflows/test-and-run) — set live and read results
* [Developers](/developers/overview) — other ways to build on Connie


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.