Order lifecycle
Every order moves through the same lifecycle, no matter which flow created it. This page is your reference for that lifecycle: each status, what it means, which actions are allowed while the order is in it, and the exact rules for capture, fulfilment, refunds, and cancellation.
Every order moves through the same statuses, whichever flow created it.
flowchart LR
A[AwaitingCheckout] -->|customer completes checkout| C[Confirmed]
A -->|decision declined| D[Declined]
A -->|2 hours pass| E[Expired]
A -->|you cancel| X[Cancelled]
C -->|fulfil part| PF[PartiallyFulfilled]
C -->|fulfil all| FF[FullyFulfilled]
PF -->|fulfil the rest| FF
C -->|you cancel| V[Voided]
PF -->|refund| PR[PartiallyReversed]
FF -->|refund| PR
PR -->|refund the rest| R[FullyReversed]
FF -->|refund all paid| R
PF -->|refund all paid| R
FF -->|customer pays every instalment| S[Settled]
Order reads, order lists and checkout-link responses return statuses as names (for example "Confirmed"). The capture, fulfil, refund and cancel responses return status as a number; see Status numbers. The server SDKs always give you the name.
Statuses
| Status | Meaning | How the order gets here | Webhook | What you can do |
|---|---|---|---|---|
AwaitingCheckout | Order created; the customer hasn't finished the hosted checkout | Any create call | None | Cancel (becomes Cancelled) |
Confirmed | The customer signed the contract and paid the first instalment. Safe to ship. | Customer completes checkout | order.confirmed | Fulfil, cancel (becomes Voided) |
PartiallyFulfilled | You recorded shipments for part of the order total | Fulfil with a partial shippedAmount | order.partially_fulfilled | Fulfil the rest, refund |
FullyFulfilled | You recorded shipments for the full order total | Fulfil | order.fulfilled | Refund |
PartiallyReversed | Refunds so far are less than what the customer has paid | Refund | order.partially_refunded | Refund more |
FullyReversed | Everything the customer paid has been refunded; unpaid instalments are cancelled | Refund | order.refunded | None |
Voided | You cancelled a confirmed order before fulfilment; unpaid instalments are cancelled | Cancel a Confirmed order | order.voided | None |
Cancelled | You cancelled the checkout before the customer finished it | Cancel an AwaitingCheckout order | order.cancelled | None |
Declined | The credit decision declined the order, or the customer abandoned it on the confirmation call | Hosted checkout | order.declined | None |
Expired | The customer didn't finish checkout within two hours | Time passes | order.expired, or checkout_link.expired for link and QR orders | None |
Settled | The customer has paid every instalment (on schedule or early). Final | Repayment | None | None |
WrittenOff | Qasimha wrote the loan off after long non-payment. Recovery continues on Qasimha's side | Collections | None | None |
New, Approved | Used only by the earlier provider-based checkout | Earlier checkout | None | Capture (becomes Confirmed) |
Draft, Updated | Not used by the current checkout. You may see Draft in the statusHistory of older orders | — | None | None |
An action that isn't allowed in the order's current status returns 400 INVALID_STATUS, and the message names the current status.
Act on an order
These calls need the orders:write scope and take the Qasimha order id (installmentId from session creation, or id from a checkout link). Send an Idempotency-Key on each one so a retry can't refund or fulfil twice.
| Action | Call | Allowed in | Result |
|---|---|---|---|
| Capture | POST /api/v1/merchant/orders/{id}/capture | New, Approved | Confirmed |
| Fulfil | POST /api/v1/merchant/orders/{id}/fulfill | Confirmed, PartiallyFulfilled | PartiallyFulfilled or FullyFulfilled |
| Refund | POST /api/v1/merchant/orders/{id}/refund | PartiallyFulfilled, FullyFulfilled, PartiallyReversed | PartiallyReversed or FullyReversed |
| Cancel | POST /api/v1/merchant/orders/{id}/cancel | AwaitingCheckout, Confirmed | Cancelled or Voided |
Capture
An order that completes today's hosted checkout is already Confirmed, so you don't need to capture it. Capture applies only to New and Approved orders from the earlier checkout; on a confirmed order it returns 400 INVALID_STATUS. No body is needed.
Response data: { "id": "...", "status": 5, "confirmedAt": "2026-10-20T09:30:00Z" } (5 is Confirmed).
Fulfil
Record a shipment when you ship. You can fulfil in several parts.
{ "shippedAmount": 600.00, "shippingCompany": "Aramex", "trackingNumber": "ARX123", "trackingUrl": "https://track.example/ARX123" }| Field | Type | Required | Meaning |
|---|---|---|---|
shippedAmount | number | No | The value shipped now. Leave it out to fulfil everything still outstanding |
shippingCompany | string | No | Carrier name |
trackingNumber | string | No | Carrier tracking number |
trackingUrl | string | No | Tracking page |
Rules:
- Outstanding =
totalAmount − fulfilledAmount. shippedAmountmust be greater than 0 and not more than the outstanding amount.0, a negative amount, or more than outstanding returns400 INVALID_SHIPPED_AMOUNT. Don't send0to mean "nothing shipped yet"; just don't call fulfil.- Amounts are rounded to two decimals before the check.
- When
fulfilledAmountreachestotalAmount, the order isFullyFulfilled; before that it isPartiallyFulfilled. - You can't fulfil after a refund has been recorded.
Response data: { "id": "...", "status": 7, "fulfilledAmount": 600.00 } (7 is PartiallyFulfilled, 6 is FullyFulfilled).
Refund
Refund a fulfilled order, fully or in parts.
{ "amount": 300.00, "reason": "Customer return" }| Field | Type | Required | Meaning |
|---|---|---|---|
amount | number | Yes | Greater than 0, and not more than refundableAmount |
reason | string | No | Stored with the refund as its comment |
Rules:
- Only
PartiallyFulfilled,FullyFulfilledandPartiallyReversedorders can be refunded. Fulfil the order first. To cancel a confirmed order that hasn't shipped, use cancel instead. - The most you can refund is what the customer has actually paid so far, minus refunds already made. Read it from
refundableAmounton the order. The unpaid balance is never refunded; it is cancelled. (For an order without an instalment schedule, the limit is the fulfilled amount minus refunds.) - When total refunds reach what the customer has paid, the order becomes
FullyReversedand every unpaid instalment is cancelled, so the customer owes nothing more. Below that, the order isPartiallyReversedand the remaining instalments stay due. 0or a negative amount returns400 INVALID_AMOUNT. More thanrefundableAmountreturns400 AMOUNT_EXCEEDED, and themessagestates the available amount.Settledorders (fully repaid) can't be refunded through the API. Contact [email protected].
Example: a 1200.00 order on 4 instalments where the customer has paid the first 300.00 has refundableAmount 300.00. Refunding 300.00 makes it FullyReversed and cancels the other three instalments.
Response data: { "id": "...", "status": 9, "refundedAmount": 100.00 }, where refundedAmount is the running total (9 is PartiallyReversed, 8 is FullyReversed).
Cancel
No body is needed. What cancel does depends on the status:
AwaitingCheckout: the open checkout is withdrawn and the order becomesCancelled. The customer can no longer complete it. (POST /api/v1/merchant/checkout-links/{id}/canceldoes the same for link and QR orders.)Confirmed: the whole order is voided and becomesVoided. Every unpaid instalment is cancelled, andvoidedAmountrecords what was released. There is no partial void.- Any other status:
400 INVALID_STATUS. Once anything is fulfilled, use refund.
Response data: { "id": "...", "status": 10 } (10 is Voided, 13 is Cancelled).
Examples
BASE=https://api-uat.qasimha.com/api/v1/merchant/orders
ORDER=9a2e6c4b-1111-4c2d-8a1f-000000000001
# Fulfil part of the order
curl -X POST $BASE/$ORDER/fulfill -H "X-Api-Key: $QASIMHA_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" -d '{ "shippedAmount": 600.00, "shippingCompany": "Aramex", "trackingNumber": "ARX123" }'
# Fulfil the rest
curl -X POST $BASE/$ORDER/fulfill -H "X-Api-Key: $QASIMHA_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" -d '{}'
# Refund part of what the customer paid
curl -X POST $BASE/$ORDER/refund -H "X-Api-Key: $QASIMHA_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" -d '{ "amount": 100.00, "reason": "Damaged cable returned" }'
# Cancel a confirmed order that hasn't shipped
curl -X POST $BASE/$ORDER/cancel -H "X-Api-Key: $QASIMHA_API_KEY" -H "Idempotency-Key: $(uuidgen)"await qasimha.orders.fulfill(orderId, { shippedAmount: 600.00, shippingCompany: 'Aramex', trackingNumber: 'ARX123' });
await qasimha.orders.fulfill(orderId, {}); // the rest
const order = await qasimha.orders.get(orderId);
if ((order.refundableAmount ?? 0) >= 100.00) {
await qasimha.orders.refund(orderId, { amount: 100.00, reason: 'Damaged cable returned' });
}
await qasimha.orders.cancel(otherOrderId); // Confirmed → Voided, AwaitingCheckout → CancelledStatus numbers
The action responses above return status as one of these numbers. Map it to the name, or read the order again.
| Number | Status | Number | Status |
|---|---|---|---|
0 | Draft | 8 | FullyReversed |
1 | New | 9 | PartiallyReversed |
2 | Approved | 10 | Voided |
3 | Declined | 11 | Updated |
4 | Expired | 12 | AwaitingCheckout |
5 | Confirmed | 13 | Cancelled |
6 | FullyFulfilled | 14 | Settled |
7 | PartiallyFulfilled | 15 | WrittenOff |
Read an order
These calls need the orders:read scope (orders:write also works). An order that belongs to another merchant returns 404 NOT_FOUND.
| Call | Use |
|---|---|
GET /api/v1/merchant/orders/{id} | By Qasimha order id (UUID) |
GET /api/v1/merchant/orders/by-reference/{orderReferenceId} | By your own reference. Exact match, case-sensitive. A reference containing / is sent as the path remainder (/by-reference/ORDER/2026/1); percent-encoding also works |
GET /api/v1/merchant/orders | List, newest first, with status, search, page (default 1) and pageSize (default 20). Rows are in data.data, paging in data.pagination (totalItems, page, pageSize, totalPages) |
GET /api/v1/merchant/orders/counts | Counts for all, approved, authorised, captured, refunded, cancelled |
GET /api/v1/merchant/orders/export.xlsx | Your orders as an Excel file. Takes the same status and search filters |
GET /api/v1/merchant/orders/{id}/contract | The signed financing contract file. 404 if the order has none |
List filters
search matches your order reference, or the customer's name, email or phone. status takes any status name above (case-insensitive) or one of these groups:
status value | Matches |
|---|---|
approved | New, Approved |
authorised / authorized | Confirmed |
captured | PartiallyFulfilled, FullyFulfilled |
refunded | PartiallyReversed, FullyReversed |
cancelled / canceled | Voided, Cancelled |
Any other value returns 400 INVALID_STATUS. A page below 1 or a negative pageSize returns 400 VALIDATION_ERROR.
Order detail
The order detail includes items, the instalment schedule (installments), the customer (id, name, email, phone), fulfillments, refunds (reversals), voids, and the full statusHistory (fromStatus, toStatus, trigger, createdAt). It also has paymentUrl, checkoutExpiresAt, confirmedAt, fulfilledAt, voidedAt, reversedAt, settledAt, contractNumber and ejarContractNumber.
Each instalment has sequence, dueDate, amount, principal, profit, paidAmount, paidAt and a status: Scheduled, Paid, Late, Reversed (cancelled by a void or full refund) or Waived.
Amount fields
| Field | Meaning |
|---|---|
totalAmount | The order total you sent |
capturedAmount, collectedAmount | What the customer has paid so far (order detail) |
fulfilledAmount | What you have shipped |
refundedAmount, reversedAmount | What has been refunded |
refundableAmount | What you can still refund: paid so far minus refunded |
voidedAmount | The unpaid amount released when the order was voided |
In list rows (GET /merchant/orders), capturedAmount is the fulfilled amount and refundedAmount the refunded amount. Read the order detail for the payment figures.
Settlements
GET /api/v1/merchant/settlements lists settlement batches due to you, net of fees, newest first, with page and pageSize. GET /api/v1/merchant/settlements/overview shows the headline figures, including the notPaidYet balance. The net amount is total minus refunds and voids, less the MDR fee and 15% VAT on that fee. Both calls need the settlements:read scope.
Troubleshooting
400 INVALID_STATUS on capture
The order is already Confirmed. Orders from the hosted checkout need no capture. Go straight to fulfil.
400 INVALID_STATUS on refund
Refund needs a fulfilled order (PartiallyFulfilled, FullyFulfilled or PartiallyReversed). If you haven't shipped, cancel instead. If the order is Settled, contact support.
400 AMOUNT_EXCEEDED on refund
You asked for more than the customer has paid minus earlier refunds. Read refundableAmount on the order and refund at most that.
400 INVALID_SHIPPED_AMOUNT on fulfil
shippedAmount was 0, negative, or more than totalAmount − fulfilledAmount. Leave the field out to fulfil the remainder.
The order still shows AwaitingCheckout after two hours
The status moves to Expired shortly after checkoutExpiresAt passes. Reading the link with GET /merchant/checkout-links/{id} updates it immediately. An order whose final step is already in progress is held open until that step finishes.
Related
Updated about 24 hours ago
