Shoppingate Logo
HomePartnerSell on E-commAbout UsContact UsDevelopers
Documentation
Get started
  • Getting started
  • Environments
Mini-app development
  • Authoring
  • app.json config
  • Lifecycle
  • Routing & query
  • Host functions
  • · orderPayment
  • Permissions
  • Deep links
Backend integration
  • Authentication
  • Endpoints
  • Webhooks
Server side

Endpoints

A working "try it" is attached to each endpoint. Paste your API key and mini-app id once — they're remembered in your browser only — and every call on this page uses them.

Your credentials
POST/s2s/v1/ordersCreate order (vendor-initiated)
What it does

Opens an order on behalf of a user. Dedup key is vendor_order_ref — send the same one twice and you get the first order back with replayed: true.

Body
user_idRequired
SG user id — get it from getUserToken in the mini app.
vendor_order_refRequired
Your cart/order id, ≤ 40 printable-ASCII chars. Idempotency key for this call.
itemsRequired
Non-empty array of line items — see the orderPayment section for the per-category shape.
subtotal, total_amountRequired
Money as decimal string. The platform checks arithmetic (subtotal + delivery + tax = total).
delivery_address, shop_icon, shop_address, order_type
Display metadata for the receipt / order card.
Request schema
{
  "user_id":          <number>,                       // required
  "vendor_order_ref": <string, 1..40 printable ASCII>, // required — dedup key
  "items":            [ /* see orderPayment */ ],      // required, non-empty
  "subtotal":         <string, decimal>,               // required
  "total_amount":     <string, decimal>,               // required
  "delivery_fee":     <string, decimal>,               // optional
  "tax_amount":       <string, decimal>,               // optional
  "currency":         <string, ISO 4217>,              // optional, defaults to mini-app currency
  "delivery_address": <object> | null,                 // optional
  "shop_name_en":     <string, ≤120>,                  // optional — branch name (EN)
  "shop_name_ar":     <string, ≤120>,                  // optional — branch name (AR)
  "shop_icon":        <string URL, ≤2048>,             // optional — branch icon
  "shop_address":     <object> | null,                 // optional — branch address
  "order_type":       <string, ≤60>,                   // optional — "delivery" | "pickup" | ...
  "customer_meta":    <object> | null                  // optional — free-form JSON
}
Response
statusboolean
Always true on 2xx. Check the HTTP status code for error branches.
replayedboolean
true when the same vendor_order_ref was seen before — the returned data is the original order and no duplicate was created. Use this to keep your retry loops idempotent.
data.order_uuidstring, UUID
Stable id for this order. Use it for the read endpoint and to match webhook payloads.
data.order_numberstring
Human-readable receipt number shown to the user in-app (e.g. MA-2026-00042).
data.vendor_order_refstring
Echo of the ref you sent — useful for reconciling against your cart on your side.
data.status, order_status, payment_status{ id, name }
Machine id for logic, localized name for display. status mirrors order_status for backwards-compat.
data.itemsarray
Line items you sent, normalized (money coerced to decimal strings, category-specific fields kept).
data.subtotal, delivery_fee, tax_amount, total_amountstring, decimal
Money as decimal strings — do not parse to a float, keep them as strings until display.
data.currencystring, ISO 4217
Falls back to the mini-app's default currency if you omitted it on create.
data.payment_methodstring | null
Null until a payment succeeds; then one of card, wallet, apple_pay, tamara.
data.created_at, updated_atstring, ISO 8601 UTC
Timestamps for audit and diffing against your side.
messagestring
Human string — do not parse or branch on it. Use the HTTP status and data.*.id fields for logic.
Response schema
// 201 Created — new order (replayed = false)
// 200 OK      — same vendor_order_ref re-sent (replayed = true, no duplicate created)
{
  "status":   true,
  "replayed": <boolean>,
  "data":     {
    "order_uuid": "0193b09d-9c8a-7af3-8b56-a51d6a3e4f10",
    "order_number": "MA-2026-00042",
    "vendor_order_ref": "VND-CART-9b3f12",
    "state": "created",
    "status":         { "id": "created", "name": "Created" },
    "order_status":   { "id": "created", "name": "Created" },
    "payment_status": { "id": "unpaid",  "name": "Unpaid"  },
    "items": [
      { "sku": "pizza-pep-l", "name": "Pepperoni Pizza — Large", "qty": 2, "unit_price": 49.00 }
    ],
    "subtotal":     "98.00",
    "delivery_fee": "10.00",
    "tax_amount":   "16.20",
    "total_amount": "124.20",
    "currency":     "SAR",
    "payment_method":  null,        // "card" | "wallet" | "apple_pay" | "tamara" once paid
    "created_at":   "2026-09-13T14:22:11.000Z",
    "updated_at":   "2026-09-13T14:22:11.000Z"
  },
  "message":  "Order created"
}
Try it
GET/s2s/v1/orders/{orderUuid}Read one order
What it does

Returns the current state of an order created either by the vendor or by the mini app.

