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

# Inbound events

> Send authenticated events from your systems into your organization's event log so Nexio automations can react to them.

Inbound events let a system you run tell Nexio that something happened: a record changed in your CRM, a deploy finished, a batch job completed. Each accepted request appends one [platform event](/events/platform-events) to your organization's log, where an [automation](/events/automations) can react to it. The route is `POST /api/v1/events/ingest/{source_key}`, and it is public API.

The route takes no API key. A request from a `nexio` or `github` **ingest source** is authenticated by an HMAC-SHA256 signature that covers the raw body, made with that source's signing secret. An ingest source is a registered sender for one organization. Its `source_key` in the URL alone decides which organization the event belongs to. The one exception is an `ams360_ons` source, which Nexio registers on request: AMS360 cannot sign a body, so that source authenticates by the authentication code AMS360 sends with each notification, and a missing or wrong code answers `401 invalid_credentials`.

## Get an ingest source

Nexio registers ingest sources for you; there is no self-serve API or portal page. Tell Nexio:

1. The kind of sender. Source kinds are a fixed set of three, defined in code:
   * `nexio`: your own code posts events in the Nexio envelope described below. Give Nexio the list of event types the source may send, for example `crm.account_updated`. The types are your own vocabulary: any name is accepted except a [reserved type](/events/platform-events#reserved-types).
   * `github`: a GitHub repository webhook that reports deployments. Give Nexio the repository (`owner/name`), the deployment environments to accept, and optionally the deployment statuses to accept (default `success`).
   * `ams360_ons`: the notification service of AMS360, a supported system type. It records `ams360.notification_received` events (see [Platform events](/events/platform-events#types-minted-by-verified-inbound-sources)). The rest of this page describes the `nexio` and `github` kinds.
2. A label, so you and Nexio can recognize the source later.

Nexio gives you back, once:

| Value          | Format                                 | Where it goes                                                                  |
| -------------- | -------------------------------------- | ------------------------------------------------------------------------------ |
| Source key     | `evsrc_` followed by 64 hex characters | In the URL: `https://api.usenexio.com/api/v1/events/ingest/<source_key>`       |
| Signing secret | `evsec_` followed by 64 hex characters | In your secret manager. Use the whole string, prefix included, as the HMAC key |

A source accepts no events until Nexio enables it. A request with a valid signature to a disabled source gets `403 ingest_source_disabled`.

## Send an event

### Envelope

The request body for a `nexio` source is a JSON object:

| Field         | Required | Meaning                                                                                          |
| ------------- | -------- | ------------------------------------------------------------------------------------------------ |
| `type`        | Yes      | The event type. Must be one of the types configured on the source, and not a reserved type.      |
| `subject`     | Yes      | What the event is about, for example `account/ACC-10442`. Must not be empty.                     |
| `body`        | Yes      | A JSON object with the IDs and changed fields. Keep it small: IDs and values, not whole records. |
| `occurred_at` | No       | RFC 3339 time the change happened. Defaults to the time Nexio received the request.              |
| `dedupe_key`  | No       | The event's identity within its type and subject. Defaults to the `X-Nexio-Delivery` value.      |

Nexio sets `org_id` from the source, `produced_by` to `ingest`, and `transition_cause` to `world_change`. A sender cannot set them. Unknown top-level fields are ignored.

### Headers

| Header              | Value                                                                               |
| ------------------- | ----------------------------------------------------------------------------------- |
| `X-Nexio-Delivery`  | Your unique ID for this request. Reuse it only when you retry the same bytes.       |
| `X-Nexio-Timestamp` | Current Unix time in seconds, decimal digits only.                                  |
| `X-Nexio-Signature` | `t=<timestamp>,v1=<hex HMAC-SHA256>`. The `t` value must equal `X-Nexio-Timestamp`. |

### Signature

This is the same scheme Nexio uses to sign [outbound webhooks](/api-reference/webhooks/overview#verify-each-delivery), applied in the other direction:

```text theme={null}
v1 = hex( HMAC-SHA256( key = signing_secret, message = timestamp + "." + raw_body ) )
```

1. Serialize the envelope to bytes once.
2. Compute the HMAC over the timestamp, a period, and exactly those bytes.
3. Send exactly those bytes as the body. Do not let your HTTP client re-serialize the JSON.

Nexio refuses a timestamp more than 300 seconds from its clock (`401 stale_timestamp`). Keep your clock synchronized.

<CodeGroup>
  ```bash curl theme={null}
  SOURCE_KEY="evsrc_example000000000000000000000000000000000000000000000000000000000"
  SECRET="evsec_example000000000000000000000000000000000000000000000000000000000"
  DELIVERY_ID="$(uuidgen | tr 'A-Z' 'a-z')"
  TS="$(date +%s)"
  BODY='{"type":"crm.account_updated","subject":"account/ACC-10442","body":{"account_id":"ACC-10442","changed_fields":["mailing_address","primary_contact"],"updated_by":"dana.ortiz"},"occurred_at":"2026-09-23T15:04:05Z"}'
  SIG="$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')"

  curl -X POST "https://api.usenexio.com/api/v1/events/ingest/$SOURCE_KEY" \
    -H "Content-Type: application/json" \
    -H "X-Nexio-Delivery: $DELIVERY_ID" \
    -H "X-Nexio-Timestamp: $TS" \
    -H "X-Nexio-Signature: t=$TS,v1=$SIG" \
    --data-binary "$BODY"
  ```

  ```python Python theme={null}
  import hashlib
  import hmac
  import json
  import time
  import uuid

  import requests

  SOURCE_KEY = "evsrc_example000000000000000000000000000000000000000000000000000000000"
  SECRET = "evsec_example000000000000000000000000000000000000000000000000000000000"

  envelope = {
      "type": "crm.account_updated",
      "subject": "account/ACC-10442",
      "body": {
          "account_id": "ACC-10442",
          "changed_fields": ["mailing_address", "primary_contact"],
          "updated_by": "dana.ortiz",
      },
      "occurred_at": "2026-09-23T15:04:05Z",
  }
  raw_body = json.dumps(envelope, separators=(",", ":")).encode("utf-8")
  delivery_id = str(uuid.uuid4())
  timestamp = str(int(time.time()))
  signature = hmac.new(
      SECRET.encode("utf-8"),
      timestamp.encode("ascii") + b"." + raw_body,
      hashlib.sha256,
  ).hexdigest()

  resp = requests.post(
      f"https://api.usenexio.com/api/v1/events/ingest/{SOURCE_KEY}",
      data=raw_body,  # the exact signed bytes, not json=
      headers={
          "Content-Type": "application/json",
          "X-Nexio-Delivery": delivery_id,
          "X-Nexio-Timestamp": timestamp,
          "X-Nexio-Signature": f"t={timestamp},v1={signature}",
      },
      timeout=30,
  )
  print(resp.status_code, resp.json())
  ```

  ```typescript TypeScript theme={null}
  import { createHmac, randomUUID } from "node:crypto";

  const SOURCE_KEY = "evsrc_example000000000000000000000000000000000000000000000000000000000";
  const SECRET = "evsec_example000000000000000000000000000000000000000000000000000000000";

  const envelope = {
    type: "crm.account_updated",
    subject: "account/ACC-10442",
    body: {
      account_id: "ACC-10442",
      changed_fields: ["mailing_address", "primary_contact"],
      updated_by: "dana.ortiz",
    },
    occurred_at: "2026-09-23T15:04:05Z",
  };
  const rawBody = JSON.stringify(envelope);
  const deliveryId = randomUUID();
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const signature = createHmac("sha256", SECRET)
    .update(`${timestamp}.${rawBody}`, "utf8")
    .digest("hex");

  const resp = await fetch(`https://api.usenexio.com/api/v1/events/ingest/${SOURCE_KEY}`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Nexio-Delivery": deliveryId,
      "X-Nexio-Timestamp": timestamp,
      "X-Nexio-Signature": `t=${timestamp},v1=${signature}`,
    },
    body: rawBody, // the exact signed string
  });
  console.log(resp.status, await resp.json());
  ```
</CodeGroup>

Response `200 OK`:

```json theme={null}
{
  "delivery_id": "0d9b7c4e-2a61-4f38-9e15-6c3a8b2f7d90",
  "outcome": "appended",
  "event_id": "4a8e2d71-5c93-4b06-8f1e-9d2c7a3b6e58",
  "inserted": true
}
```

| Response field | Meaning                                                                                                             |
| -------------- | ------------------------------------------------------------------------------------------------------------------- |
| `delivery_id`  | Your `X-Nexio-Delivery` value, or GitHub's `X-GitHub-Delivery` value for a GitHub source.                           |
| `outcome`      | `appended`: an event is in the log. `ignored`: the request was valid but produced no event, and `reason` says why.  |
| `reason`       | Present on `ignored`: `event_not_translated` or `status_not_configured` (GitHub sources).                           |
| `event_id`     | The event's ID, present on `appended`. If an event with the same identity already existed, this is that event's ID. |
| `inserted`     | `false` when this is a retry of a delivery Nexio already recorded. Nothing new happened.                            |

## Retries and idempotency

Nexio records each request it answers with `200` or `422` against its source and delivery ID (`X-Nexio-Delivery`, or `X-GitHub-Delivery` for a GitHub source), together with a SHA-256 of the body and the outcome.

* **Same delivery ID, same bytes:** Nexio returns the first answer again with `inserted: false` and appends nothing. A request first refused with `422` is refused again with the same `422`. Retry freely after a timeout or a `5xx`.
* **Same delivery ID, different bytes:** `409 delivery_id_reused`. Use a new delivery ID for new content.
* **Different delivery ID, same event identity:** the log keeps one event per `type`, `subject`, and `dedupe_key`, so a second append is dropped and the response carries the existing `event_id`. Set `dedupe_key` when your system can send the same fact under different delivery IDs.

Requests refused with any other status are not recorded, so they do not use up a delivery ID.

To retry, resend the same body with the same `X-Nexio-Delivery`. Compute a new timestamp and signature for each attempt; the body bytes must stay identical.

## Status codes

Requests are checked in this order: rate limit, body, source key, signature, source enabled, content. The first failure answers.

| HTTP | `code`                        | Cause                                                                                                                                                                                                                                                                                                                                            | Retry?                                               |
| ---- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------- |
| 200  | (success body)                | Recorded as `appended` or `ignored`.                                                                                                                                                                                                                                                                                                             | No                                                   |
| 400  | `invalid_body`                | The body could not be read.                                                                                                                                                                                                                                                                                                                      | No, fix the client                                   |
| 401  | `invalid_signature`           | A required header is missing or malformed, `t` does not equal `X-Nexio-Timestamp`, or no `v1` matches an accepted secret. The message names which, without echoing values.                                                                                                                                                                       | No, fix the signing                                  |
| 401  | `stale_timestamp`             | The timestamp is more than 300 seconds from Nexio's clock.                                                                                                                                                                                                                                                                                       | Yes, with a new timestamp                            |
| 401  | `ingest_source_not_found`     | No source has this key. A wrong key fails authentication, like a wrong secret.                                                                                                                                                                                                                                                                   | No                                                   |
| 401  | `invalid_credentials`         | An `ams360_ons` source's delivery carried no accepted authentication code.                                                                                                                                                                                                                                                                       | No, fix the code configured in the sending system    |
| 403  | `ingest_source_disabled`      | The signature is valid but the source is disabled.                                                                                                                                                                                                                                                                                               | No, ask Nexio                                        |
| 409  | `delivery_id_reused`          | The delivery ID was already recorded with different bytes.                                                                                                                                                                                                                                                                                       | No, use a new ID                                     |
| 413  | `body_too_large`              | The body is over 1 MiB.                                                                                                                                                                                                                                                                                                                          | No                                                   |
| 422  | The refusal reason            | The request was recorded as `rejected`. `code` is one of `event_type_not_allowed`, `event_type_reserved`, `subject_required`, `body_not_object`, `invalid_payload`, and for GitHub sources `repository_not_allowed`, `environment_not_allowed`, `commit_sha_required`. The message is "delivery was refused by the ingest source configuration". | No, fix the content or ask Nexio to widen the source |
| 429  | `rate_limited`                | Over a rate limit. Wait `Retry-After` seconds.                                                                                                                                                                                                                                                                                                   | Yes                                                  |
| 500  | `internal_error`              | Nexio could not look up the source, read its secret, or record the delivery.                                                                                                                                                                                                                                                                     | Yes, same delivery ID and bytes                      |
| 500  | `ingest_source_misconfigured` | The source's stored kind or configuration is invalid.                                                                                                                                                                                                                                                                                            | No, tell Nexio                                       |

## Rate limits

| Bucket             | Limit                   |
| ------------------ | ----------------------- |
| Per client address | 120 requests per minute |
| Per source key     | 600 requests per minute |

Both limits use a sliding one-minute window. A refused request gets `429 rate_limited` with `Retry-After`.

## GitHub deployments

A `github` source accepts GitHub's own webhook format, so you point a repository webhook straight at Nexio.

In the repository's settings on GitHub, add a webhook with:

* **Payload URL:** `https://api.usenexio.com/api/v1/events/ingest/<source_key>`
* **Content type:** `application/json`
* **Secret:** the `evsec_...` signing secret
* **Events:** Deployment statuses

GitHub signs each request with `X-Hub-Signature-256: sha256=<hex>` over the raw body and sends `X-GitHub-Delivery` and `X-GitHub-Event`. All three are required. GitHub requests carry no timestamp, so Nexio does not check their age.

What Nexio does with each request:

| GitHub request                                                                    | Result                                           |
| --------------------------------------------------------------------------------- | ------------------------------------------------ |
| Any event other than `deployment_status`, including GitHub's `ping`               | `200`, `ignored`, reason `event_not_translated`  |
| Repository is not the configured one (compared without case)                      | `422 repository_not_allowed`                     |
| Deployment environment is not configured                                          | `422 environment_not_allowed`                    |
| Status is not in the configured statuses (for example `pending` or `in_progress`) | `200`, `ignored`, reason `status_not_configured` |
| Body is not valid JSON, or has no deployment status ID                            | `422 invalid_payload`                            |
| No commit SHA                                                                     | `422 commit_sha_required`                        |
| Otherwise                                                                         | `200`, `appended`: a `code.deployed` event       |

The event's subject is `repository/<owner>/<name>` and its dedupe key is `deployment/<environment>/<commit_sha>`, so one commit deployed to one environment is one event, whatever number of statuses GitHub sends. The event body is described under [Event types](/events/platform-events#event-types).

## Secret rotation

Ask Nexio to rotate a source's secret. Nexio returns the new secret once, and the previous secret keeps working for 24 hours. Switch your sender to the new secret inside that window. After 24 hours only the new secret verifies.

## Limits

| Item                                  | Value                  |
| ------------------------------------- | ---------------------- |
| Body size                             | 1 MiB                  |
| Timestamp tolerance (`nexio` sources) | 300 seconds either way |
| Rate limit per client address         | 120 per minute         |
| Rate limit per source key             | 600 per minute         |
| Previous secret after rotation        | Valid for 24 hours     |

<CardGroup cols={2}>
  <Card title="Send an inbound event" href="/api-reference/events/ingest-event">
    The endpoint reference.
  </Card>

  <Card title="Automations" href="/events/automations">
    What Nexio can do when your event arrives.
  </Card>
</CardGroup>
