CSV export and webhooks
Get issued POs out of Mittral and into your spreadsheets or your own systems.
CSV export and webhooks come with the Starter, Growth, Business and Enterprise plans. Workspace admins find both under Settings → Integrations. Every plan emails each issued PO to its supplier, whether or not these are on.
CSV export
Each issued PO has a CSV download on its request page, for anyone who can see the request. Workspace admins can also download every PO issued between two dates, with one row per PO or one row per line, from Settings → Integrations.
The file matches the PO PDF: it’s made from the PO as it was issued. It opens in Excel with accents intact. The last column, PO status, says whether the PO is in force (Issued), was cancelled, or was replaced by a revision.
Webhooks
With a webhook on, Mittral sends each PO issued from then on to an address you choose, as a signed JSON message. Admins set the address, see what was sent, retry a failed delivery and send a test event under Settings → Integrations.
The address must use https and a public domain name (not an IP address or an internal name), on port 443 or above 1023, with no user name or password in it. Mittral doesn’t follow redirects, so use the final address.
Receiving webhooks
Each message is an HTTPS POST with a JSON body and
these headers:
-
Mittral-Event-Id: the event’s id, the same on every retry. -
Mittral-Event-Type:purchase_order.issued,purchase_order.cancelled, orpingfor a test event. Ignore types you don’t know: new ones may be added. -
Mittral-Delivery-Attempt: 1 for the first try, then 2, 3 and so on. -
Mittral-Signature:t=<unix seconds>,v1=<signature>.
To check a message is from Mittral:
- Take the raw request body, before parsing it.
-
Compute an HMAC-SHA256, in hex, of
<t>.<raw body>using your webhook’s signing secret. -
Compare it, in constant time, with each
v1value in the header. After you generate a new secret, the old one keeps signing alongside it for 24 hours, so there can be two. -
Refuse the message if
tis more than 5 minutes from your clock.
Keep the event ids you’ve processed for at least 30 days and ignore repeats: a retry sends the same event again with the same id.
Answering and retries
Answer with any 2xx status within 10 seconds, and do slow work
afterwards. Anything else is tried again after 1, 5 and 25 minutes,
about 2 hours, then every 6 hours: 8 tries over about a day. If they
all fail, the workspace’s admins are emailed and can retry it from
Settings. A 410 Gone answer or a redirect fails
straight away.
What’s in a message
The body is version 1 of the event:
id, type, version,
createdAt, workspace (id and name) and
data. For an issued PO,
data.purchaseOrder has the PO number and revision,
when it was issued, the request reference and who approved it, the
buyer, the supplier, the delivery address, the currency and tax,
the net, tax and gross totals, and every line. Amounts come as an
exact decimal string and in minor units, for example
{"value": "123.45", "minor": 12345}.
data.purchaseOrder.pdf has a link to download the
PO’s PDF: url, expiresAt,
contentType and fileName. Fetch the
url with a plain GET, no sign-in
needed, when you process the message, and keep the file. The
link works for 7 days, only for
that PO, and only while the workspace is active; after that it
answers 410 Gone (the PDF is still on the PO in
Mittral). Anyone with the link can download the PDF, so don’t log
it or pass it on. A retry carries a fresh link.
A revised PO comes as another purchase_order.issued
with the same number, the next revision and
supersedes naming the revision it replaces: update
the order you have rather than adding one. When an issued PO is
cancelled, purchase_order.cancelled follows its issue
with data.cancellation: the PO’s id, number and
revision, the request reference, supplier, total, when and by whom
it was cancelled, and the reason.
A change that could break your code comes as a new version, and
your webhook stays on the version it was set up with until an
admin moves it. New optional fields, like pdf, can
be added to a version, so ignore fields you don’t know.