# Send data into Hodoflow with a webhook

> Create a service account, point a workflow's webhook trigger at it, and post a payload that starts a real run.

This is the complete inbound path: an identity for the caller, a workflow with
a URL, and one authenticated request that produces a run you can open.

## What you will build

- A service account called **Webhook ingest** holding only the `webhooks`
  scope.
- A workflow called **Order intake** with a webhook trigger that uses it.
- One accepted delivery, visible as a completed run in Monitor.

## Before you start

- Creating a service account is limited to owners and admins. If **New Service
  Account** is missing, that is why.
- Nothing external needs configuring. You will send the request yourself.

## 1. Create the service account

Go to **Settings → Service Accounts** and choose **New Service Account**.

| Field | Value |
|---|---|
| Name | `Webhook ingest` |
| Scopes | `webhooks` only |
| Run as | Yourself, or whoever should own this integration long term |

**Run as** matters more than it looks. A service account has no permissions of
its own — it borrows a member's. Every request authenticated with it executes
as that person, so deactivating them changes what the credential can reach.

**Checkpoint:** the service account appears in the list as **Active**.

## 2. Copy the client ID and secret

Open the service account and find **Client Credentials**. You get a client ID
beginning `cid_` and a client secret beginning `csec_`, each with a **Copy**
button.

Unlike most secrets, these stay retrievable — you can come back to this page
whenever an integration needs reconfiguring. Access is limited to owners and
admins.

**Checkpoint:** you have both values to hand.

## 3. Build the workflow

Go to **Workflows → New Workflow**. In the trigger wizard choose **Webhook**,
then select **Webhook ingest** in the **Service account** picker. Press
**Done**.

Add one step so the workflow has something to do — an **HTTP Request** to any
endpoint you like is enough for this walk-through — and connect the trigger to
it.

Name the workflow **Order intake** and press **Save**.

**Checkpoint:** the trigger inspector now shows a read-only **Webhook URL**.
Before saving it said *Save workflow to generate webhook token* — the token is
minted on save.

## 4. Activate it

Press **Activate**. Only active triggers are served: until you do this, the
URL answers `404`.

**Checkpoint:** the workflow reads **Active**.

## 5. Exchange the credentials for a token

The client ID and secret are not what you send with the request. Exchange them
for a short-lived token first:

```bash
curl -X POST https://app.example.com/api/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "cid_...",
    "client_secret": "csec_..."
  }'
```

The response is the token, its type, and its lifetime in seconds:

```json
{"access_token": "eyJ...", "token_type": "bearer", "expires_in": 900}
```

Fifteen minutes. A long-lived integration exchanges again rather than caching
one forever.

**Checkpoint:** you have an `access_token`.

## 6. Post a payload

Copy the **Webhook URL** from the trigger inspector and post to it:

```bash
curl -X POST https://app.example.com/api/v1/webhooks/YOUR_TRIGGER_TOKEN \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"order_id": "A-123", "total": 250}'
```

An accepted delivery returns `202 Accepted` with the id of the run it started:

```json
{"status": "accepted", "workflow_run_id": "0198f2c1-..."}
```

**Checkpoint:** you got `202` and a `workflow_run_id`.

## 7. Teach the workflow the payload's shape

Steps below the trigger can only offer fields they know about. In the trigger
inspector, open **Output Schema** and press **Refresh Schema**.

Hodoflow shows a **Test URL (no data saved)** and waits. Post the same payload
to *that* URL, and the field names appear as *N fields found*. Nothing is
stored and no run is created.

`order_id` and `total` are now available to every step below, as
`input.order_id` and `input.total`.

**Checkpoint:** *2 fields found*.

## Verify the result

You are done when all of these are true:

- **Settings → Service Accounts** lists **Webhook ingest** as **Active** with
  only the `webhooks` scope.
- **Workflows** lists **Order intake** as **Active**.
- Your `curl` returned `202` with a `workflow_run_id`.
- **Workflows → Monitor** holds a run whose **Source** is that trigger.
- The trigger's **Output Schema** lists the fields you sent.

## Common problems

**`401` with `{"error": "invalid_client"}` at the token step.** Every
credential problem returns this same response — wrong secret, unknown client,
deactivated account. That is deliberate. Check the service account's status in
Hodoflow rather than reading the error.

**`403` when posting to the webhook.** The token authenticated but does not
carry the `webhooks` scope. Add the scope, then exchange again — a scope change
takes effect on the next exchange, not instantly.

**`404` when posting to the webhook.** The workflow is not active, or the URL
is from a version that is no longer live. Activate it and re-copy the URL.

**`202` with `{"status": "duplicate"}`.** Hodoflow matched your request to a
delivery it already accepted, so it did not start a second run. Change the
payload, or send a different `Idempotency-Key` header.

**The service account picker in the wizard was empty.** Only active service
accounts holding `webhooks` are listed. Check both.

**`429`.** The cap is 100 requests per minute per trigger token.

## Next steps

- [Receive webhooks](/help/workflows/webhook-triggers) — redeliveries, limits,
  and verifying the sender.
- [Persist workflow data](/help/data-dashboards/persisting-data) — keep what
  arrives instead of discarding it.
- [Naming, reuse, rotation, and deletion](/help/credentials-connections/lifecycle)
  — before this credential is in production.
