1. Checkout Module
Salla Platform Docs
  • Embedded SDK
    • Overview
    • Getting Started
    • Create an Embedded App
    • Authentication
    • App Design Guidelines
    • Installation
    • Playground
  • Auth Module
    • Get Token
    • Client Introspect
    • Refresh Token
  • Page Module
    • External Redirects
    • Navigation
    • Set Page Title
  • Nav Module
    • Create Navbar Action
    • Listen for Navbar Actions
    • Clearing Navbar Actions
    • Add Navbar Item
    • Update Navbar Item
    • Listen for Navbar Item Click
    • Remove Navbar Item
  • UI Module
    • Confirm Dialogs
    • Toast Notifications
    • Loading States
    • Breadcrumbs
  • Checkout Module
    • Create Checkout
    • Get App Add-ons
    • Subscribe for Payment Result
    • Server-Created Checkouts
  • Endpoints
    • Create Checkout
      POST
    • Get Checkout
      GET
    • Token Introspect
      POST
  • Resources
    • Support
    • Common Examples
  • Report Builder
    • Overview
    • Getting Started
    • JMESPath Tutorial
    • Understanding the Report Lifecycle
    • Salla Reports Examples
    • Set up Store data using merchant APIs
    • Report Types
      • Unit
      • Bar
      • Breakdown
      • Calendar
      • Distribution
      • Pipe
      • Plot
      • Ranking
      • Summary
      • Agrid
    • APIs
      • Token Introspect
  • App Onboarding
    • Overview
    • Get Started
    • Interfaces and Responses
    • Example
  • Schemas
    • Checkout
Merchant
Merchant
  • Merchant API
  • Embedded SDK
  • Salla OAuth 2.0
Storefront
Storefront
  • Twilight (React)
  • Twilight SDK
  • Web Components (Deprecated)
  • Ecommerce Events
  • Component Bundle
  • Checkout APIs
  • Twilight Engine (Deprecated)
  • Change Log
App Functions
Partner APIs
Partner APIs
  • App API
  • Shipments & Fulfillment APIs
  • Salla AWB
  • Recurring Payments API
  • Billing System Salla partners
  • Communication Apps
Dev Tools
Dev Tools
  • Partners Agent Kit
  • Salla Apps Playbook
  • Salla CLI
Merchant
Merchant
  • Merchant API
  • Embedded SDK
  • Salla OAuth 2.0
Storefront
Storefront
  • Twilight (React)
  • Twilight SDK
  • Web Components (Deprecated)
  • Ecommerce Events
  • Component Bundle
  • Checkout APIs
  • Twilight Engine (Deprecated)
  • Change Log
App Functions
Partner APIs
Partner APIs
  • App API
  • Shipments & Fulfillment APIs
  • Salla AWB
  • Recurring Payments API
  • Billing System Salla partners
  • Communication Apps
Dev Tools
Dev Tools
  • Partners Agent Kit
  • Salla Apps Playbook
  • Salla CLI
Salla - Opensource
Salla - Developers Community
  1. Checkout Module

Server-Created Checkouts

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 flow#

Before you start#

1.
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.

Step 1: Create the checkout on your backend#

FieldRules
reference_idRequired. Your own reference, up to 64 characters. Scoped to your app and the store.
taxableOptional, default true. false makes the checkout VAT-exempt and needs approval (see below).
currencyOptional, 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.
itemsRequired. At least one line.
items[].nameRequired. 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[].amountRequired. The line total for quantity units, VAT-exclusive, from 1 to 999999.99.
items[].quantityOptional integer, at least 1, default 1.
items[].descriptionOptional, 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.

Creating again with the same reference_id#

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.

Step 2: Hand the checkout to the merchant#

Pick one of the two ways, or offer both.

A. Pay inside your embedded app#

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.

B. Send the payment link#

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}.

Step 3: Confirm the payment and fulfil#

With the app.checkout.paid event#

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.
4.
Answer 200 once the fulfilment is stored.

By reading the checkout#

{
  "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.

Amounts and VAT#

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.

VAT-exempt checkouts#

Checkouts are VAT-taxed by default (taxable: true). To sell VAT-exempt:
1.
In the Partners Portal, request VAT-exempt Checkout SDK purchases in your app publication (Pricing step).
2.
Submit the publication. The request takes effect when Salla approves the publication.
3.
Send taxable: false on create.
Without approval, taxable: false answers 403 non_taxable_not_allowed.

Errors#

All errors use the Salla envelope { "status", "success": false, "error": { "code", "message" } }.
HTTPerror.codeWhenWhat to do
401UnauthorizedThe token lacks the checkouts scope (The access token should have access to one of those scopes: …), or is invalid or expired.Add the Checkouts scope to your app; use a valid store access token.
401Invalid-tokenNo token was sent.Send Authorization: Bearer <access_token>.
403non_taxable_not_allowedtaxable: false without VAT-exempt approval.Send taxable: true, or get approval first.
403reference_id_already_paidThe checkout with this reference_id is already paid.Use a new reference_id for a new purchase.
403app_not_installedCreate: your app is not installed on the store.Ask the merchant to install your app.
403errorThe token carries no app (This token cannot create or read checkouts.).Use the store's app access token.
404not_foundGET: no checkout of your app on this store has this id.Check the ID; create a new checkout.
422errorValidation failed; error.fields lists each invalid field.Fix the listed fields.
Embedded app results:
result.error.codeWhen
CHECKOUT_NOT_FOUNDThe checkout is not open on this store: unknown ID, already paid, or replaced by a newer checkout with the same reference_id.
INVALID_CHECKOUT_INPUTThrown by create() for a malformed checkout_id.
CHECKOUT_ERRORThe checkout could not be opened or the payment failed.

Testing on a demo store#

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.

Checklist#

The Checkouts scope added to your app (checkouts.read_write to create).
The amount is computed on your backend; reference_id is your own order reference.
The checkout id passed to create() as a string; onResult() registered before create() and on your default embedded page.
app.checkout.paid handler verifies the signature and fulfils once per checkout id.
Every error code above mapped to an action.
taxable: false sent only after VAT-exempt approval.
Modified at 2026-11-08 13:12:14
Previous
Subscribe for Payment Result
Next
Create Checkout