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, or ping for 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:

  1. Take the raw request body, before parsing it.
  2. Compute an HMAC-SHA256, in hex, of <t>.<raw body> using your webhook’s signing secret.
  3. Compare it, in constant time, with each v1 value in the header. After you generate a new secret, the old one keeps signing alongside it for 24 hours, so there can be two.
  4. Refuse the message if t is 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.