Path
orderUuidRequired
The order_uuid returned by create-order or received on a webhook.
Response
statusboolean
true on 200, false on error envelopes.
dataobject
Same shape as Create order's data — see that endpoint for every field. The state fields (order_status, payment_status, payment_method, updated_at) reflect the order's current state at read time.
error_codestring (on 4xx)
Machine string for branching — currently ORDER_NOT_FOUND for 404. Prefer this over the human message.
messagestring
Human string for logs — do not parse.
Response schema
// 200 OK
{
  "status":  true,
  "data":    {
    "order_uuid": "0193b09d-9c8a-7af3-8b56-a51d6a3e4f10",
    "order_number": "MA-2026-00042",
    "vendor_order_ref": "VND-CART-9b3f12",
    "state": "created",
    "status":         { "id": "created", "name": "Created" },
    "order_status":   { "id": "created", "name": "Created" },
    "payment_status": { "id": "unpaid",  "name": "Unpaid"  },
    "items": [
      { "sku": "pizza-pep-l", "name": "Pepperoni Pizza — Large", "qty": 2, "unit_price": 49.00 }
    ],
    "subtotal":     "98.00",
    "delivery_fee": "10.00",
    "tax_amount":   "16.20",
    "total_amount": "124.20",
    "currency":     "SAR",
    "payment_method":  null,        // "card" | "wallet" | "apple_pay" | "tamara" once paid
    "created_at":   "2026-09-13T14:22:11.000Z",
    "updated_at":   "2026-09-13T14:22:11.000Z"
  },
  "message": "OK"
}

// 404 Not Found
{
  "status":     false,
  "error_code": "ORDER_NOT_FOUND",
  "message":    "Order 0193b09d-… not found"
}
Try it
POST/s2s/v1/orders/{orderUuid}/transitionsUpdate order status
What it does

Move an order to a new state — accept it into processing, mark it completed, or cancel it. Enforced by the platform's state machine, so only transitions valid for a vendor actor are accepted. Vendor-allowed moves: created → cancelled, paid → processing, paid → cancelled, processing → completed, processing → cancelled. Not vendor-driven: created → paid (the payments service fires it on successful capture), and any transition to refunded (admin-only). Terminal states (completed, cancelled, refunded) accept no further outgoing transitions.

Auto-refund on vendor cancel. A vendor cancel from paid or processing also triggers a full automatic refund of order.total_amount, routed back to the original payment method (card → card gateway, wallet → wallet, tamara → tamara, split-paid → proportional). The response returns as soon as the state flip commits; the gateway leg settles asynchronously. Two webhooks fire on cancel-from-paid: order.cancelled (immediate) + order.refunded (when the gateway confirms settlement — usually seconds for wallet/Tamara, up to a few days for card). Cancel from created has no captured payment: only order.cancelled fires, no refund. Partial refunds on cancel are not supported — full only.

Path
orderUuidRequired
The order_uuid returned by create-order or received on a webhook.
to_stateRequired
Target state. One of paid, processing, completed, cancelled, refunded. Vendors cannot pick paid or refunded — those are gated to the payments service and admin respectively.
reason
Free-form string, ≤ 1000 chars — audit note captured on the transition row. Recommended for cancelled.
metadata
Free-form JSON object stored on the transition row for your own audit (e.g. { "internal_note": "kitchen accepted" }).
Request schema
{
  "to_state": <string, enum>,           // required — "paid" | "processing" | "completed" | "cancelled" | "refunded"
                                        //   vendor-driven set: "processing" | "completed" | "cancelled"
  "reason":   <string, ≤1000> | null,   // optional — audit note
  "metadata": <object> | null           // optional — free-form JSON stored on the transition row
}
Response
statusboolean
true on 200, false on every error envelope.
data.order_uuidstring, UUID
Echo of the path param.
data.status, data.statestring
The order's new state — same value as the to_state you sent. Both keys are returned for backwards compat; treat them as aliases.
data.updated_atstring, ISO 8601 UTC
Timestamp of this transition. Also emitted on the matching webhook (order.processing, order.completed, order.cancelled), so you can diff against your side.
error_code (on 4xx)string
Machine string for branching: TARGET_STATE_REQUIRED (400), ORDER_NOT_FOUND (404), INVALID_TRANSITION / TERMINAL_STATE / ORDER_UPDATED_ELSEWHERE (409). Do not parse the human message.
messagestring
Human string for logs — do not parse.
Response schema
// 200 OK — transition applied
{
  "status": true,
  "data": {
    "order_uuid": "0193b09d-9c8a-7af3-8b56-a51d6a3e4f10",
    "status":     "processing",   // the new state
    "state":      "processing",   // alias of status (backwards-compat)
    "updated_at": "2026-09-21T14:22:11.000Z"
  },
  "message": "OK"
}

// 400 Bad Request — required field missing or malformed
{
  "status":     false,
  "error_code": "TARGET_STATE_REQUIRED",
  "message":    "to_state is required"
}

// 404 Not Found — no such order for this mini-app
{
  "status":     false,
  "error_code": "ORDER_NOT_FOUND",
  "message":    "Order 0193b09d-… not found"
}

