Tote developers
All guides

Shopping

Payments

Paying for an order with the Moyasar SDK โ€” the payment attempt, verification, retries, and why the server always has the last word.

Orders are paid by card through Moyasar. The app charges the card with the Moyasar SDK; the server decides the amount beforehand and confirms the result with Moyasar afterwards. A payment is never trusted on the app's word alone.

The flow#

sequenceDiagram
    participant App as Customer app
    participant API as Tote API
    participant SDK as Moyasar SDK
    participant M as Moyasar

    App->>API: POST orders/checkout
    API-->>App: order (unpaid) + payment {payment_id, amount}
    App->>SDK: charge(given_id = payment_id, amount, metadata)
    SDK->>M: create payment
    M-->>SDK: result
    SDK-->>App: done
    App->>API: POST payments/verify {order_id, payment_id}
    API->>M: fetch the payment
    M-->>API: paid, amount
    API-->>App: 200, order paid
    Note over API,M: Backup paths: Moyasar's webhook,<br/>and a re-check every 10 minutes
  1. Checkout places the order and returns a payment object: the first payment attempt. If payment is null, open one with POST payments/intent.
  2. Start the Moyasar SDK with the values from that object.
  3. When the SDK finishes, call POST payments/verify. The server asks Moyasar for the real result and marks the order paid.

The payment attempt#

{
  "payment_id": "5f0c7b9e-2a41-4c1f-9d6e-8a3b2c1d0e9f",
  "amount": 13950,
  "currency": "SAR",
  "description": "Order #1042",
  "metadata": { "order_id": "1042", "order_number": "#1042" }
}

Pass these to the SDK unchanged:

  • payment_id goes in as Moyasar's given_id. Moyasar uses it as the id of the payment it creates, so if the app retries a charge that timed out, Moyasar returns the first payment instead of charging twice.
  • amount is in halalas (1 SAR = 100 halalas), as Moyasar expects. Never recompute it from prices on the device.
  • description and metadata tie the payment to the order. Verification rejects a payment whose metadata names a different order.

Verifying#

POST /api/v1/customers/auth/payments/verify

{"order_id": 1042, "payment_id": "5f0c7b9e-2a41-4c1f-9d6e-8a3b2c1d0e9f"}
Response Meaning
200 The order is paid (or already was).
400 Moyasar says the payment did not succeed, or its amount does not match the order.
500 Moyasar could not be reached. The payment may still have succeeded.

On a 500, retry verify after a moment, or simply show the order as "processing". Payments do not depend on the app getting through: Moyasar also notifies the server directly, and the server re-checks unfinished attempts every 10 minutes. Either path marks the order paid and sends an order_paid push.

Retrying a failed payment#

Calling payments/intent again within 30 minutes returns the same attempt, so a double tap cannot open two.

After a declined card, open a fresh attempt with "retry": true. A Moyasar given_id can only ever belong to one payment, failed or not, so a new card try needs a new payment_id.

payments/intent answers 409 when the order can no longer be paid: it is already paid, or it was cancelled.

After payment#

  • The order shows is_paid: true, and the store is pushed order_paid.
  • A store sees an order in its queue as soon as it is placed, before it is paid. Merchant apps should show is_paid clearly.
  • Refunds โ€” for a cancelled paid order, or an approved Return โ€” always go back to the original card. They are issued by Tote; the apps never start one.