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

# HTTP Delivery

> Receiving Ontoto exports over HTTP and verifying signed requests

Ontoto can push data to an HTTP endpoint you host. You register the endpoint in
the [Ontoto Dashboard](https://beta.ontoto.com) under **Developer → Export
Destinations → HTTP Endpoints** (URL, method, static request headers such as an
`Authorization` token, and an optional signing secret), then attach it to an
export. Ontoto sends the payload as the raw request body of a `POST` or `PUT`
request.

The export system supports the following payloads:

* **File exports** send the generated file as the body, with its path in
  `X-Ontoto-File-Path`.
* **Events** send a small JSON body produced by the export function itself,
  with no file involved. Events suit a fetch-on-notify integration, e.g.
  an export configured to run when a device transmits can tell you when new data is received, so
  your backend can fetch the readings immediately
  through the sensor data API.

Requests are signed following the
[Standard Webhooks](https://www.standardwebhooks.com) specification, so you
can verify them with an off-the-shelf `standard-webhooks` library in most
languages instead of implementing the validation yourself.

## Request headers

Every delivery carries a common envelope:

| Header              | Description                                                                                                                            |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `Content-Type`      | Media type of the request body.                                                                                                        |
| `webhook-id`        | Unique id of the delivered payload. Every retry of the same payload, automatic or manual, carries the same id, so you can deduplicate. |
| `webhook-timestamp` | Unix timestamp (seconds) at which the request was sent. Included in the signature to bound replay attacks.                             |
| `webhook-signature` | Space-separated list of `<version>,<base64 signature>` entries, currently `v1,...`. Only present when a signing secret is configured.  |

Any static headers configured on the endpoint (for example `Authorization`)
are sent as-is on every request. Individual consumers add their own headers on
top of the envelope:

| Header                  | Sent by       | Description                                                                                     |
| ----------------------- | ------------- | ----------------------------------------------------------------------------------------------- |
| `X-Ontoto-File-Path`    | File exports  | File path of the exported file body, defined on the export. Not included in event payloads.     |
| `X-Ontoto-Export-Uuid`  | Event exports | The applied export the event came from.                                                         |
| `X-Ontoto-Device-Sn`    | Event exports | Serial number of the device the event covers. Absent when the export is not scoped to a device. |
| `X-Ontoto-Generated-At` | Event exports | ISO-8601 UTC instant the export ran, the same value the export function saw.                    |

Receivers should ignore unrecognised headers and unrecognised
`webhook-signature` versions; new consumers and scheme versions add entries
without removing existing ones.

The `webhook-id` of each export delivery is also logged in the export generation history available
in the **Exports** page.

## Verifying the signature

The signing secret shown when you configure the endpoint is in the Standard
Webhooks `whsec_<base64>` format. The easiest way to verify is a
`standard-webhooks` library:

```javascript theme={null}
import { Webhook } from "standard-webhooks";

const wh = new Webhook("whsec_...");

// rawBody must be the unmodified request body; headers as received.
function handleDelivery(headers, rawBody) {
  wh.verify(rawBody, headers); // throws if invalid or outside the replay window
  // ... process the body
}
```

To verify manually instead:

1. Read `webhook-timestamp` and check it is within your replay window (the
   reference libraries default to 5 minutes of your server clock).
2. Base64-decode the secret after the `whsec_` prefix; those bytes are the
   HMAC key.
3. Compute the base64 HMAC-SHA256 over the UTF-8 bytes of
   `{webhook-id}.{webhook-timestamp}.{raw body}`. Use the raw body bytes exactly as received.
4. Compare against each `v1,<signature>` entry in `webhook-signature` using a
   constant-time comparison; accept if any entry matches.

```javascript theme={null}
import { createHmac, timingSafeEqual } from "crypto";

// headers: lowercase-keyed, as Node delivers them. rawBody: a Buffer of the
// bytes as received, never a string or a parsed object.
function verifyOntotoSignature(headers, rawBody, secret) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatures = headers["webhook-signature"];
  if (!id || !timestamp || !signatures) return false;

  const ageMs = Math.abs(Date.now() - Number(timestamp) * 1000);
  if (Number.isNaN(ageMs) || ageMs > 5 * 60 * 1000) return false;

  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = createHmac("sha256", key)
    .update(`${id}.${timestamp}.`)
    .update(rawBody)
    .digest("base64");

  return signatures.split(" ").some((entry) => {
    if (!entry.startsWith("v1,")) return false;
    const sig = Buffer.from(entry.slice("v1,".length));
    const exp = Buffer.from(expected);
    return sig.length === exp.length && timingSafeEqual(sig, exp);
  });
}
```

### Why the replay window matters

The signature proves who sent a request and that it was not altered, but not
when; a captured signed request would otherwise pass verification forever if resent. Because the timestamp is inside the signed content, an attacker
can neither replay an old request outside your window nor freshen its
timestamp without breaking the signature. Use `webhook-id` to deduplicate any
replays within the window.

## Test requests

The **Send test request** button in the endpoint dialog sends a small payload
(by default the JSON body `{ "test": true }`) through the same pipeline:
static headers, envelope headers, and signature are identical to a real
delivery, so you can use it to verify connectivity, authentication, and your
signature implementation end to end.

## Responding

Respond with any `2xx` status to acknowledge a delivery. Any other status (or
a timeout, currently 30 seconds) is treated as a failed delivery.
