Ingest API
MarkdownA summary of the server endpoints that send order, delivery and email events to Sayproof.
These endpoints are called server to server. For a detailed explanation and example code in several languages, see Sending orders and deliveries and Email delivery events.
Authentication
Section titled “Authentication”| Endpoint | Method |
|---|---|
/v1/ingest/orders, /v1/ingest/deliveries |
Authorization: Bearer <server-key>; the key needs the send orders (orders) permission. Optionally, a signature with a signing password can be used instead. |
/v1/mail-events/generic |
A signature with the email delivery events password |
If the request has an Authorization: Bearer header, the server key is checked; otherwise the signature headers below are expected.
Signature
Section titled “Signature”| Header | Value |
|---|---|
X-Sayproof-Secret |
The password ID (the Password ID column of the password table in the panel; not the password’s name) |
X-Sayproof-Timestamp |
An ISO 8601 time value including the time zone; it may differ from Sayproof’s clock by at most ±5 minutes |
X-Sayproof-Signature |
hex(HMAC-SHA256(password, time + "." + body)) |
The signature is calculated over the exact bytes sent.
Endpoints
Section titled “Endpoints”| Endpoint | Body | Response |
|---|---|---|
POST /v1/ingest/orders |
A single order or {"channel", "orders": […]} (up to 100 orders) |
{"accepted", "orders": [{"external_id", "id"}]} |
POST /v1/ingest/deliveries |
A single delivery or {"deliveries": […]} (up to 100 records) |
{"accepted", "unknown": [{"external_id", "line"}]} |
POST /v1/mail-events/generic |
A single event or {"events": […]} (up to 500 events) |
{"accepted", "matched", "duplicates", "ignored", "opted_out"} |
On the endpoints specific to email services (/v1/mail-events/ses, sendgrid, postmark, mailgun), the address contains an access code specific to your brand instead of a signature; these addresses are shown in the panel.
Behaviour
Section titled “Behaviour”- The order list is processed as a whole. If one of the orders is refused, none of the other orders in the same request are saved.
channelcan be given at the top of the body, in each order, or both; if both are given, the value in the order wins. The order’s market must match the channel’s market; otherwise the request receives400 invalid / market_channel_mismatch: <market>.external_id,line_reference,variant_key,group_key,catalog_idandcustomer_keycan be up to 128 characters.- When the same order is sent again, it is updated; no duplicate is created. Order lines that are missing from a later submission are not deleted. If the customer details of an order have been turned into pseudonyms and the order is sent again,
customer_email,customer_phoneandcustomer_keyare not written back. - Unknown lines in the delivery list do not cause the request to fail; they are returned in the
unknownlist of the response. An unknown state value, however, causes the entire request to be rolled back. - The same delivery information (the same line, state, source and time) is not saved a second time.
- An estimated
deliveredreport does not decide the line’s state and does not hide confirmed delivery information. - If a returned or cancelled line arrives again as
deliveredin the order data withoutdelivered_at, that state is ignored. - When the payment changes to
refundedorvoided, or a line is reported asreturned, product reviews written earlier receive the “Returned after the review” mark on their proof and the automatic checks run again; process experience reviews do not receive this mark. - The same email event (the same
event_id) is not processed a second time.
Value lists
Section titled “Value lists”| Field | Values |
|---|---|
payment_state |
pending, paid, partially_refunded, refunded, voided |
fulfilment_state, state |
placed, preparing, shipped, in_transit, delivered, cancelled, returned |
source |
platform, carrier, manual, estimated |
confidence |
verified, estimated |
Email event type |
delivered, deferred, bounced, complained, opened, unsubscribed |
For error codes, see Error codes › Transfer.
Example: a shared request function
Section titled “Example: a shared request function”The order and delivery endpoints are called with the server key, and email events are signed with the email delivery events password. The helper below covers both: given a server key it adds the Authorization header, given a password it adds the signature headers.
import { createHmac } from "node:crypto";
export async function sayproofPost(path, payload, auth) { const body = JSON.stringify(payload); const headers = { "Content-Type": "application/json" }; if (auth.serverKey) { headers.Authorization = `Bearer ${auth.serverKey}`; } else { const timestamp = new Date().toISOString(); headers["X-Sayproof-Secret"] = auth.secretId; headers["X-Sayproof-Timestamp"] = timestamp; headers["X-Sayproof-Signature"] = createHmac("sha256", auth.secret).update(`${timestamp}.${body}`).digest("hex"); } const answer = await fetch(`${process.env.SAYPROOF_BASE}${path}`, { method: "POST", headers, body, signal: AbortSignal.timeout(10000), }); const result = await answer.json().catch(() => ({})); if (!answer.ok) throw new Error(`${answer.status} ${result.error ?? ""} ${result.detail ?? ""}`.trim()); return result;}
await sayproofPost( "/v1/ingest/deliveries", { deliveries: [ { external_id: "ORD-1001", line_reference: "1", state: "delivered", observed_at: "2026-09-27T09:00:00+03:00", source: "carrier" }, ], }, { serverKey: process.env.SAYPROOF_SERVER_KEY });