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

# Webhooks from your systems

> CRM, ERP and help desk come in through the generic webhook, with a versioned mapping and authentication.

Your CRM, ERP, help desk, billing and order systems already emit an event whenever something changes: an order is created, a bill is disputed, a ticket reopens, a payment fails. Niadra receives those events through one generic webhook, keeps the raw payload and applies a versioned mapping that pulls out the type, the object, the customer's id in that system, the fields and the time. No language model reads a system event, and you write no code in the system that sends it.

This guide connects an ERP so that `invoice.credited` for invoice 0823 lands on Marina Souza's memory at 2:06 pm, next to the action the billing agent recorded.

## How a system event becomes memory

1. The ERP posts its own JSON to `POST /v1/ingest/webhook/{source_id}`.
2. Niadra authenticates the request with the method declared for that source and stores the raw payload before answering.
3. The versioned mapping of the source produces the event: `canonical_type`, the object, the `system_id` of the customer, the fields and `occurred_at`.
4. The object gets its timeline and a derived state, with `as_of` and a reference to the source record. Open items tied to it may close.
5. The live layer has it in under a second; the recompiled packs in under ten.

The official value stays in the ERP. The memory keeps what the agents need to remember and points to the source.

## Steps

### 1. Create a source for the system

Each system is a source, with its own purpose and audience. Create it in the Console or through the [control API](/en/api/control/sources-create). Note the `source_id`: it is the last segment of the webhook address.

```text theme={null}
https://acme-prod.us-east-1.api.niadra.com/v1/ingest/webhook/0192f7b0-3c2d-7e41-9a55-6b1d0e8f2a13
```

### 2. Choose how the system authenticates

Authentication is mandatory and declared per source in the mapping. Without it, anyone who learned the address could inject a fake history, an action that closes a promise or a paid invoice.

| Method      | How it works                                                                                                                                  | When to use                         |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
| `hmac`      | The system signs the body; you declare the header, the algorithm, the signed text and the secret. Timestamps older than 5 minutes are refused | The system supports signed webhooks |
| `bearer`    | A fixed token in `Authorization`                                                                                                              | The system lets you set a header    |
| `url_token` | A secret token in the address                                                                                                                 | The system only takes a URL         |
| `mtls`      | Client certificate                                                                                                                            | The system supports mutual TLS      |

The secret is written straight into the vault of your cell and never shown again. A request that fails authentication gets 401 and is counted in the coverage of the source, so a misconfigured system shows up before anyone misses its events. When the sending system asks for a verification challenge by `GET` before it starts posting, Niadra answers it.

### 3. Write the mapping

A mapping turns the payload your system already sends into events, with expressions in the style of JMESPath. This is the ERP payload:

```json ERP payload theme={null}
{
  "event": "invoice.credited",
  "invoice": "0823",
  "customer_id": "48213",
  "amount": 40.00,
  "currency": "USD",
  "at": "2026-09-22T17:06:21Z"
}
```

And this is a mapping for it:

```yaml Mapping theme={null}
version: 3
auth:
  method: hmac
  header: X-ERP-Signature
  algorithm: sha256
  signed: "{timestamp}.{body}"
  tolerance_seconds: 300
types:
  - when: "event == 'invoice.credited'"
    canonical_type: invoice.credited
    object: { type: invoice, namespace: erp, id: "invoice" }
    subject: { type: system_id, scope: crm, value: "customer_id" }
    occurred_at: "at"
    fields:
      amount: "amount"
      currency: "currency"
  - when: "event == 'invoice.disputed'"
    canonical_type: invoice.disputed
    object: { type: invoice, namespace: erp, id: "invoice" }
    subject: { type: system_id, scope: crm, value: "customer_id" }
    occurred_at: "at"
```

Only mapped types get in. That filter at the edge protects the memory from the volume of an ERP. An unmapped type is kept for 7 days in cold storage, so you can map it later and replay it, and it never enters the memory. Because the raw payload is always kept, a better mapping can be applied to events that already arrived.

