Skip to content

Inbound webhook URLs

A webhook URL takes an order file that one of your own systems POSTs, such as an ERP export or an internal tool, and runs it through the same order intake as an emailed order. It lands in the workspace’s Inbox and is read for an order. Owners and admins create URLs under Organization → Orders → Receiving. They’re for your own systems, not for handing to outside senders.

POST the file to https://api.sunnysdk.com/in with the URL’s token as a bearer token. No API key, session or CORS is involved: the token is the credential.

curl
curl -X POST "https://api.sunnysdk.com/in" -H "Authorization: Bearer sunny_in_…" -H "Content-Type: text/plain" -H "Idempotency-Key: po-1001" --data-binary @order.sif
Node.js 18+
import { readFile } from "node:fs/promises";
const res = await fetch("https://api.sunnysdk.com/in", {
method: "POST",
headers: {
Authorization: "Bearer sunny_in_…",
"Content-Type": "text/plain",
"Idempotency-Key": "po-1001",
},
body: await readFile("order.sif"),
});
// 202 { "id": "msg_…", "duplicate": false }
console.log(res.status, await res.json());

Tools that can only take a URL. Zapier, Make, n8n and many ERP export settings can’t set an Authorization header. Give them the full URL, https://api.sunnysdk.com/in/<token>, which Sunny shows next to the token when the URL is created. Prefer the header when the sender can set one: a URL gets written to request logs along the way and a header doesn’t. Sending a token in both places is a 400.

Send the file itself as the body, or as the one file part of a multipart form.

Accepted
Content-Type text/plain, application/xml, text/xml, application/pdf, image/*, application/octet-stream, none, or multipart/form-data with exactly one file part (other fields are ignored).
Files SIF, OFDA XML, PDF and images, detected from the file’s first bytes and then its name. Other file types are rejected with unsupported_file. PDFs and images are read by the same model as emailed attachments.
Filename The multipart part’s filename, else Content-Disposition, else one picked from the file kind (order.sif, order.xml, order.pdf).
Size At most 20 MB.

Each URL also has a signing secret (whsec_…). Send X-Sunny-Signature: t=<unix seconds>,v1=<hex>, the HMAC-SHA256 of {t}. followed by the raw request body bytes (the whole multipart body, for a multipart POST), keyed with that secret. It’s the scheme Sunny’s outgoing webhooks use, with the same 5-minute tolerance.

Sign a request
import { createHmac } from "node:crypto";
const t = Math.floor(Date.now() / 1000);
const v1 = createHmac("sha256", process.env.SUNNY_SIGNING_SECRET) // whsec_…
.update(Buffer.concat([Buffer.from(`${t}.`), body])) // body: the exact bytes you send
.digest("hex");
headers["X-Sunny-Signature"] = `t=${t},v1=${v1}`;

A signature that doesn’t verify is always refused. An unsigned request is accepted unless the URL has Only accept signed files turned on; then a copied token or URL is useless without the secret.

Send an Idempotency-Key (1 to 255 printable ASCII characters), such as your purchase order number. A repeat with the same key answers 202 with the first delivery’s id and duplicate: true instead of storing the file again. Without a key, the same file sent to the same URL within 24 hours counts as the same delivery.

On a 503 or a 429, send the same request again (after Retry-After for a 429). A 503 can mean the file was stored but its order intake couldn’t be queued; repeating the request with the same key or file queues it without storing a second copy.

Each URL, and each workspace across all its URLs, takes up to 30 requests a minute.

Status When
202 Stored, or a duplicate of a stored delivery: { "id": "msg_…", "duplicate": false }. The id is the delivery’s, not an order’s: the order is created afterwards, and a file that turns out not to be an order makes none.
400 ambiguous_token, empty_body, unreadable_body, expected_one_file, unsupported_content_type, unsupported_file, invalid_idempotency_key.
401 token_required, invalid_signature, signature_required.
404 Unknown, paused or deleted URL. One body for all of them.
413 too_large: over 20 MB.
429 rate_limited. Wait for Retry-After, then send again.
503 unavailable. Send the same request again.

Errors are JSON: { "error": "<code>", "detail": "<sentence>" }.

The delivery shows in the Inbox at once, named after the URL’s label. Sunny then reads it for an order and checks the lines against the manufacturer catalogs. To have your own system told when that order is ready, subscribe an endpoint to order.received and order.status_changed under Webhooks.

To import an order file straight into a draft quote from your own software instead, without the Inbox, use POST /v1/quotes/import in the REST API (reference: api.sunnysdk.com/v1/docs). The /v1 surface is in beta.