Skip to content

Ingest API

Markdown

A 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.

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.

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.

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.

  • 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.
  • channel can 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 receives 400 invalid / market_channel_mismatch: <market>.
  • external_id, line_reference, variant_key, group_key, catalog_id and customer_key can 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_phone and customer_key are not written back.
  • Unknown lines in the delivery list do not cause the request to fail; they are returned in the unknown list 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 delivered report does not decide the line’s state and does not hide confirmed delivery information.
  • If a returned or cancelled line arrives again as delivered in the order data without delivered_at, that state is ignored.
  • When the payment changes to refunded or voided, or a line is reported as returned, 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.
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.

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 }
);