<Tip>
  The configuration assistant writes the first version for you. Ask for it with [`POST /v1/assist`](/en/api/assist) (`task: "webhook_mapping"` and the `source_id`, as a person with the `integration` role): it proposes the mapping, validates it against the sample events of your space and lists in `problems` what stayed unmapped. The proposal becomes a diff in the control API, and a person approves it. Follow the run with [`GET /v1/assist/{run_id}`](/en/api/assist-run) and list past runs with [`GET /v1/assist`](/en/api/assist-runs).
</Tip>

### 4. Propose the mapping as a versioned change

Configuration is never written in place. A mapping change is a diff in the control API, with the whole new `document` of the type and a `reason`, approved by a person; the cell receives it through a signed snapshot. [`GET /v1/config/types`](/en/api/control/config-types) lists the types and the roles that may change each one, and [`GET /v1/config/mappings`](/en/api/control/config-document) returns the current document to start from. Every version stays in the [history](/en/api/control/config-history) and can be [rolled back](/en/api/control/config-diff-rollback).

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://control.api.niadra.com/v1/config/diffs" \
    -H "Authorization: Bearer $NIADRA_CONTROL_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "space_id": "0192f6e1-8b7a-7c20-b3d4-2a9e5f1c6d08",
      "type": "mappings",
      "document": { "sources": { "0192f7b0-3c2d-7e41-9a55-6b1d0e8f2a13": { "version": 3, "types": ["invoice.credited", "invoice.disputed"] } } },
      "reason": "Map ERP credits and disputes"
    }'
  ```
</CodeGroup>

See [Propose a configuration change](/en/api/control/config-diffs) and [Approve a change](/en/api/control/config-diff-approve).

### 5. Point the system at the address

Configure the webhook in your ERP with the address from step 1 and the secret from step 2. From then on, every request answers with the same shape as a batch: `accepted`, `duplicates` and `errors` per event, with 200 when everything went in and 207 when something was rejected:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://acme-prod.us-east-1.api.niadra.com/v1/ingest/webhook/0192f7b0-3c2d-7e41-9a55-6b1d0e8f2a13" \
    -H "X-ERP-Signature: t=1758560781,v1=5f1c9e..." \
    -H "Content-Type: application/json" \
    -d '{ "event": "invoice.credited", "invoice": "0823", "customer_id": "48213", "amount": 40.00, "currency": "USD", "at": "2026-09-22T17:06:21Z" }'
  ```
</CodeGroup>

```json Response theme={null}
{ "accepted": 1, "duplicates": 0, "errors": [] }
```

### 6. Check what arrived

Read the object to see its derived state and timeline. The credit shows the ERP event and the billing agent's action as one record: the action was `declared` at 2:06 pm and became `confirmed` when `invoice.credited` arrived.

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://acme-prod.us-east-1.api.niadra.com/v1/objects/invoice/erp/0823/timeline" \
    -H "Authorization: Bearer $NIADRA_API_KEY"
  ```
</CodeGroup>

## Free text inside a system

A ticket description or an e-mail body is not a structured event. Send it as a `message` on its own channel (`ticket`, `email`) and it goes through the normal extraction, like a conversation. Free text from tickets and e-mails is billed like a conversation; mapped system events are not billed.

## Systems without webhooks

For systems that only export files, turn the export into JSONL with one batch item per line and send it to [`POST /v1/ingest/files`](/en/api/ingest-files); each line is validated like an item of `POST /v1/batch`. To seed identity from a CRM export, send it as CSV. For systems your team already integrates in code, send system events through the SDK or [`POST /v1/batch`](/en/api/batch) with `kind: "system_event"` and `canonical_type`.

## Next steps

<CardGroup cols={2}>
  <Card title="Systems, objects and actions" href="/en/concepts/systems">
    objects, derived state and how actions close open items.
  </Card>

  <Card title="Internal agents" href="/en/guides/internal-agents">
    the billing agent that recorded the credit.
  </Card>

  <Card title="Receive a system webhook" href="/en/api/ingest-webhook">
    the endpoint reference.
  </Card>

  <Card title="Triggers and webhooks" href="/en/concepts/triggers-and-webhooks">
    the other direction, from Niadra to your systems.
  </Card>
</CardGroup>