// 409 Conflict — state machine rejected the transition.
// error_code narrows the failure:
//   INVALID_TRANSITION       — not allowed from the current state for a vendor
//   TERMINAL_STATE           — the order is already in a terminal state
//   ORDER_UPDATED_ELSEWHERE  — optimistic-lock lost, refetch and retry
{
  "status":     false,
  "error_code": "INVALID_TRANSITION",
  "message":    "no transition from \"created\" to \"completed\""
}
Try it
POST/s2s/v1/identity/verifyVerify a mini-app-minted identity token
What it does

Confirms an identity JWT your mini app minted via getUserToken. Always hands back the SG user_id and the current consent_status; also returns name, email, phone_number, and phone_code when the user has granted data-sharing consent for this mini app.

Body
tokenRequired
The JWT string handed to your backend from the mini app.
Request schema
{
  "token": <string, JWT>            // required — the JWT your mini app got from getUserToken
}
Response
statusboolean
true when the signature verified; false on the 401 envelope. Note: status = true does not mean the user shared their profile — always read consent_status before touching PII fields.
data.user_idnumber
Shoppingate user id — always returned, regardless of consent state. Persist this against your own account row so subsequent sessions with the same user land on the same profile. This is the load-bearing identifier for your side of the integration.
data.mini_app_idnumber
Your mini-app id — must match the X-Mini-App-Id header. If it does not, treat as a rejected token even if the call returned 200.
data.consent_status"granted" | "revoked" | "not_set"
Always returned. Reflects the user's current data-sharing decision for this mini app. granted — the user has agreed and PII fields below are populated. revoked — the user previously agreed then revoked; PII fields are all null. not_set — the user has never been asked; PII fields are all null. Branch on this before reading any of the profile fields, and offer the user a way to grant consent from inside your mini-app when it isn't granted.
data.namestring | null (consent-gated)
User's full display name — first_name and last_name joined with a space. Populated only when consent_status === "granted"; null otherwise.
data.first_name, last_namestring | null (consent-gated)
Individual name parts, in case you need to store them separately or greet the user with just their first name. Both are null unless consent is granted.
data.emailstring | null (consent-gated)
User's email — populated only when consent is granted. May itself still be null even under consent if the user hasn't added an email to their Shoppingate account.
data.phone_number, phone_codestring | null (consent-gated)
Mobile number and dial code (e.g. "+966") — populated only when consent is granted. Combine them for a full E.164 number. All Shoppingate users have a verified phone, so both are populated whenever consent is granted.
data.verified_atstring, ISO 8601 UTC
When this verify call resolved. Log for audit; do not use for token freshness (use expires_at).
data.issued_at, expires_atstring, ISO 8601 UTC
Token lifetime. Default TTL is 10 minutes (short-lived by design). Have the mini app call getUserToken again to mint a fresh one — this endpoint is a handshake, not a request-time header.
error_code (on 401)string
Machine string for the failure mode: IDENTITY_TOKEN_EXPIRED, AUDIENCE_MISMATCH (token minted for a different mini app), MINI_APP_MISMATCH, INVALID_SIGNATURE, or INVALID_SUBJECT. Branch on this — do not parse the human message.
messagestring
Human string for logs — do not parse.
Response schema
// 200 OK — signature valid. Verify ALWAYS succeeds on a good signature,
// even when consent is not granted — vendor still learns the confirmed user_id.
// PII fields (name / email / phone_number / phone_code) are populated only when
// consent_status === "granted"; otherwise they are all null.
{
  "status": true,
  "data": {
    "user_id":        <number>,                                  // SG user id — always present
    "mini_app_id":    <number>,                                  // your mini-app id (matches X-Mini-App-Id)
    "consent_status": "granted" | "revoked" | "not_set",         // always present
    "name":           <string> | null,                           // consent-gated (first_name + last_name joined)
    "first_name":     <string> | null,                           // consent-gated
    "last_name":      <string> | null,                           // consent-gated
    "email":          <string> | null,                           // consent-gated
    "phone_number":   <string> | null,                           // consent-gated
    "phone_code":     <string> | null,                           // consent-gated (e.g. "+966")
    "verified_at":    <string ISO>,                              // when this verify call resolved
    "issued_at":      <string ISO>,                              // JWT iat
    "expires_at":     <string ISO>                               // JWT exp — 10 min after iat by default
  },
  "message": "Identity verified"
}

// 401 Unauthorized — signature, issuer, audience, or expiry failed.
// error_code narrows the failure: IDENTITY_TOKEN_EXPIRED | AUDIENCE_MISMATCH
// | MINI_APP_MISMATCH | INVALID_SIGNATURE | INVALID_SUBJECT
{
  "status":     false,
  "error_code": "IDENTITY_TOKEN_EXPIRED",
  "message":    "jwt expired"
}
Try it

Need help? We’re here.

Our team is ready to support you in every step of your experience.

Email Us
[email protected]
Sell With SGContact UsTerms of ServicePrivacy PolicyFAQTravel FAQ
Connect with us
X / TwitterLinkedInInstagramFacebookTiktok

Copyright © Shoppingate 2026

Mada
Visa
Mastercard
Apple Pay
Tamara