Transactions
Posting an order is how a member earns points, spends coupons and collects stamps. It is the highest-value call in the integration and the one with the most fields.
When to post
Section titled “When to post”When the money is taken. Not when the cart is created, not when the order is placed pending payment.
If you post on order creation and the payment then fails, the member has points for a purchase that never happened, and you have to cancel. Posting after capture avoids the whole class of problem.
For a shop that captures on dispatch rather than on order, post on dispatch.
Creating a transaction
Section titled “Creating a transaction”POST /api/v2.0/stores/{storeId}/members/{memberId}/transactionsAuthorization: Bearer {token}Content-Type: application/json{ "external_id": "ORDER-1001", "external_receipt_id": "REC-55021", "created_at": "2026-09-10T12:30:00+02:00", "currency": "NOK", "total": 424.15, "total_tax": 84.83, "total_discount": 74.85, "round_off_amount": 0, "eft_pos": "Paid", "send_email_receipt": false, "payment_details": [ { "payment_method": "Card", "sub_total": 424.15 } ], "department_id": "101", "origin": { "system_id": "my-webshop", "employee_id": null, "department_id": "101", "channel": "OnlineStore" }, "coupon_codes": ["WELCOME15"], "stamp_cards": [ { "stamp_card_id": 12, "action": "collected", "count": 1 } ], "details": [ { "product": { "external_id": "1042", "sku": "SKU-123", "name": "T-shirt, large", "brand": "Acme", "category": { "name": "Tops", "external_id": "cat-11" }, "gtin": { "type": "EanGtin13", "code": "7036772255075" } }, "quantity": 1, "unit_price": 499.00, "unit_measure": "pcs", "tax_percentage": 25, "discount": 74.85, "total_price": 424.15, "coupon_codes": [], "stamp_cards": [] } ]}{ "is_ok": true, "details": "" }The status is 200 whether or not the order was recorded. Check is_ok —
a duplicate, for one, comes back 200 with is_ok: false.
Posting the transaction also releases any coupon reservation made for this
order. You do not need to call cancel-reservation afterwards.
Field reference
Section titled “Field reference”Order level
Section titled “Order level”| Field | Type | Notes |
|---|---|---|
external_id | string | Required. Your order id. Used later to cancel or refund, and to catch duplicates |
external_receipt_id | string | Optional receipt number |
created_at | date-time | Required. See Timestamps |
currency | string | NOK, SEK, EUR, USD, GBP |
total | number | Amount the customer actually paid, after discount, including tax |
total_tax | number | Tax included in total |
total_discount | number | Total discount given |
round_off_amount | number | Rounding adjustment; 0 if none |
eft_pos | string | Card terminal text. Webshops typically use "Paid" |
send_email_receipt | bool | Leave false unless the merchant has asked us to send receipts |
payment_details[] | array | One entry per tender. Must sum to total |
department_id | string | Which department the sale belongs to |
origin | object | Set channel: "OnlineStore" |
coupon_codes[] | string[] | Order-level coupons. Repeat a code for multiple redemptions |
coupons[] | array | Point-redemption coupons — see below |
stamp_cards[] | array | Stamps collected or deducted |
details[] | array | Required, non-empty. Line items |
Line item
Section titled “Line item”| Field | Type | Notes |
|---|---|---|
product.external_id | string | Your product id |
product.sku | string | The SKU shared across channels. Set this — it is how we match products between your shop and the till |
product.name | string | |
product.brand | string | Optional |
product.category | object | { name, external_id } |
product.gtin | object | { type, code }, type is Isbn, UpcGtin12 or EanGtin13 |
quantity | number | Fractional allowed, for weight |
unit_price | number | Including tax |
unit_measure | string | pcs, kg, … |
tax_percentage | number | 0.00–100.00. A 25% rate is 25, not 0.25 |
discount | number | Discount on this line |
total_price | number | quantity x unit_price - discount |
total_price is quantity times unit price, minus discount. Compute it
rather than copying unit_price, or your line totals will not reconcile on
multi-quantity lines.
Payments
Section titled “Payments”One entry per tender. A split payment is several entries:
"payment_details": [ { "payment_method": "Gift card", "sub_total": 100.00 }, { "payment_method": "Card", "sub_total": 324.15 }]payment_method is free text and appears in merchant reporting — keep it
consistent across your integration.
Two coupon arrays
Section titled “Two coupon arrays”A transaction carries two coupon fields, and which one you use depends on the coupon type.
| Field | Shape | Use for |
|---|---|---|
coupon_codes | ["WELCOME15"] | Ordinary coupons — percentage, fixed amount, free shipping |
coupons | [{ "code": "PTS500", "redeemed_value": 50.00 }] | Point-redemption coupons, where points are converted to money |
"coupon_codes": ["WELCOME15"],"coupons": [ { "code": "PTS500", "redeemed_value": 50.00 }]redeemed_value is an amount of money, in the transaction’s
currency — how much of the order the coupon paid for. It is not a number
of points, and not a count of uses. We need it because a points-to-currency
coupon has no fixed value; the customer chooses how much to redeem.
Both fields exist at order level and per line item, and both are optional — send only what applies.
For an ordinary coupon used twice, repeat the code rather than reaching for
coupons:
"coupon_codes": ["STAMP5", "STAMP5"]Timestamps
Section titled “Timestamps”Send created_at with an explicit UTC offset.
"created_at": "2026-09-10T12:30:00+02:00"or, if the value is genuinely UTC:
"created_at": "2026-09-10T10:30:00Z"The field is a DateTimeOffset. When an offset is present the instant is
unambiguous, and that is the end of the matter. Include the seconds —
12:30:00, not 12:30.
Off-by-a-few-hours timestamps are quietly destructive: campaign windows, “purchases this month” and expiry all shift, and nothing errors.
Cancelling
Section titled “Cancelling”Reverses the whole transaction — points, coupons and stamps — and releases any coupon reservation still held for the order.
POST /api/v2.0/stores/{storeId}/transactions/{externalTransactionId}/cancel{ "reason": "Customer cancelled the order", "origin": { "system_id": "my-webshop", "department_id": "101", "channel": "OnlineStore" }}Refunding
Section titled “Refunding”Use a refund for a partial return; use cancel for a full one.
POST /api/v2.0/stores/{storeId}/members/{memberId}/transactions/{transactionId}/refund{transactionId} is the original order’s external_id — ORDER-1001
— the same value cancel takes. Unlike cancel, this path does include the
member segment.
The body is the transaction shape with a reason added, and per-line
coupon and stamp fields removed. Send only the lines being returned.
We set the refund’s own external_id for you, whatever the body says: the
first refund of ORDER-1001 is recorded as ORDER-1001_refund1, the
second as ORDER-1001_refund2, and so on. Do not try to invent your own.
{ "external_id": "ORDER-1001", "reason": "Item returned, wrong size", "created_at": "2026-09-12T09:15:00+02:00", "currency": "NOK", "total": 424.15, "total_tax": 84.83, "total_discount": 0, "round_off_amount": 0, "payment_details": [ { "payment_method": "Card", "sub_total": 424.15 } ], "department_id": "101", "origin": { "system_id": "my-webshop", "department_id": "101", "channel": "OnlineStore" }, "details": [ { "product": { "sku": "SKU-123", "name": "T-shirt, large" }, "quantity": 1, "unit_price": 499.00, "tax_percentage": 25, "discount": 74.85, "total_price": 424.15 } ]}If the entire transaction needs to be refunded, use the cancel endpoint instead — it fully reverses the transaction, including coupons and stamp cards.
A partial refund does not give the coupon back. If the merchant wants that, it is a manual decision on their side — say so in your own support notes rather than assuming.
Stamps
Section titled “Stamps”"stamp_cards": [ { "stamp_card_id": 12, "action": "collected", "count": 1 }]| Field | Notes |
|---|---|
stamp_card_id | From GET /stores/{storeId}/stamp-cards |
action | "collected" or "deducted" |
count | Must be positive |
Works at order level and per line item.
Duplicates
Section titled “Duplicates”We catch duplicate transactions for you. A transaction is a duplicate when
its external_id, origin.department_id and origin.system_id all match
one we already have. A duplicate is not recorded again; it comes back as:
{ "is_ok": false, "details": "You cannot add duplicate transaction." }The status is 200, so the only signal is is_ok.
That makes retries safe. Order webhooks retry, and a handler that times out
reading our response can simply post again: if the first attempt landed,
the second is rejected as a duplicate and nothing is counted twice. Treat
this exact details message as “already recorded” and move on.
Three rules keep it working:
- Make
external_iddeterministic from the order —ORDER-{id}. Never include a timestamp or random component, or every retry looks like a new order. - Keep
origin.department_idandorigin.system_idfixed. They are part of the match. Change either between attempts and a retry is no longer a duplicate. - Check
is_okon every response. A duplicate is a success for a retry, but a failure for a genuinely different order that reused anexternal_id.
Guest orders
Section titled “Guest orders”An order from someone who is not a member has nowhere to go. Do not invent a member for it — that requires consent you do not have. Post nothing.
If your checkout offers loyalty signup at payment and the customer accepts, create the member first, then post the transaction against the new member id. Order of operations matters: no member, no transaction.
Validation errors
Section titled “Validation errors”| Field | Rule |
|---|---|
external_id | Required, non-empty |
created_at | Required |
details | Required, non-empty |
origin | Required |
Failures return 400 with a
validation error.
