Some charges can't be priced in advance. A merchant buys 500 SMS credits today and 2,000 next week, pays for last month's usage, or accepts a quote your team prepared. Add-ons sell one fixed price defined in your publication; the Checkout SDK lets your backend charge any amount it computes, paid by the merchant in Salla's native payment flow.Your backend creates the checkout, the merchant pays it (in your embedded app or through a payment link), and your backend confirms the payment with the app.checkout.paid event or by reading the checkout. Salla collects the payment and pays your share out with your regular partner payouts.
Fixed price or computed amount
Selling an add-on at the price set in your publication? Use embedded.checkout.create({ type: "addon", slug }) and the add-on subscription events. Use a server-created checkout when your backend computes the amount: usage, credits, top-ups, quotes.
The checkouts scope. Add the Checkouts scope to your app in the Partners Portal. Creating a checkout needs checkouts.read_write; reading one needs checkouts.read or checkouts.read_write. A token without it is refused with 401. The scope is not available to private apps: the Checkout SDK is for public apps.
2.
The store's access token. Call the API with the app access token of the store, like the other Merchant API endpoints. The app must be installed on the store.
3.
An embedded app using @salla.sa/embedded-sdk 0.2.7 or later (the checkout_id form of checkout.create()), if the merchant pays inside your app. Optional: an embedded app is needed only to pay in the in-app drawer. A public app without embedded pages sends the merchant to checkout_url.
4.
A webhook URL on your app that handles app.checkout.paid, next to your other App Events.
Required. Your own reference, up to 64 characters. Scoped to your app and the store.
taxable
Optional, default true. false makes the checkout VAT-exempt and needs approval (see below).
currency
Optional, default SAR, case-insensitive; one of SAR, AED, KWD, BHD, QAR, OMR, JOD, EGP, LBP, USD, GBP, EUR (another value answers 422). items[].amount is in this currency; the charge is converted to the store's billing currency (set by the store's country), and the response amounts are in that billing currency.
items
Required. At least one line.
items[].name
Required. Object of names per language, e.g. { "ar": "…", "en": "…" }, each up to 255 characters. Arabic is the default name; without it, the first language sent.
items[].amount
Required. The line total for quantity units, VAT-exclusive, from 1 to 999999.99.
items[].quantity
Optional integer, at least 1, default 1.
items[].description
Optional, up to 500 characters. Returned on read and in app.checkout.paid.
Salla sets the app, the store and the return page from your token: source, app_id and redirect_uri are ignored if you send them, and cart_id, partner_company_id, items[].service_id and items[].service_type are rejected with 422.The response (201) returns the whole checkout, in the same shape as GET /admin/v2/checkouts/{id}, with status: "pending", the totals the merchant will pay and the payment link:
{"status":201,"success":true,"data":{"id":1638492741,"reference_id":"topup-8812","status":"pending","subtotal":{"amount":125,"currency":"SAR"},"total_discount":{"amount":0,"currency":"SAR"},"tax":{"amount":18.75,"currency":"SAR"},"total":{"amount":143.75,"currency":"SAR"},"checkout_url":"https://s.salla.sa/checkout/1638492741","paid_at":null,"items":[{"name":"رصيد 500 رسالة","description":"Top-up for October","quantity":500,"amounts":{"price_without_tax":{"amount":0.25,"currency":"SAR"},"total_discount":{"amount":0,"currency":"SAR"},"tax":{"percent":15,"amount":{"amount":18.75,"currency":"SAR"}},"total":{"amount":143.75,"currency":"SAR"}}}]}}
Store id next to your order: it identifies the checkout in the embedded app, in GET /admin/v2/checkouts/{id} and in app.checkout.paid.
If the earlier checkout with that reference_id is unpaid, it is replaced: the old checkout is deleted and the response carries a new id and checkout_url. Use only the new one; the old ID can no longer be opened.
If it is already paid, the request fails with 403 reference_id_already_paid. Use a new reference_id for a new purchase.
Use this to change the amount of a quote the merchant has not paid yet: create again with the same reference_id and the new items.
Register onResult() first, then open the payment drawer with the id from your backend, as a string:
checkout_id goes alone: combined with add-on items, empty, longer than 64 characters or with characters other than letters, digits, _ and -, create() throws INVALID_CHECKOUT_INPUT before anything is sent.
Paid without a redirect, onResult() fires with status: "paid" and order_id. Closing the drawer gives status: "cancelled".
3DS: when the payment needs a 3DS redirect, the merchant returns to https://s.salla.sa/embedded/app/{app_id}?paid=1&legacy=0 (?paid=&legacy=0, with an empty paid, when the payment failed), which opens your app's default embedded page (your first embedded page). There, onResult() fires with status: "paid" (or "failed"), your context and the checkout_id; the return URL carries no order_id, so read it from Get Checkout. Register onResult() on your default page too, and use context to route the merchant back.
Send checkout_url to the merchant: by email, in a chat, or as a button in your own UI. It opens the checkout page of the Salla dashboard (https://s.salla.sa/checkout/{id}), where the merchant, signed in to their store, reviews the lines and pays like any payment link. After paying, the merchant lands on /embedded/app/{app_id}?paid=1&legacy=0 (the default embedded page) for an embedded app, or on the app's installed-app page /apps/installed/{id}?paid=1&legacy=0 for a non-embedded app. paid is empty when the payment failed. On the embedded page, onResult() fires with status only (no order_id, checkout_id or context, since the checkout was not opened from your app).The link works until the checkout is paid or replaced by a new one with the same reference_id.
The merchant's return is not proof of payment
onResult() and the return to your page only drive your UI. Fulfil from the app.checkout.paid event, or after your backend reads status: "paid" from GET /admin/v2/checkouts/{id}.
Salla sends app.checkout.paid to your app's webhook URL once per paid checkout, after the order is created, with the same headers and signature as every other App Event. The payload carries data.checkout, the same object as the create and read responses: id, reference_id, status, subtotal, total_discount, tax, total, checkout_url, paid_at and items. The full payload is in App Events → App Checkout Paid.
1.
Verify the webhook signature.
2.
Use data.checkout.id as the idempotency key: record it in a unique-keyed table in the same transaction that grants the purchase, so a repeated delivery changes nothing.
3.
Match your order with data.checkout.reference_id and check total.amount against what you charged.
{"status":200,"success":true,"data":{"id":1638492741,"reference_id":"topup-8812","status":"paid","subtotal":{"amount":125,"currency":"SAR"},"total_discount":{"amount":0,"currency":"SAR"},"tax":{"amount":18.75,"currency":"SAR"},"total":{"amount":143.75,"currency":"SAR"},"checkout_url":"https://s.salla.sa/checkout/1638492741","paid_at":"2026-10-28T14:05:11+03:00","items":[{"name":"رصيد 500 رسالة","description":"Top-up for October","quantity":500,"amounts":{"price_without_tax":{"amount":0.25,"currency":"SAR"},"total_discount":{"amount":0,"currency":"SAR"},"tax":{"percent":15,"amount":{"amount":18.75,"currency":"SAR"}},"total":{"amount":143.75,"currency":"SAR"}}}]}}
status is pending until the merchant pays, then paid with paid_at. Read the checkout when your embedded page gets status: "paid", or to reconcile an event that never reached you, and fulfil a paid checkout through the same idempotent path as the event. Your app sees only its own checkouts on the token's store; any other ID returns 404.
items[].amount is the line total, VAT-exclusive. On read, each line's amounts shows price_without_tax (the unit price: amount ÷ quantity, rounded to 2 decimals), tax (the line VAT: percent and amount) and total (the line amount plus its VAT).
Before payment, total = subtotal − total_discount + tax (the VAT amount), the same the merchant sees on the checkout page; total is what the merchant pays. Once paid, subtotal, total_discount, tax and total are the order's figures, and paying from the store wallet never lowers total. The create response already carries them, so you can show the merchant the final amount before they pay.
Every amount is { amount, currency } in the store's billing currency (set by the store's country); total_discount is the discount applied to the checkout: usually 0, since checkouts take no coupon.
With taxable: false, every tax amount and tax.percent is 0.
On a partner demo store a Checkout API purchase costs 0: subtotal, tax and total are 0, the merchant confirms without a card, and app.checkout.paid arrives with total 0.