Skip to content

Orders ​

Send webshop orders to ZERPY and follow their status, shipments, invoices and payment. Creating, cancelling and payment links need orders:write; reading needs orders:read.

POST /orders ​

Creates a sales order (channel webshop). By default it is confirmed immediately, which reserves the stock.

bash
curl -X POST https://zerpy.app/api/v1/orders \
  -H "Authorization: Bearer $ZERPY_API_KEY" \
  -H "Content-Type: application/json" \
  -d @order.json
json
{
  "external_id": "1001",
  "prices_include_vat": true,
  "confirm": true,
  "customer": {
    "email": "mari@example.ee",
    "name": "Mari Maasikas",
    "phone": "+372 5000 0000",
    "company_name": "Optional OÜ",
    "reg_code": "12345678",
    "vat_number": "EE123456789"
  },
  "billing_address": {
    "line1": "Pärnu mnt 10", "city": "Tallinn", "region": "Harju maakond",
    "postal_code": "10148", "country": "EE"
  },
  "shipping_address": {
    "name": "Mari Maasikas", "phone": "+372 5000 0000",
    "line1": "Omniva parcel machine", "city": "Tallinn", "postal_code": "10111", "country": "EE",
    "pickup_point": "Ülemiste Selver"
  },
  "lines": [
    { "sku": "P1", "quantity": 2, "unit_price": 12.40 },
    { "sku": "P2", "quantity": 1, "unit_price": 6.20, "vat_rate": 24, "description": "Gadget (gift wrap)" },
    {
      "sku": "SAUNA-1", "quantity": 1, "unit_price": 11532.00,
      "options": [{ "group": "Heater", "value": "HUUM Hive" }, "3f42a0c1-…"]
    }
  ],
  "shipping": { "description": "Omniva parcel machine", "price": 3.99 },
  "payment": { "amount": 11573.99, "method": "online", "reference": "MONTONIO-8f3a" },
  "notes": "Please call before delivery"
}

Request fields ​

FieldRequiredDescription
external_idyesYour order number (max 100 characters). Idempotency key — see below
currencyOptional 3-letter code; must equal the company currency, else 422 unsupported_currency. API orders are always in the company currency
prices_include_vatDefault true: unit_price and shipping.price are gross (what the customer paid). ZERPY converts them to net. false = the prices are net
confirmDefault true: the order is confirmed and stock reserved. false = left as a draft for someone to check and confirm in ZERPY
customer.emailyesCustomer e-mail
customer.nameyesPerson's name
customer.phone
customer.company_nameSet for business customers — becomes the customer name
customer.reg_codeCompany registry code (used for matching)
customer.vat_numberVAT number
billing_addressAddress object (below)
shipping_addressAddress object, plus name, phone, pickup_point (parcel machine / pickup point)
linesyes1–200 lines
lines[].skuyesProduct SKU. Unknown or archived SKUs → 422 unknown_sku
lines[].quantityyesPositive number
lines[].unit_priceyesUnit price, ≥ 0 (gross or net per prices_include_vat), including option prices
lines[].vat_rateDefaults to the product's VAT rate
lines[].descriptionLine text; defaults to the product name
lines[].optionsConfigurable products — see below
shipping{ description, price, vat_rate? } — added as a separate line without a product. VAT defaults to 24
paymentMoney already received in the shop — see below
notesOrder notes (max 5000 characters)

Address object: line1, line2, city, region (county / state / province, e.g. WA), postal_code, country (ISO 3166 alpha-2, e.g. EE). All fields are optional.

Idempotency (external_id) ​

external_id is unique per company. If an order with the same external_id already exists, ZERPY creates nothing and returns the existing order with status 200, header Idempotent-Replayed: true and "warnings": []. This is safe even for two identical requests at the same moment — so always retry POST /orders after a timeout or network error.

Customer matching ​

  1. If customer.reg_code is given: an existing customer with that registry code.
  2. Otherwise (or if not found): an existing customer with the same e-mail (case-insensitive; the oldest one wins).
  3. Otherwise a new customer is created: name = company_name or name, business customer when company_name is given, payment terms 0 days, billing address = billing_address (else shipping_address), note "Created from webshop order …".

