Skip to main content
9Pic Checkout can POST every completed purchase to an endpoint you control. This page is the wire-format reference for that request, written for the case that motivates most integrations: turning a purchase into a tax invoice in your own accounting system. Unlike the rest of this reference, this is not an endpoint you call. 9Pic calls you, so the shared Conventions — base URL, 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

If you configured an authorization header, it is sent verbatim with the value you saved — for example Authorization: Bearer sk_live_.... Check it on every request; treat a mismatch as a 401.
Return 2xx as soon as you have durably stored the body, and do your invoice work afterwards. Slow endpoints get retried, which means duplicates you then have to collapse.

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.
Key your invoice on data.reference_id, not on webhook_event_id. An organiser who clicks Resend webhook after fixing an outage sends the same purchase with a new webhook_event_id; keying on that would issue a second invoice for money you were only paid once.
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:
Currencies do not all use two decimals, which is exactly why the exponent travels with the amount instead of being assumed: 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.
Do not assume currency_exponent is 2, and do not derive it from a lookup table of your own. Read it from the payload — it is stored per payment, so it stays correct even if a gateway or currency is added later.

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:
  1. Always true:
  2. subtotal_minor - discount_minor is the amount charged before the tax adjustment. Which field that equals depends on tax.mode:
Worked through the example above (exclusive, exponent 3):
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:
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.
Do not read discounts from data.coupon alone. Volume-tier discounts — “20% off when you buy 20 or more” — are applied by the pricing engine before any coupon and never appear in the coupon block. An invoice built from data.coupon only will not reconcile against total_minor on tiered orders.
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.
  • version changes 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 the unit_price family can all be null.
  • 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.