SALAF EMSDevelopers
salafems.com

Create an order

Customer, variants, idempotency key, and what comes back.

Your system took an order — on a Facebook page, in a call centre, through your own checkout — and it needs to exist in Salaf, with stock reserved and the merchant's dashboard showing it. This is that call, start to finish.

You need a key with orders:write (and products:read if you resolve variants by id). orders:write implies orders:read, so you can read the order back with the same key.

1. Decide the idempotency key first

Before you build the request, generate the key and store it with your own order record.

That ordering is the whole point. If you generate the key at the moment of the HTTP call, a process restart between attempts produces a new key — and a new order. Generated first and persisted, the same key survives a crash, a queue redelivery, and a manual re-run.

your_order.salaf_idempotency_key = "3f1a5c8e-0b2d-4f77-9a13-5e6c7d8f9a10"

2. Resolve the line items

Lines reference a variant by variant_id or sku — one or the other, per line. If your own system already stores SKUs, use them and skip this step entirely; that is what they are for.

If you need to look one up — to check it exists before placing the order, or to read its current price:

export SALAF_API_KEY="salaf_sk_YOUR_API_KEY"

curl -sS "https://api.salafems.com/ext/v1/variants/TSHIRT-RED-M" \
  -H "Authorization: Bearer $SALAF_API_KEY"

GET /variants/{identifier} accepts an id, a SKU or a barcode, so a barcode scanner's output can be looked up directly.

3. Resolve the customer

Two options, and you must pick exactly one:

  • customer_id — an existing customer in this store.
  • customer{ phone, full_name?, email? }, which finds or creates.

Find-or-create matches on the canonical Bangladeshi phone number, so +8801712345678 and 01712345678 are the same person. full_name is required only when the phone matches nobody.

4. Place the order

# The key identifies the ORDER you are placing, not this HTTP attempt.
# Generate it once, store it with your own record, and reuse it on retries.
IDEMPOTENCY_KEY=$(uuidgen)

curl -sS -X POST "https://api.salafems.com/ext/v1/orders" \
  -H "Authorization: Bearer $SALAF_API_KEY" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "facebook",
    "customer": { "phone": "01712345678", "full_name": "Rafiqul Islam" },
    "items": [
      { "sku": "TSHIRT-RED-M", "quantity": 2 }
    ],
    "shipping_charge": 120,
    "note": "Deliver after 6pm."
  }'

The fields that matter, and their defaults:

FieldRequiredNotes
items[]YesAt least one line. variant_id or sku, plus quantity.
customer_id / customerYes, one ofNever both.
store_idOnly for company keysA store key may repeat its own store, and may not name another.
channelNoA channel slug. Defaults to the store's manual channel.
location_idNoWhich warehouse reserves the stock. Defaults to the company default.
shipping_chargeNoDefault 0. Nothing calculates this for you.
tax_amountNoDefault 0.
coupon_codeNoPriced and redeemed by the same engine as every other surface. An unusable code fails the order rather than mispricing it.
paymentNoAn upfront settled payment — { amount, payment_method_id?, transaction_id? }.
unit_price / discount_price per lineNoYour prices are trusted here. Omitted, the catalogue price is used.

5. Read the response

201 with the full order — items and customer expanded, totals computed, stock already reserved:

{
  "id": "c9d8e7f6-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
  "number": "ORD-20260811-1042",
  "source": "api",
  "channel": { "slug": "facebook", "name": "Facebook", "type": "facebook" },
  "customer_id": "5b1e7c33-2a94-4f18-8d60-7e3b9c40a2f1",
  "status": "pending",
  "payment_status": "unpaid",
  "fulfillment_status": "unfulfilled",
  "subtotal": "2500.00",
  "shipping_charge": "120.00",
  "total_amount": "2620.00",
  "paid_amount": "0.00",
  "due_amount": "2620.00",
  "items": ["…one object per line…"],
  "customer": { "full_name": "Rafiqul Islam", "phone": "01712345678" }
}

Store id and number against your own record. id is what every later call takes; number is what the merchant and the customer will say out loud.

Two things to note in that body: source is api and cannot be set — it is how the merchant tells API orders from POS ones — and money is a string with two decimals, because JSON numbers cannot hold every Decimal(12,2) exactly.

6. Handle the failures that matter

CodeHTTPWhat to do
IDEMPOTENCY_IN_FLIGHT409Wait a couple of seconds and retry with the same key. Never re-key.
IDEMPOTENCY_CONFLICT422Your code reused a key for a different body. A bug; do not retry.
INSUFFICIENT_STOCK400fields.items names the line. Surface it to whoever is taking the order — this is a real business answer, not a transient failure.
VALIDATION_ERROR422Unknown SKU, bad phone, unknown channel slug, missing store_id on a company key. fields says which.
FORBIDDEN_SCOPE403The key lacks orders:write. Scopes are fixed at creation — make a new key.
RATE_LIMITED429Writes are 30/min per key by default. Honour Retry-After.

What happens next

Placing the order reserves stock but does not finish it. The rest of its life happens through:

  • POST /orders/{id}/payments — record money as it arrives. The amount is capped at the net due, and transaction_id is unique per store.
  • PATCH /orders/{id}/status — move it along, validated against the same strict transition map staff use.
  • POST /orders/{id}/cancel — release the reservation.
  • Webhooksorder.updated, order.paid and order.fulfilled tell you when the merchant's staff move it, so you do not have to poll for it.

The whole thing, in order

  1. Generate an idempotency key and persist it with your record.
  2. Resolve variants (or use your SKUs directly).
  3. Build the body — customer, items, shipping charge, channel.
  4. POST /orders with the key. Retry the same key on 409 and on network failure.
  5. Store id and number.
  6. Subscribe to order.* webhooks, or poll updated_after, for the rest.