Existing customers are never overwritten — the order's own addresses go on the order. Archived customers are not matched.

Prices and VAT ​

  • With prices_include_vat: true (default) ZERPY divides each price by 1 + vat_rate/100 and stores net prices. Totals may differ from your shop by ±0.01 in rare rounding cases — compare with totals.gross in the response.
  • Your shop decides VAT. For a VAT-free sale (an EU business with a VAT number, or delivery outside the EU) send "vat_rate": 0 on every line (and on shipping). If ZERPY then detects an EU supply or export from the customer and delivery country, the order is marked as such and the legal VAT note is printed on the invoice. Otherwise the order is treated as domestic.
  • ZERPY does not recalculate prices from price lists for API orders — the shop's price is stored.

Configurable products ​

lines[].options lists the chosen option values (max 20), each either:

  • an option value id from GET /products, or
  • a name pair { "group": "Heater", "value": "HUUM Hive" } — matched case-insensitively against the names in the company language or any translation.

Rules:

  • One value per group. A required group that is not answered → 422 options_required (details.groups lists the missing groups).
  • An unknown value, a value of another product, or two values of one group → 422 invalid_options.
  • unit_price must already include the option prices — ZERPY stores the configuration (it goes to production, invoices and documents) but does not add the deltas.
json
{
  "error": {
    "code": "options_required",
    "message": "SAUNA-1: choose \"Heater\"",
    "details": { "sku": "SAUNA-1", "groups": ["Heater"] }
  }
}

Payment received in the shop ​

Send payment when the customer has already paid online (card, bank link, Montonio …):

FieldDescription
amountRequired. Amount received (gross, positive)
methodonline (default), card, bank_transfer, cash, other
referenceTransaction id; defaults to external_id
paid_atYYYY-MM-DD, default today

It is recorded as a payment on the order. When the invoice is issued later it is marked paid automatically. Omit payment for unpaid orders (e.g. bank transfer after ordering) — see payment link for collecting money online.

Response ​

201 Created:

json
{
  "data": { "id": "8622…", "number": "SO-00042", "external_id": "1001", "status": "confirmed", "…": "see Order object" },
  "warnings": [
    { "code": "insufficient_stock", "sku": "P2", "needed": 5, "available": 2 }
  ]
}

Orders are accepted even when stock is short (backorder). Check warnings: each insufficient_stock warning says how much was needed and how much was available. Warnings only appear when the order is confirmed.

The order ships from the company's default own warehouse. If the company has no warehouse → 409 no_warehouse.

GET /orders/{id} ​

One order. {id} is the ZERPY id (UUID) or the order number (SO-00042) — the same applies to every /orders/{id}/… path.

bash
curl https://zerpy.app/api/v1/orders/SO-00042 -H "Authorization: Bearer $ZERPY_API_KEY"

Returns { "data": Order } or 404 not_found.

GET /orders ​

Lists the company's orders (all channels — webshop, manual, partner portal), sorted by updated_at, oldest first.

QueryDescription
external_idOnly the order with this external_id — find your shop order
updated_sinceISO 8601 timestamp with time zone: only orders changed after it
page, limitPage from 1; limit default 50, max 100
bash
curl "https://zerpy.app/api/v1/orders?external_id=1001" -H "Authorization: Bearer $ZERPY_API_KEY"

Returns { "data": [Order, …], "page", "limit", "total", "has_more" }.

POST /orders/{id}/cancel ​

Cancels an order and releases its stock reservation. Possible only while the order is a draft or confirmed (nothing shipped) and has no invoice. Otherwise 409 cannot_cancel. Returns { "data": Order } with status: "cancelled".

If the customer had paid, the payment status becomes refund_due — the refund itself is handled in ZERPY / your payment provider.

POST /orders/{id}/payment-link ​

Creates an online payment link (bank link / card) through the company's Montonio account — your shop needs no Montonio keys. The company must have Montonio connected in ZERPY.

Body (all optional):

FieldDescription
amountDefault: what the customer should pay next — the rest of the prepayment, else the open balance. Cannot exceed the open balance (400 invalid_amount)
return_urlhttp(s) URL where Montonio sends the customer after paying
localePayment page language: et, en, fi, lv, lt, pl, de, ru. Default from the customer's billing country
bash
curl -X POST https://zerpy.app/api/v1/orders/SO-00042/payment-link \
  -H "Authorization: Bearer $ZERPY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "return_url": "https://shop.example.com/thank-you", "locale": "en" }'
