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.
Sending a file
Section titled “Sending a file”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 -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.sifimport { 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.
Body and files
Section titled “Body and files”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. |
Signing (optional)
Section titled “Signing (optional)”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.
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.
Retries and idempotency
Section titled “Retries and idempotency”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.
Responses
Section titled “Responses”| 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>" }.
After it arrives
Section titled “After it arrives”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.