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

StatusMeaningHow the order gets hereWebhookWhat you can do
AwaitingCheckoutOrder created; the customer hasn't finished the hosted checkoutAny create callNoneCancel (becomes Cancelled)
ConfirmedThe customer signed the contract and paid the first instalment. Safe to ship.Customer completes checkoutorder.confirmedFulfil, cancel (becomes Voided)
PartiallyFulfilledYou recorded shipments for part of the order totalFulfil with a partial shippedAmountorder.partially_fulfilledFulfil the rest, refund
FullyFulfilledYou recorded shipments for the full order totalFulfilorder.fulfilledRefund
PartiallyReversedRefunds so far are less than what the customer has paidRefundorder.partially_refundedRefund more
FullyReversedEverything the customer paid has been refunded; unpaid instalments are cancelledRefundorder.refundedNone
VoidedYou cancelled a confirmed order before fulfilment; unpaid instalments are cancelledCancel a Confirmed orderorder.voidedNone
CancelledYou cancelled the checkout before the customer finished itCancel an AwaitingCheckout orderorder.cancelledNone
DeclinedThe credit decision declined the order, or the customer abandoned it on the confirmation callHosted checkoutorder.declinedNone
ExpiredThe customer didn't finish checkout within two hoursTime passesorder.expired, or checkout_link.expired for link and QR ordersNone
SettledThe customer has paid every instalment (on schedule or early). FinalRepaymentNoneNone
WrittenOffQasimha wrote the loan off after long non-payment. Recovery continues on Qasimha's sideCollectionsNoneNone
New, ApprovedUsed only by the earlier provider-based checkoutEarlier checkoutNoneCapture (becomes Confirmed)
Draft, UpdatedNot used by the current checkout. You may see Draft in the statusHistory of older orders—NoneNone

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.

ActionCallAllowed inResult
CapturePOST /api/v1/merchant/orders/{id}/captureNew, ApprovedConfirmed
FulfilPOST /api/v1/merchant/orders/{id}/fulfillConfirmed, PartiallyFulfilledPartiallyFulfilled or FullyFulfilled
RefundPOST /api/v1/merchant/orders/{id}/refundPartiallyFulfilled, FullyFulfilled, PartiallyReversedPartiallyReversed or FullyReversed
CancelPOST /api/v1/merchant/orders/{id}/cancelAwaitingCheckout, ConfirmedCancelled 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" }
FieldTypeRequiredMeaning
shippedAmountnumberNoThe value shipped now. Leave it out to fulfil everything still outstanding
shippingCompanystringNoCarrier name
trackingNumberstringNoCarrier tracking number
trackingUrlstringNoTracking page

Rules:

  • Outstanding = totalAmount − fulfilledAmount.
  • shippedAmount must be greater than 0 and not more than the outstanding amount. 0, a negative amount, or more than outstanding returns 400 INVALID_SHIPPED_AMOUNT. Don't send 0 to mean "nothing shipped yet"; just don't call fulfil.
  • Amounts are rounded to two decimals before the check.
  • When fulfilledAmount reaches totalAmount, the order is FullyFulfilled; before that it is PartiallyFulfilled.
  • 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" }
FieldTypeRequiredMeaning
amountnumberYesGreater than 0, and not more than refundableAmount
reasonstringNoStored with the refund as its comment

Rules:

  • Only PartiallyFulfilled, FullyFulfilled and PartiallyReversed orders 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 refundableAmount on 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 FullyReversed and every unpaid instalment is cancelled, so the customer owes nothing more. Below that, the order is PartiallyReversed and the remaining instalments stay due.
  • 0 or a negative amount returns 400 INVALID_AMOUNT. More than refundableAmount returns 400 AMOUNT_EXCEEDED, and the message states the available amount.
  • Settled orders (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 becomes Cancelled. The customer can no longer complete it. (POST /api/v1/merchant/checkout-links/{id}/cancel does the same for link and QR orders.)
  • Confirmed: the whole order is voided and becomes Voided. Every unpaid instalment is cancelled, and voidedAmount records 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)"

Status numbers

The action responses above return status as one of these numbers. Map it to the name, or read the order again.

NumberStatusNumberStatus
0Draft8FullyReversed
1New9PartiallyReversed
2Approved10Voided
3Declined11Updated
4Expired12AwaitingCheckout
5Confirmed13Cancelled
6FullyFulfilled14Settled
7PartiallyFulfilled15WrittenOff

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.

CallUse
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/ordersList, 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/countsCounts for all, approved, authorised, captured, refunded, cancelled
GET /api/v1/merchant/orders/export.xlsxYour orders as an Excel file. Takes the same status and search filters
GET /api/v1/merchant/orders/{id}/contractThe 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 valueMatches
approvedNew, Approved
authorised / authorizedConfirmed
capturedPartiallyFulfilled, FullyFulfilled
refundedPartiallyReversed, FullyReversed
cancelled / canceledVoided, 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

FieldMeaning
totalAmountThe order total you sent
capturedAmount, collectedAmountWhat the customer has paid so far (order detail)
fulfilledAmountWhat you have shipped
refundedAmount, reversedAmountWhat has been refunded
refundableAmountWhat you can still refund: paid so far minus refunded
voidedAmountThe 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


Did this page help you?