X-API-Key, and the unified responseType / message / data envelope — do not apply. The body documented below is the entire request body.
Enabling It
1
Add your endpoint
In the dashboard, open Payment Configuration → Purchase webhook. Save your HTTPS URL, and optionally an authorization header name and token. The URL and token are stored once for the whole organisation.
2
Turn it on per event
Open the event’s Checkout configuration. The purchase webhook is on by default once the organisation URL is saved. Turn the switch off if you do not want that event to send purchases.
Your endpoint must be reachable over HTTPS on a public host. URLs that use
http://, embed credentials, or resolve to a private, loopback, or link-local address are rejected when you save them and again before every delivery.The Request
Authorization: Bearer sk_live_.... Check it on every request; treat a mismatch as a 401.
Identifiers, and Which One to Key On
This is the part worth reading twice, because the two ids answer different questions.data.reference_id is derived from the order and payment records and has the form 9pic-<order_id>-<payment_id>. Every delivery of the same purchase carries the same value — automatic retries and dashboard resends included.
Use webhook_event_id for what it is good at: correlating your logs with 9Pic’s delivery history when you are debugging a specific attempt.
Payload
Envelope
data.order, data.event, data.organiser, data.buyer
organiser carries the trading name only. The supplier tax registration number for the invoice comes from data.tax.registration_id.data.payment
Money and Currency
Every monetary value appears twice:*_minor as an integer, and the same value as a decimal string in major units.
Always compute from the _minor integers. They are exact. The string form is a convenience for display and for feeding a decimal type; never parse it into a binary float, or you will lose fractions of a currency unit on large orders.
What currency_exponent Means
currency_exponent is how many decimal places the currency has — the power of ten between the minor unit and the major unit:
The example above is Bahraini dinar with exponent
3, so 21120000 fils is 21120.000 BHD — not 211,200.00. Hardcoding two decimals would misprice it by a factor of ten.
data.amounts — the Invoice Breakdown
This block exists so you never have to re-derive the maths. Every field is in the currency named by amounts.currency_code at amounts.currency_exponent.
Reconciliation Rules
Two relationships let you assert your invoice balances before you issue it:-
Always true:
-
subtotal_minor - discount_minoris the amount charged before the tax adjustment. Which field that equals depends ontax.mode:
total_minor is taken from the payment record, never recomputed. If your own arithmetic disagrees with it, trust total_minor — that is the money that moved — and investigate the difference.data.tax
null when the event has no tax configured, or when the gateway is Stripe — Stripe issues its own tax documents, so 9Pic does not add a second tax layer on top.
Exclusive vs Inclusive
The distinction changes which number you print as the net line, so it must not be guessed:- exclusive
- inclusive
Tax is added on top of the listed price. The buyer paid more than the listed amount.
The whole tax block is frozen on the order at purchase time. Changing the organisation’s tax rate later never rewrites the numbers behind an invoice you already issued.
data.discounts
An ordered list of every reduction between subtotal and taxable_amount. Empty when the buyer paid list price. Their amount_minor values sum to amounts.discount_minor.
data.coupon remains available and describes just the coupon, or is null when none was used: code, discount_type, discount_minor, discount, original_amount_minor, final_amount_minor.
data.items
Bill the invoice line at
priced_count, not item_count, when the two differ — priced_count × unit_price_minor is what produced subtotal_minor. Individual photo ids are deliberately not included; orders regularly run to thousands of photos.Building the Invoice
Python
Forward Compatibility
- Ignore unknown fields. New keys are added without bumping
version. versionchanges only for breaking changes — a removed field, a renamed field, or a changed meaning.- Treat nullable fields as nullable.
data.tax,data.coupon,order.sale_phase,payment.paid_at, and theunit_pricefamily can all benull. - Do not pin to key order. JSON object order is not part of the contract.
Troubleshooting
The delivery status of every purchase — sent, queued for retry, or failed, with the last response code — is visible on the transaction detail page in the dashboard, along with a Resend webhook action.