json
{
  "data": {
    "url": "https://…",
    "amount": "59.79",
    "currency": "EUR",
    "expires_at": "2026-11-21T10:00:00.000Z",
    "reference": "SO-00042.3fa2c1"
  }
}
  • 201 = a new link; 200 = an unpaid link for the same amount was reused. Links are valid for 14 days.
  • Redirect the customer to url. When they pay, Montonio notifies ZERPY, the payment is recorded on the order (payment.status → paid) and the invoice is issued as paid.
  • Errors: 409 cannot_pay (Montonio not connected, order cancelled, currency not EUR or PLN, nothing left to pay), 400 invalid_amount, 422 validation_failed (invalid body), 503 provider_unavailable.

Order object ​

json
{
  "id": "8622…",
  "number": "SO-00042",
  "external_id": "1001",
  "status": "partially_shipped",
  "channel": "webshop",
  "order_date": "2026-11-07T00:00:00.000Z",
  "currency": "EUR",
  "customer": { "name": "Mari Maasikas", "email": "mari@example.ee" },
  "shipping_address": {
    "line1": "Omniva parcel machine", "city": "Tallinn", "postal_code": "10111", "country": "EE",
    "name": "Mari Maasikas", "phone": "+372 5000 0000", "pickup_point": "Ülemiste Selver"
  },
  "lines": [
    {
      "sku": "P1", "description": "Widget", "options": [],
      "quantity": "2", "quantity_shipped": "2",
      "unit_price": "10", "discount_pct": "0", "vat_rate": "24", "total_net": "20.00"
    },
    {
      "sku": null, "description": "Omniva parcel machine", "options": [],
      "quantity": "1", "quantity_shipped": "0",
      "unit_price": "3.2177", "discount_pct": "0", "vat_rate": "24", "total_net": "3.22"
    }
  ],
  "totals": { "net": "23.22", "vat": "5.57", "gross": "28.79" },
  "shipments": [
    {
      "number": "SH-00007", "carrier": "omniva", "tracking_number": "CC123EE",
      "shipped_at": "2026-11-08T09:12:00.000Z",
      "lines": [{ "sku": "P1", "quantity": "2" }]
    }
  ],
  "invoices": [
    { "number": "INV-00031", "type": "invoice", "status": "paid", "total": "28.79", "issue_date": "2026-11-08T00:00:00.000Z", "due_date": "2026-11-08T00:00:00.000Z" }
  ],
  "payment": {
    "status": "paid", "paid": "28.79", "balance": "0.00",
    "prepayment": null, "prepayment_due_date": null, "reference": "80000428"
  },
  "notes": null,
  "created_at": "2026-11-07T10:00:00.000Z",
  "updated_at": "2026-11-08T09:12:00.000Z"
}
FieldDescription
statusdraft, confirmed (stock reserved), partially_shipped, shipped, completed (shipped and invoiced), cancelled — derived by ZERPY from shipments and invoices
channelwebshop for API orders; manual, portal or api for orders created elsewhere
lines[].skunull for lines without a product (e.g. shipping)
lines[].optionsChosen configuration: [{ "group": "Heater", "value": "HUUM Hive" }], empty for normal products. Option prices are included in unit_price
lines[].unit_priceNet unit price (up to 4 decimals)
lines[].total_netNet line total after discount
totalsnet, vat, gross
shipments[]Each shipment with carrier, tracking number, date and shipped lines
invoices[]Issued invoices and credit notes only (drafts are not listed). type = invoice or credit_note (credit notes have negative totals); status = issued or paid
payment.statusunpaid, awaiting_deposit / deposit_overdue (a prepayment was requested and is not paid yet), partially_paid, paid, overpaid, refund_due (cancelled with money received), none (nothing to pay)
payment.paid / balanceReceived so far / still to pay (total − paid)
payment.prepayment, prepayment_due_dateRequested prepayment, if any (set in ZERPY; API orders get none automatically)
payment.referencePayment reference number for bank transfers on this order