Skip to main content

Point orders

Background​

A point order is a gift of points from one client to another, or points a client buys for themselves. The client picks an offer, optionally an occasion, and a recipient, then pays with points or with a card. On Shukah the app does this with POST /maeda/point-order, POST /maeda/point-order/get-description and two payment calls on /maeda/order/checkout-payment-*. Locate has a route for each of those four, plus a new route that reads one order. Three of the four are adapt: create and the two pay routes need app changes. Get-description is drop-in: only the base URL and path change, but its TL;DR has to-watch lines (the ids and the token). Six things need care:

  • The order the app gets back has different keys: the id is a UUID called correlation_id, and the price is money_amount. The pay routes name the order by correlation_id in the path.
  • Points spent on one side are not taken off the other (within seconds of the run; whether it arrives later was not checked). On dev on 2026-10-02 a pay-by-points order cut only the balance of the side that made it, so the app must read the balance and the history on the side where it creates orders.
  • Create accepts only payment_type point or checkout. The app can also send payfort or hyperpay, and Locate answers 400 to both.
  • Pay-by-points and the two pay routes sit behind two server flags. Both were on for dev on 2026-10-01. Production is not confirmed.
  • Create has no idempotency key, and after a 504 the app cannot find the order it may have made.
  • Several of the app's other gift calls have no Locate route: the sent-gifts list, the payment-token call, the STC Pay call and the PayFort calls.

Feature​

Where the calls happen in the app​

In the app snapshot of 2026-02-26 the gift flow is on the send-gift form and the gift checkout. All five Locate routes need a token, and on dev today it must be a Shukah token: they use the same token check as the other point routes, which gave a Locate JWT 401 on 2026-10-02 (see Which tokens work). On 2026-10-01 both tokens still worked on the balance route.

StepCallWhat the app does
1. Compose the messageDescriptionWhile the client fills the form, the app asks for the message the recipient will see. The point service's docs saw it call twice for one order: once with the offer, and once more with the offer and the occasion.
2. Find the recipientClient lookupOn submit, the app looks the recipient up by phone and takes the userId as to_client_id.
3. Create the orderCreateThe app first reloads the common settings. It sends payment_type point when the client pays with points, and otherwise the payment value from those settings: payfort, hyperpay or checkout.
4a. Paid with pointsnoneThe order is already paid when create returns. The app reloads the history, opens the Sent gifts tab and shows "Gift sent successfully".
4b. Saved cardPay with cardThe app sends the saved card's id and the CVV. The reply starts the 3-D Secure step.
4c. New card, Apple Pay or STC PayPay with sessionThe app asks for a payment session and gives it to the Checkout SDK. The payment_type it sends (applePay, stcpay or card in the snapshot) is ignored by Locate. App 2.31.5 sent applePay here on 2026-10-02, and sent STC Pay to Shukah's own checkout-payment-stcpay route, which has no Locate route.
5. After a card paymentnoneOn the SDK's success the app reloads the history and opens the Sent gifts tab.
Not used by any screenGet orderThe app has a call for one Shukah order, but no screen uses it.

When the client settings do not say checkout, the app pays with PayFort or Hyperpay instead, with calls that have no Locate route. The same goes for the app's Frames-token call, its STC Pay call and the CVV-token call to the payment service (see Details).

Lifecycle​

A new order starts as initial_order or, when paid with points, as successful. The statuses are Shukah's own words, kept as they are.

statuspayment_statusMeaningHow it is reached
initial_ordernullCreated, not paid.Create with checkout. The order stays here until the gateway tells the point service the result. An order that nobody pays is left alone.
successfulnullPaid with points. The sender was charged and the recipient credited.Create with point.
successfulCAPTUREDPaid by card. The recipient was credited.The gateway reports a captured payment, usually within seconds of the pay call.
payment_failedFAILEDThe card was declined. This is final: both pay routes now answer 409, so the client has to create a new order.The gateway reports a declined payment.
paidAUTHORIZEDOccur only on orders copied from Shukah. Locate never sets them.Not reachable from the Locate routes.

There is no refresh route. After a pay call the app watches Get order or Point history until the status changes.

Who can do what​

CallWhoEveryone else
CreateThe caller is always the payer. The payer is taken from the token, never from the body.—
Get orderThe payer or the recipient.404 point order not found, the same answer as for a missing order.
Pay with card, pay with sessionThe payer only.404, also for the recipient.
DescriptionAny client. The giver in the sentence is the caller.—

No idempotency key, and what to do after a 504​

Create has no idempotency key. Each call makes a new order with a new correlation_id. A retry after a timeout can therefore make a second order.

A 504 means the outcome is unknown. order-v2's own docs say the app must read the order back instead of retrying blindly. What the code allows (a reading of the code, not something the docs state):

  • Reads, description, pay with session. Safe to retry. A repeated pay-with-session call only makes another session.
  • Pay with card. The app knows the correlation_id. Read the order first and do not call pay with card again until the order is successful or payment_failed. The gateway does not make a payment reference unique, so a second capture is only raised as an alert and not refunded.
  • Create with checkout. The app never got the correlation_id, there is no list route and no idempotency key, so it cannot find the order. A duplicate unpaid order is harmless: orders that are never paid are left alone.
  • Create with point. The same, but a duplicate charges the sender twice if the first call finished. Check the balance and the history before retrying.

How the app should find the order after a create 504: Not documented yet.

Flags​

Two switches in the point service decide whether payment works at all. Both were probed on dev on 2026-10-01 with calls that write nothing.

FlagWhat it controlsDev, probed 2026-10-01ProductionWhat the app sees when it is off
POINT_ORDER_POINT_ENABLEDCreate with payment_type point. checkout is not affected.On. A pay-by-points body to yourself answered 400 a point order cannot be paid with points to yourself. With the flag off it would be 403.Not confirmed.403 {"success":false,"error":{"code":"FORBIDDEN","message":"paying a point order with points is disabled"}}, before any lookup or write.
POINT_ORDER_CARD_ENABLEDBoth pay routes.On. pay/session on a random order id answered 404 point order not found. With the flag off it would be 403.Not confirmed.403 {"success":false,"error":{"code":"FORBIDDEN","message":"card payment for point orders is disabled"}}, before any read: even an unknown order id gets 403.

Older docs (2026-09-21, 09-22 and 09-25) say both flags are unset in every environment. A doc of 2026-09-17 says the precondition for pay-by-points was met on dev only and that production stays unset until its rollout. Those statements are older than the probe, so for dev the probe wins. For production: Not documented yet. The values are set in the point service's environment and baked into its image, so a change needs a new image build.

Timeouts​

order-v2 waits 10 seconds for the point service and 20 seconds on the two pay routes. The app's own limit for order-v2 is 20 seconds, so on the pay routes the app can give up at about the same moment as order-v2's 504. The pay routes go on to the payment service, which has its own 10 second default.

POST /api/v2/client/point-orders​

Replaces ShukahPOST /maeda/point-order✅ Replaced🚩 POINT_ORDER_POINT_ENABLED
Adapt6 breaking · 4 to watch
  • 🔴 No point_order_id: the app builds pointOrderId = String(undefined) = "undefined"; amount is named money_amount
  • 🔴 Also missing: client_id, to_client_id, point_occasion_id, occasion, description, payment_point_order_id
  • 🔴 Pay routes need the UUID correlation_id in the path; keep it from create or pay answers 404
  • 🔴 A spend was not taken off the other side within seconds (later not checked): read balance and history where you order
  • 🔴 payment_type payfort or hyperpay (the app sends the settings value) is 400; only point or checkout work
  • 🔴 point_offer_id and point_occasion_id must be the 24-hex ids from the Locate lists; Shukah integers are 400
  • 🟡 Shukah answers 201, Locate 200; Locate adds correlation_id, order_no, payment_status and order_type
  • 🟡 payment_type point to your own id is 400; checkout self-gift is allowed (Shukah allows both)
  • 🟡 point needs POINT_ORDER_POINT_ENABLED (403 when off); on dev no 403 opens a global error
  • 🟡 No idempotency key; after a 504 the app never learns the order id

Point-service flag for payment_type "point" (403 FORBIDDEN when off). ON on dev, probed 2026-10-01; production not confirmed.

Accepts only payment_type point or checkout (not payfort or hyperpay) and returns a different order shape (correlation_id, money_amount, and so on).

Request​

FieldInTypeRequiredConstraintsDescription
AuthorizationheaderstringyesBearer <token>: a Shukah token on dev today. A Locate JWT gets 401 at the same check as the other point routes (2026-10-02): see Which tokens work.The payer's token.
Content-Typeheaderstringyesapplication/json.The body is JSON.
point_offer_idbodystringyes24 hex characters, checked and never converted. A Shukah integer, a number or "3" is 400.The offer id from the offers list.
to_client_idbodyintegeryesA positive integer. A JSON number or a numeric string is accepted. " 7 " becomes 7 and true becomes 1. "", null, a missing key, "abc" and "12.5" are 400.The recipient's client id, as client lookup returns it.
payment_typebodystringyesExactly point or checkout. card, payfort and hyperpay are 400.point: pay with the payer's points in this call. checkout: make an unpaid order to pay by card.
point_occasion_idbodystring or nullno24 hex characters. A missing key, null, 0, "0" and "" all mean no occasion. Any other non-hex value (" ", 5) is 400.The occasion id from the occasions list.
occasionbodystring or nullnoTrimmed, 1 to 255 characters. "", spaces and null mean none. A non-string or 256 characters is 400.Free text, for a custom occasion.
Any other keybodyanynoDropped.—

A body that is not a JSON object fails with 400 and details of {"formErrors":["Expected object, received null"],"fieldErrors":{}}.

Response​

200 with a bare object (not 201). It has nine keys. The same nine come back from Get order.

FieldTypeNullableFormat / constraintsDescription
correlation_idstringnoA UUID v4, 36 characters.The order id. It goes in the path of the get and pay routes.
order_nointegeryesFirst value 100000. null only on orders copied from Shukah.The human-readable order number.
statusstringnoinitial_order, paid, payment_failed or successful. A new checkout order is initial_order, and a paid-by-points order is successful.The order status.
payment_statusstringyesCAPTURED, FAILED, AUTHORIZED or null. null at creation.The gateway's state.
point_amountnumbernoThe offer's point_amount.The points the recipient gets.
purchased_point_amountnumberyesThe offer's value. null when the offer has no points price.The points the payer is charged when paying with points.
money_amountstringnoExactly two decimals, for example "40.00".The SAR the card is charged.
payment_typestringnopoint or checkout.Echo of the request.
order_typestringnogift when an occasion row was found, otherwise transfer. The free text never decides it.The kind of order.
{
"correlation_id": "2755e9a8-a6b5-4244-9f3d-777efe89ef80",
"order_no": 100010,
"status": "initial_order",
"payment_status": null,
"point_amount": 100,
"purchased_point_amount": 110,
"money_amount": "40.00",
"payment_type": "checkout",
"order_type": "transfer"
}

Captured on dev, 2026-10-01 (client 356259): a checkout order for the first offer, sent to the client's own id, with no occasion. It was never paid and stays on dev, because no route deletes an order.

checkout and point. A checkout order writes one order row and nothing else: no points move and nothing is sent to Shukah or the payment service. A point order settles in the same call. It charges the payer purchased_point_amount, credits the recipient point_amount, and saves the order as successful. All of that happens together or not at all. On the live offers the payer is charged more than the recipient gets, for example 110 points for 100.

Self-gifts. to_client_id may equal the caller for checkout: buying points for yourself is the common case on Shukah (814 of 1,228 orders). point refuses it with 400.

Errors​

Checks run in this order for point: flag, self-payment, offer, recipient, occasion, purchased_point_amount, payer profile, then the charge. For checkout: offer, recipient, occasion, then the order is saved.

HTTPBodyWhen
400{"success":false,"error":{"code":"VALIDATION_ERROR","message":"Invalid point-order body","details":{"formErrors":[],"fieldErrors":{"payment_type":["Invalid enum value. Expected 'point' | 'checkout', received 'card'"]}}}}The body fails the check. The example is payment_type: "card", from a local run of the service's schema, not captured live.
400{"success":false,"error":{"code":"VALIDATION_ERROR","message":"a point order cannot be paid with points to yourself"}}payment_type is point and to_client_id is the caller. Captured on dev, 2026-10-01.
400The same shape, message to_client_id <id> does not exist.The recipient is not a known client. Not captured.
400The same shape, message point occasion <id> not found or inactive.The occasion id is unknown or inactive. Not captured.
400The same shape, message point offer <id> cannot be paid with points: it has no purchased_point_amount.point on an offer with no points price. Not captured.
400The same shape, message client <id> does not exist.The payer has no profile (only reached on point). Not captured.
403{"success":false,"error":{"code":"FORBIDDEN","message":"paying a point order with points is disabled"}}payment_type is point and the flag is off. Checked before any lookup. Not captured, because the flag was on.
404{"success":false,"error":{"code":"NOT_FOUND","message":"point offer <id> not found or inactive"}}The offer is unknown, inactive or deleted. Not captured.
422{"success":false,"error":{"code":"INSUFFICIENT_POINTS","message":"insufficient points for client <id>"}}point, and the balance is below purchased_point_amount. No order is made. Not captured.
401{"success":false,"error":{"code":"UNAUTHORIZED","message":"Missing or invalid authorization header"}}There is no Authorization header, or it does not start with Bearer . Captured on dev, 2026-10-01, with no token.
401The same shape, with message Invalid or expired token, auth service unreachable: <error text> or auth response missing numeric client id.The auth service refused the token, could not be reached (an auth outage looks like a bad token), or answered without a client id. Not captured on this route. The other point routes gave Invalid or expired token to a Locate JWT on 2026-10-02.
500{"success":false,"error":{"code":"INTERNAL_ERROR","message":"Internal server error"}}Any unexpected failure in the point service, for example a database error. The message is AUTH_SERVICE_URL not set when the service is misconfigured. Not captured.
502{"success":false,"error":{"code":"POINT_SERVICE_UNAVAILABLE","message":"Point service unavailable: <error text>"}}order-v2 could not reach the point service. This body comes from order-v2, not from the point service. Not captured.
504{"success":false,"error":{"code":"POINT_SERVICE_TIMEOUT","message":"Point service did not respond in time; the request outcome is unknown"}}The point service did not answer within 10 seconds. From order-v2. The order may already exist: see "No idempotency key" above. Not captured.

Every error from the point service has the envelope {"success":false,"error":{"code":"…","message":"…"}}.

Try it​

This call makes an order on dev that cannot be deleted. Use checkout: point moves points.

export ORDER_BASE_URL=https://dev.v2.api.order.locate.sa
export SHUKAH_TOKEN='<shukah-token>' # a Locate JWT gets 401 here today; see "Which tokens work" on Points wallet
export OFFER_ID='<id-from-the-offers-list>'
export TO_CLIENT_ID='<recipient-client-id>'
curl -s -X POST "$ORDER_BASE_URL/api/v2/client/point-orders" -H "Authorization: Bearer $SHUKAH_TOKEN" -H "Content-Type: application/json" -d "{\"point_offer_id\":\"$OFFER_ID\",\"to_client_id\":\"$TO_CLIENT_ID\",\"payment_type\":\"checkout\"}"

Expected: 200 and the nine-key body shown under Response, with status initial_order (captured on dev, 2026-10-02, with a Shukah token). A Locate JWT gets 401 at the token check (code reading: create was not called with the JWT). With no token you get the 401 shown under Errors.

Differences from Shukah​

Both sides were called on dev on 2026-10-02 (client 356259, Shukah token) with the same offer, recipient and payment_type: once with checkout and once with point, to another dev client. Shukah answered 201 with its order row (the body is on the comparison page). Locate answered 200. The point order on Locate:

{
"correlation_id": "add67555-e1e4-4fac-ae71-9675a98efffe",
"order_no": 100012,
"status": "successful",
"payment_status": null,
"point_amount": 100,
"purchased_point_amount": 110,
"money_amount": "40.00",
"payment_type": "point",
"order_type": "transfer"
}

Captured on dev, 2026-10-02. The checkout order (order_no 100011) has the same nine keys, with status initial_order and payment_type checkout. Both orders stay on dev.

AspectShukahLocateImpact on the app
URLPOST $SHUKAH_BASE_URL/maeda/point-orderPOST $ORDER_BASE_URL/api/v2/client/point-ordersChange the base URL and the path.
TokenShukah opaque token.A Shukah token only on dev today: it worked on create on 2026-10-02. A Locate JWT gets 401 at the point service's token check, as on the other point routes (2026-10-02; not tried on create).The app must keep sending a Shukah token here. For other environments: Not documented yet.
HTTP status201.200 (captured on dev, 2026-10-02).None when the app accepts any 2xx. Code that tests for exactly 201 must change.
payment_typeThe app sends point, or the payment value from the common settings: payfort, hyperpay or checkout.Only point or checkout. payfort and hyperpay are 400.If the settings say payfort or hyperpay, create fails. Whether those gateways are being dropped for point orders: Not documented yet. On Shukah, 1,126 of 1,225 orders used payfort (the point service's doc of 2026-07-29).
Ids in the bodyIntegers as text.point_offer_id and point_occasion_id are 24-character strings. to_client_id is unchanged.Send the ids from the Locate lists. A Shukah integer is 400.
Response keysTwelve keys: point_order_id, payment_point_order_id, client_id, to_client_id, point_amount, purchased_point_amount, amount, status, payment_type, point_occasion_id, occasion, description (create, captured on dev, 2026-10-02; the same keys as Shukah's order rows in the sent-gifts list). The app types occasion_id instead of point_occasion_id, so its occasionId is always undefined.Nine keys: correlation_id, order_no, status, payment_status, point_amount, purchased_point_amount, money_amount, payment_type, order_type.Only point_amount, purchased_point_amount, status and payment_type are on both. amount is money_amount on Locate, and point_order_id, client_id, to_client_id, point_occasion_id, occasion, description and payment_point_order_id are missing. The app reads point_order_id, amount and payment_point_order_id and passes the order id to every pay call, so the order object has to be rebuilt from the new keys.
Order idpoint_order_id: a number (1256 and 1257 from create, 2026-10-02), sent back as a string ("1252") in the app 2.31.5 payment bodies (2026-10-02).A UUID, correlation_id.The pay and get routes take it in the path. Create answers without point_order_id, so the app's pointOrderId becomes the text "undefined" and every pay call gets 404.
Self-giftAllowed.Allowed for checkout, 400 for point.None for checkout.
Status wordsinitial_order, payment_failed, successful, paid (as the app types them).The same words. A Locate order only reaches initial_order, successful and payment_failed.None.
IdempotencyNone.None.The same risk on both.
Error bodyNot documented yet.The point-service envelope under Errors.Anything that matches on error text must change.
Shukah's view of the orderNot applicable.A point order queues two updates for Shukah, but nothing sends that queue on k8s today. A checkout order queues none.Shukah does not see Locate orders until a drainer exists. The Sent gifts tab reads Shukah.
Balance after a point orderOrder 1257 cut the Shukah balance by 110 (514853.64 to 514743.64). The Locate balance did not move.Order 100012, seconds later, cut the Locate balance by 110 (514405.6788 to 514295.6788). The Shukah balance did not move. Dev, 2026-10-02.Breaking. The two ledgers are not synced on a spend, at least not within seconds; whether a spend arrives later was not checked. If the app creates orders on Locate but reads the balance or the history from Shukah (or the reverse), it shows a wrong balance.

GET /api/v2/client/point-orders/:correlation_id​

New endpoint — no Shukah equivalent.

New route. The point service's docs say only "Read-through, no settle". The app has a call for a single Shukah order (GET /maeda/point-order/{id}), but no screen uses it, so this route has no Shukah counterpart in the app's flow. Which Shukah call it replaces, if any: Not documented yet.

Request​

FieldInTypeRequiredConstraintsDescription
AuthorizationheaderstringyesBearer <token>: a Shukah token on dev today. A Locate JWT gets 401 at the same check as the other point routes (2026-10-02): see Which tokens work.The payer's or the recipient's token.
correlation_idpathstringyesAny non-empty string; there is no format check. GET …/point-orders/description is read as the id description.The correlation_id that create returned.
Any query keyquery—noForwarded by order-v2 and ignored by the point service.—

Response​

200 with the same nine keys as the create response. For an order copied from Shukah, order_no is null and status and payment_status carry Shukah's own values.

{
"correlation_id": "2755e9a8-a6b5-4244-9f3d-777efe89ef80",
"order_no": 100010,
"status": "initial_order",
"payment_status": null,
"point_amount": 100,
"purchased_point_amount": 110,
"money_amount": "40.00",
"payment_type": "checkout",
"order_type": "transfer"
}

Captured on dev, 2026-10-01 (client 356259): the order made by the create sample, read right after.

The call reads only. It never settles or refreshes an order: the status changes only when the gateway reports a result. There is no way to list orders, so the app can read only an order whose id it already holds.

Errors​

HTTPBodyWhen
404{"success":false,"error":{"code":"NOT_FOUND","message":"point order not found"}}There is no order with that id for the caller as payer or recipient. A missing order and someone else's order give the same answer. Not captured.
401{"success":false,"error":{"code":"UNAUTHORIZED","message":"Missing or invalid authorization header"}}There is no Authorization header, or it does not start with Bearer . Captured on dev, 2026-10-01, with no token.
401The same shape, with message Invalid or expired token, auth service unreachable: <error text> or auth response missing numeric client id.The auth service refused the token, could not be reached (an auth outage looks like a bad token), or answered without a client id. Not captured on this route. The other point routes gave Invalid or expired token to a Locate JWT on 2026-10-02.
500{"success":false,"error":{"code":"INTERNAL_ERROR","message":"Internal server error"}}Any unexpected failure in the point service, for example a database error. The message is AUTH_SERVICE_URL not set when the service is misconfigured. Not captured.
502{"success":false,"error":{"code":"POINT_SERVICE_UNAVAILABLE","message":"Point service unavailable: <error text>"}}order-v2 could not reach the point service. This body comes from order-v2, not from the point service. Not captured.
504{"success":false,"error":{"code":"POINT_SERVICE_TIMEOUT","message":"Point service did not respond in time; the request outcome is unknown"}}The point service did not answer within 10 seconds. This body comes from order-v2. Not captured.

Try it​

export ORDER_BASE_URL=https://dev.v2.api.order.locate.sa
export SHUKAH_TOKEN='<shukah-token>' # a Locate JWT gets 401 here today; see "Which tokens work" on Points wallet
export CORRELATION_ID='<correlation_id-from-create>'
curl -s "$ORDER_BASE_URL/api/v2/client/point-orders/$CORRELATION_ID" -H "Authorization: Bearer $SHUKAH_TOKEN"

Expected: 200 and the nine-key body shown under Response. An id that is not yours gives the 404 shown under Errors. Use a Shukah token on dev today, as for every point route.

POST /api/v2/client/point-orders/description​

Replaces ShukahPOST /maeda/point-order/get-description✅ Replaced
Drop-in0 breaking · 4 to watch
  • 🟡 Shukah answers {"description":""}, Locate a full sentence; display only, so not breaking
  • 🟡 ids must be Locate 24-hex ids; Shukah "1" and "1" give 400 VALIDATION_ERROR on both fields
  • 🟡 The sentence is always English and uses the caller's name and masked phone; personal data, do not log it
  • 🟡 JWT was not tried on this route; the same check rejects it on every other point route

Request​

FieldInTypeRequiredConstraintsDescription
AuthorizationheaderstringyesBearer <token>: a Shukah token on dev today. A Locate JWT gets 401 at the same check as the other point routes (2026-10-02): see Which tokens work.The giver's token.
Content-Typeheaderstringyesapplication/json.The body is JSON.
point_offer_idbodystringyes24 hex characters. A Shukah integer or a number is 400.The offer id.
point_occasion_idbodystring or nullno24 hex characters. A missing key, null, "", "0" and 0 mean none. Any other non-hex value is 400.The occasion id.
occasionbodystring or nullnoTrimmed, 1 to 255 characters. "", spaces and null mean none. Free text wins: when it is not empty, the occasion id is not looked up at all.Free text, for a custom occasion.

There is no recipient field. The sentence is the recipient's view, built from the caller.

Response​

200 with one key:

FieldTypeNullableFormat / constraintsDescription
descriptionstringnoYou have received a gift {occasion} of {points} points from {giver name} ({masked phone}). See the rules below.The text the app shows while the client composes the gift.
{
"description": "You have received a gift Birthday of 100 points from Test User (*****0000)"
}

Captured on dev, 2026-10-01 (client 356259), with the first offer and the Birthday occasion; the giver's name and phone digits replaced with Test User and *****0000.

The sentence rules:

  • The text is always English. There is no language input.
  • occasion is the free text when it is not empty after trimming. Otherwise it is the occasion's English label when point_occasion_id is given. Otherwise it is empty.
  • With no occasion the sentence keeps a double space: a gift of 400 points. The point service keeps it on purpose, as Shukah does, so the app can switch hosts without changing a string. Do not trim it.
  • points is the offer's point_amount, what the recipient gets, not purchased_point_amount.
  • The giver name is the caller's first and last name joined by a space, or Locate user when both are empty.
  • The masked phone is ***** followed by the last four digits of the caller's phone, or just ***** when there are no digits.
{
"description": "You have received a gift of 400 points from Test User (*****0000)"
}

From the point service's doc locate-point-service@2c44ce3: docs/integrations/2026-07-16-client-facing-point-apis.md (last changed 2026-09-22, lines 397-404, checked there against Shukah dev on 2026-09-21), with the name and digits replaced. It is not a Locate capture: no occasion-less call was made on 2026-10-01.

The sentence holds the caller's real name and the last four digits of the phone. Treat it as personal data when you log it.

Errors​

HTTPBodyWhen
400{"success":false,"error":{"code":"VALIDATION_ERROR","message":"Invalid description body","details":{"formErrors":[],"fieldErrors":{"point_offer_id":["Required"]}}}}The body fails the check. The example is {}, from a local run of the service's schema, not captured live.
400The same shape, message point occasion <id> not found or inactive.There is no free text, and the occasion id is unknown or inactive. Not captured.
400The same shape, message client <id> does not exist.The caller has no profile. Not captured.
404{"success":false,"error":{"code":"NOT_FOUND","message":"point offer <id> not found or inactive"}}The offer is unknown, inactive or deleted. Not captured.
401{"success":false,"error":{"code":"UNAUTHORIZED","message":"Missing or invalid authorization header"}}There is no Authorization header, or it does not start with Bearer . Captured on dev, 2026-10-01, with no token.
401The same shape, with message Invalid or expired token, auth service unreachable: <error text> or auth response missing numeric client id.The auth service refused the token, could not be reached (an auth outage looks like a bad token), or answered without a client id. Not captured on this route. The other point routes gave Invalid or expired token to a Locate JWT on 2026-10-02.
500{"success":false,"error":{"code":"INTERNAL_ERROR","message":"Internal server error"}}Any unexpected failure in the point service, for example a database error. The message is AUTH_SERVICE_URL not set when the service is misconfigured. Not captured.
502{"success":false,"error":{"code":"POINT_SERVICE_UNAVAILABLE","message":"Point service unavailable: <error text>"}}order-v2 could not reach the point service. This body comes from order-v2, not from the point service. Not captured.
504{"success":false,"error":{"code":"POINT_SERVICE_TIMEOUT","message":"Point service did not respond in time; the request outcome is unknown"}}The point service did not answer within 10 seconds. This body comes from order-v2. Not captured.

Try it​

export ORDER_BASE_URL=https://dev.v2.api.order.locate.sa
export SHUKAH_TOKEN='<shukah-token>' # a Locate JWT gets 401 here today; see "Which tokens work" on Points wallet
export OFFER_ID='<id-from-the-offers-list>'
export OCCASION_ID='<id-from-the-occasions-list>'
curl -s -X POST "$ORDER_BASE_URL/api/v2/client/point-orders/description" -H "Authorization: Bearer $SHUKAH_TOKEN" -H "Content-Type: application/json" -d "{\"occasion\":\"\",\"point_offer_id\":\"$OFFER_ID\",\"point_occasion_id\":\"$OCCASION_ID\"}"

Expected: 200 and the description shown under Response, with the caller's own name and phone digits. With no token you get the 401 shown under Errors. Use a Shukah token on dev today: it got 200 on 2026-10-02, and a Locate JWT gets 401 on the other point routes.

Differences from Shukah​

AspectShukahLocateImpact on the app
URLPOST $SHUKAH_BASE_URL/maeda/point-order/get-descriptionPOST $ORDER_BASE_URL/api/v2/client/point-orders/descriptionChange the base URL and the path.
TokenShukah opaque token.A Shukah token (200 on dev, 2026-10-02). A Locate JWT was not tried on this route; the same check gives it 401 on the other point routes.The app must keep sending a Shukah token here.
Ids in the bodyIntegers as text, for example "1".24-character strings. A Shukah integer is 400.Send the ids from the Locate lists.
Answer on dev{"description":""} for every body tried on 2026-10-01 (ids as text and as numbers, and with free text), and again on 2026-10-02.The full sentence (captured on dev, 2026-10-01).With Shukah the message box would be empty today. The point service's doc says both sides matched on 2026-09-21; on 2026-10-01 they did not. Why Shukah returns "" now: Not documented yet.
SentenceSame wording, with the double space when there is no occasion (per the point service's doc, 2026-09-21).Same wording and the same double space.None if the answer is not empty.
Error bodyNot documented yet.The point-service envelope under Errors.Anything that matches on error text must change.

POST /api/v2/client/point-orders/:correlation_id/pay/card​

Replaces ShukahPOST /maeda/order/checkout-payment-standalone✅ Replaced🚩 POINT_ORDER_CARD_ENABLED
Adapt2 breaking · 3 to watch
  • 🔴 Order id moves from body point_order_id (string "1252") to the path as the UUID correlation_id
  • 🔴 The app keeps only pointOrderId; from a Locate create it is "undefined", so the call is 404 "point order not found"
  • 🟡 Body {id, cvv} is the same; point_order_id and payment_type are ignored (pay-point-order-card.ts:7-15)
  • 🟡 cvv null becomes the text "null"; the 3DS answer is payment-api's body, the app type expects 3ds.downgraded
  • 🟡 Needs POINT_ORDER_CARD_ENABLED (403 FORBIDDEN when off); dev Shukah answers 422 "not configured properly"

Point-service flag for the pay routes (403 FORBIDDEN when off). ON on dev, probed 2026-10-01; production not confirmed.

For point orders only: same body (id, cvv), but the order is named in the path by correlation_id, not point_order_id.

This route pays a checkout order with a card the client has saved. Moving off Shukah's checkout-payment-standalone is a URL change and a new order id, not a new body. The same Shukah route is also used for restaurant orders; this page covers only its use for gifts.

Request​

FieldInTypeRequiredConstraintsDescription
AuthorizationheaderstringyesBearer <token>: a Shukah token on dev today. A Locate JWT gets 401 at the same check as the other point routes (2026-10-02): see Which tokens work. The raw token is forwarded to the payment service. Whether the payment service accepts a Locate JWT: Not documented yet.The payer's token. The recipient gets 404.
Content-Typeheaderstringyesapplication/json.The body is JSON.
correlation_idpathstringyesTrimmed. Spaces only gives 400 correlation_id is required.The order's correlation_id.
idbodystringyesTrimmed, at least 1 character. A non-string is 400.The saved card's id (src_…). The app's card.tokenName.
cvvbodystringnoA string or number, trimmed, 1 to 8 characters. "" counts as missing. null becomes the text "null". The gateway needs it for MADA cards. It is forwarded, not stored.The card's security code.
point_order_id, payment_type, any other keybodyanynoAccepted and ignored. The order is named by the path.Kept so the old Shukah body still works.

Response​

200 with the payment service's body, unchanged. It was not captured live: the call goes to the payment gateway and creates a payment. The app starts 3-D Secure from this object. The point service reads nothing from it, and the order stays initial_order until the gateway reports a result.

{
"id": "pay_2c6e…",
"status": "Pending",
"reference": "lp-<correlation_id>",
"3ds": {"enrolled": "Y", "is_redirect": true},
"_links": {"redirect": {"href": "https://…/sessions-interceptor/sid_…"}}
}

From the point service's doc locate-point-service@2c44ce3: docs/integrations/2026-07-16-client-facing-point-apis.md (section 3.6, last changed 2026-09-22), with the long values shortened by the doc itself. Not captured live, because the call creates a payment at the gateway and the plan forbids it.

One response in 902 came back already Declined with no _links.redirect. The app has to handle that case.

Errors​

The checks run in this order: the flag, the order for this payer, the payment type, the kind of order, successful, payment_failed, the payer's profile, then the amount.

HTTPBodyWhen
400{"success":false,"error":{"code":"VALIDATION_ERROR","message":"Invalid pay-by-card body","details":{…}}}The body fails the check, for example no id. Not captured.
400The same shape, message this point order is not paid by card.The order's payment_type is not checkout. Not captured.
400The same shape, message this point order is not payable by card.The order was copied from Shukah, or has no credit id behind it. Not captured.
400The same shape, message client <id> does not exist, or point order <correlation_id> has a non-positive money_amount.The payer has no profile, or the amount is zero or negative. Not captured.
403{"success":false,"error":{"code":"FORBIDDEN","message":"card payment for point orders is disabled"}}POINT_ORDER_CARD_ENABLED is off. Checked first, before any read. Not captured, because the flag was on.
404{"success":false,"error":{"code":"NOT_FOUND","message":"point order not found"}}No such order for the caller as payer. The recipient gets this too. Not captured.
409{"success":false,"error":{"code":"CONFLICT","message":"point order is already paid"}}The order is successful. Not captured.
409The same shape, message point order payment failed; create a new order.The order is payment_failed. A decline is final. Not captured.
The payment service's statusThe payment service's own body, unchanged. For example 422 source_id_invalid for a dead saved card, or 400 for a missing MADA CVV.The payment service answered with a 4xx. No payment exists and the order stays payable. Not captured.
500{"success":false,"error":{"code":"INTERNAL_ERROR","message":"Internal server error"}}, or message PAYMENT_API_URL not set.The payment service could not be reached, did not answer in 10 seconds, or answered 5xx: nothing is recorded. Not captured.
401{"success":false,"error":{"code":"UNAUTHORIZED","message":"Missing or invalid authorization header"}}There is no Authorization header, or it does not start with Bearer . Captured on dev, 2026-10-01, with no token.
401The same shape, with message Invalid or expired token, auth service unreachable: <error text> or auth response missing numeric client id.The auth service refused the token, could not be reached (an auth outage looks like a bad token), or answered without a client id. Not captured on this route. The other point routes gave Invalid or expired token to a Locate JWT on 2026-10-02.
502{"success":false,"error":{"code":"POINT_SERVICE_UNAVAILABLE","message":"Point service unavailable: <error text>"}}order-v2 could not reach the point service. This body comes from order-v2, not from the point service. Not captured.
504{"success":false,"error":{"code":"POINT_SERVICE_TIMEOUT","message":"Point service did not respond in time; the request outcome is unknown"}}The point service did not answer within 20 seconds. From order-v2. The payment may already exist at the gateway: read the order, do not call again. Not captured.

Try it​

This call goes to the payment gateway and can charge a card. Run it only on dev, with a test card and an order you made yourself.

export ORDER_BASE_URL=https://dev.v2.api.order.locate.sa
export SHUKAH_TOKEN='<shukah-token>' # a Locate JWT gets 401 here today; see "Which tokens work" on Points wallet
export CORRELATION_ID='<correlation_id-from-create>'
export CARD_ID='<saved-card-id>'
curl -s -X POST "$ORDER_BASE_URL/api/v2/client/point-orders/$CORRELATION_ID/pay/card" -H "Authorization: Bearer $SHUKAH_TOKEN" -H "Content-Type: application/json" -d "{\"id\":\"$CARD_ID\"}"

Expected: 200 and the payment object shown under Response. The order stays initial_order until the gateway reports the result. Use a Shukah token on dev today, as for every point route.

Differences from Shukah​

AspectShukahLocateImpact on the app
URLPOST $SHUKAH_BASE_URL/maeda/order/checkout-payment-standalonePOST $ORDER_BASE_URL/api/v2/client/point-orders/{correlation_id}/pay/cardChange the base URL and the path, and put the order's correlation_id in it.
Body{id, cvv, point_order_id}.{id, cvv}. point_order_id and payment_type are accepted and ignored.None: the same names.
Order idpoint_order_id in the body, a string such as "1252" (app 2.31.5 payment bodies, captured on dev, 2026-10-02).A UUID, correlation_id, in the path.The app keeps only pointOrderId. It has to keep the new id from create.
TokenShukah opaque token.A Shukah token only on dev today: a Locate JWT gets 401 at the same check as the other point routes (2026-10-02). The raw token also goes to the payment service.The app must keep sending a Shukah token here.
Shukah on dev422 {"name":"Unprocessable Content","message":"Checkout payment is not configured properly.",…} for point-order payments (2026-10-02).Not called live.Dev Shukah cannot take a point payment today, so the two sides could not be compared live.
FlagNone.POINT_ORDER_CARD_ENABLED: on for dev (probed 2026-10-01), production not confirmed.If the flag is off, every call is 403.
Payment errorsThe payment service's own 4xx.Passed through unchanged with the same status and body.None: the app already handles that shape.
ResponseThe app's type has 3ds.downgraded.The doc's sample has 3ds.is_redirect, and a declined payment can come back with no _links.redirect, which the app's type requires.Check the 3-D Secure start against a real response.
Other gift paymentsThe app also uses checkout-payment, checkout-payment-stcpay, PayFort calls and a CVV-token call.No route for them. Only this one and pay with session exist.See "Details" for the calls that are not replaced.
Error bodyNot documented yet.The point-service envelope under Errors.Anything that matches on error text must change.

POST /api/v2/client/point-orders/:correlation_id/pay/session​

Replaces ShukahPOST /maeda/order/checkout-payment-flow✅ Replaced🚩 POINT_ORDER_CARD_ENABLED
Adapt2 breaking · 3 to watch
  • 🔴 Order id moves from body point_order_id (string "1253") to the path as the UUID correlation_id
  • 🔴 The app keeps only pointOrderId; from a Locate create it is "undefined", so the call is 404
  • 🟡 payment_type "applePay" and point_order_id are ignored; the Checkout session offers the wallets
  • 🟡 Body is optional ({rememberMe}); a string "true" or "false" is coerced, a number is 400
  • 🟡 Needs POINT_ORDER_CARD_ENABLED; dev Shukah answers 422 "Checkout payment is not configured properly."

Point-service flag for the pay routes (403 FORBIDDEN when off). ON on dev, probed 2026-10-01; production not confirmed.

For point orders only: same body (rememberMe); point_order_id and payment_type are ignored and the order is named by correlation_id in the path.

This route starts a payment session for a checkout order. The Checkout SDK uses it for a new card, Apple Pay, Google Pay and STC Pay. It charges nothing by itself. The same Shukah route is also used for restaurant orders; this page covers only its use for gifts.

Request​

FieldInTypeRequiredConstraintsDescription
AuthorizationheaderstringyesBearer <token>: a Shukah token on dev today. A Locate JWT gets 401 at the same check as the other point routes (2026-10-02): see Which tokens work.The payer's token. The recipient gets 404.
Content-Typeheaderstringnoapplication/json. An empty or missing body is valid and means {}.The body is JSON.
correlation_idpathstringyesTrimmed. Spaces only gives 400 correlation_id is required.The order's correlation_id.
rememberMebodybooleannotrue, false, or the text "true" or "false". Default false. A number, null or any other text is 400.Save the card, filed under the payer, after a successful payment.
point_order_id, payment_type, any other keybodyanynoAccepted and ignored.Kept so the old Shukah body still works.

Response​

200 with the payment service's session, unchanged. It was not captured live: the call goes to the payment gateway. The Checkout SDK starts from this object. A session can be abandoned and made again on the same order; nine on one reference were seen, and none is an error.

{
"id": "ps_3J5G…",
"payment_session_secret": "<redacted-secret>",
"payment_session_token": "<redacted-token>",
"_links": {"self": {"href": "…"}}
}

From the point service's doc locate-point-service@2c44ce3: docs/plans/2026-09-21-point-order-manual-test-guide.md (lines 336-343, last changed 2026-09-22), with the secret and the token replaced. Not captured live, because the call opens a payment session at the gateway and the plan forbids it.

Errors​

The same guards as Pay with card, in the same order. The rows are listed in full.

HTTPBodyWhen
400{"success":false,"error":{"code":"VALIDATION_ERROR","message":"Invalid pay-by-session body","details":{…}}}rememberMe is not a boolean-like value. Not captured.
400The same shape, message this point order is not paid by card or this point order is not payable by card.The order is not a checkout order that Locate made. Not captured.
400The same shape, message client <id> does not exist, or point order <correlation_id> has a non-positive money_amount.The payer has no profile, or the amount is zero or negative. Not captured.
403{"success":false,"error":{"code":"FORBIDDEN","message":"card payment for point orders is disabled"}}POINT_ORDER_CARD_ENABLED is off. Checked first, before any read. Not captured, because the flag was on.
404{"success":false,"error":{"code":"NOT_FOUND","message":"point order not found"}}No such order for the caller as payer. Captured on dev, 2026-10-01, with a random order id (this is how the flag was probed).
409{"success":false,"error":{"code":"CONFLICT","message":"point order is already paid"}}, or message point order payment failed; create a new order.The order is successful, or it is payment_failed. Not captured.
The payment service's statusThe payment service's own body, unchanged.The payment service answered with a 4xx. Not captured.
500{"success":false,"error":{"code":"INTERNAL_ERROR","message":"Internal server error"}}, or message PAYMENT_API_URL not set.The payment service could not be reached, did not answer in 10 seconds, or answered 5xx. Not captured.
401{"success":false,"error":{"code":"UNAUTHORIZED","message":"Missing or invalid authorization header"}}There is no Authorization header, or it does not start with Bearer . Captured on dev, 2026-10-01, with no token.
401The same shape, with message Invalid or expired token, auth service unreachable: <error text> or auth response missing numeric client id.The auth service refused the token, could not be reached (an auth outage looks like a bad token), or answered without a client id. Not captured on this route. The other point routes gave Invalid or expired token to a Locate JWT on 2026-10-02.
502{"success":false,"error":{"code":"POINT_SERVICE_UNAVAILABLE","message":"Point service unavailable: <error text>"}}order-v2 could not reach the point service. This body comes from order-v2, not from the point service. Not captured.
504{"success":false,"error":{"code":"POINT_SERVICE_TIMEOUT","message":"Point service did not respond in time; the request outcome is unknown"}}The point service did not answer within 20 seconds. From order-v2. A 504 here is safe to retry: a repeated call only makes another session. Not captured.

Try it​

This call opens a payment session at the gateway. Run it only on dev, with an order you made yourself.

export ORDER_BASE_URL=https://dev.v2.api.order.locate.sa
export SHUKAH_TOKEN='<shukah-token>' # a Locate JWT gets 401 here today; see "Which tokens work" on Points wallet
export CORRELATION_ID='<correlation_id-from-create>'
curl -s -X POST "$ORDER_BASE_URL/api/v2/client/point-orders/$CORRELATION_ID/pay/session" -H "Authorization: Bearer $SHUKAH_TOKEN" -H "Content-Type: application/json" -d '{"rememberMe":false}'

Expected: 200 and the session object shown under Response. A random correlation_id gives the 404 shown under Errors (with the flag on). Use a Shukah token on dev today, as for every point route.

Differences from Shukah​

AspectShukahLocateImpact on the app
URLPOST $SHUKAH_BASE_URL/maeda/order/checkout-payment-flowPOST $ORDER_BASE_URL/api/v2/client/point-orders/{correlation_id}/pay/sessionChange the base URL and the path, and put the order's correlation_id in it.
Body{point_order_id, payment_type, rememberMe}.{rememberMe}. point_order_id and payment_type are accepted and ignored.None: the same names.
payment_typeApp 2.31.5 sent applePay (captured on dev, 2026-10-02). The snapshot's type also allows stcpay and card, but app 2.31.5 sends STC Pay to Shukah's own checkout-payment-stcpay route.Ignored. The Checkout session offers the wallets.The wallet is not chosen on the server. Check the SDK shows the right wallet. STC Pay has no Locate route of its own.
Order idA string in the body: {"point_order_id":"1253",…} in the app 2.31.5 capture (2026-10-02).A UUID, in the path.The app has to keep the correlation_id from create.
Shukah on devApp 2.31.5 called it on dev on 2026-10-02 and got 422 Checkout payment is not configured properly.Not called live.Dev Shukah cannot take a point payment today, so the two sides could not be compared live.
FlagNone.POINT_ORDER_CARD_ENABLED: on for dev (probed 2026-10-01), production not confirmed.If the flag is off, every call is 403.
ResponseNot documented yet (not captured).The payment service's session, unchanged.The app's type has id, payment_session_secret, payment_session_token and _links.self, the same keys as the doc's sample.
Error bodyNot documented yet.The point-service envelope under Errors.Anything that matches on error text must change.

Watch out​

  • The order has new keys (correlation_id, money_amount) and the pay routes take correlation_id in the path, so the app must keep it from create.
  • Create has no idempotency key: after a 504 the app never learns the order id, and a retry can make a second order or, for point, charge twice.
  • Pay-by-points and both pay routes need two flags. Both were on for dev on 2026-10-01; production is not confirmed, and an off flag gives 403.

Details (for backend)​

Only dev was checked, through order-v2 at https://dev.v2.api.order.locate.sa, prefix /api/v2/client. UAT and production hosts: Not documented yet. In this section OV2: means locate-order-v2-api@d9e4e5f6 and PS: means locate-point-service@2c44ce3. How the proxy, the token check and the error envelope work is described once, under How the proxied calls work.

Routes and proxy​

App route (order-v2)Upstream route (point service)ForwardedLimit
POST /api/v2/client/point-ordersPOST /api/client/point-ordersAuthorization and the body as received10 s
GET /api/v2/client/point-orders/:correlation_idGET /api/client/point-orders/<id>, with the id passed through encodeURIComponentAuthorization and the whole query (ignored upstream)10 s
POST /api/v2/client/point-orders/descriptionPOST /api/client/point-orders/descriptionAuthorization and the body10 s
POST /api/v2/client/point-orders/:correlation_id/pay/cardPOST /api/client/point-orders/<id>/pay/cardAuthorization and the body20 s
POST /api/v2/client/point-orders/:correlation_id/pay/sessionPOST /api/client/point-orders/<id>/pay/sessionAuthorization and the body20 s

(OV2: src/modules/client/point/point.controller.ts:10,110-201, PS: services/api/src/routes/client.routes.ts:23-29.) The literal route POST point-orders/description is mounted before GET point-orders/:correlation_id, so a stray GET …/point-orders/description is read as the order id description and answers 404 point order not found (PS: services/api/src/routes/client.routes.ts:24-27, PS: services/api/src/routes/client.routes.test.ts:43-75). The description route keeps the default 10 seconds, because it only composes text (OV2: src/modules/client/point/point.controller.ts:126-131). A POST keeps the upstream status: a 200 stays 200, not Nest's default 201 (seen in a local reproduction of the proxy pattern; OV2: src/modules/client/point/point.service.ts:64). The point service's internal routes, including the webhook and …/refresh, are never proxied (OV2: docs/plans/2026-09-16-point-order-proxy-plan.md:21, OV2: docs/locate-point-client-api-reference.md:46).

Create​

  • The handler checks the body with a schema, then createPointOrder runs the checks in the order given under Errors (PS: services/api/src/handlers/create-point-order.ts:10-47, PS: packages/core/src/points/create-point-order.ts:66-186). Both ids are 24-hex strings and are never converted (PS: services/api/src/lib/object-id.ts:12-14). The recipient must exist in the shared clients collection (PS: packages/core/src/points/client-lookup.ts:20-35).
  • checkout: one order insert and a counter increment for order_no. No ledger row and no outbox row (PS: packages/core/src/points/create-point-order.ts:162-172). The order stays unpaid and is left alone if abandoned (PS: docs/plans/2026-09-10-point-order-write-path-design.md:692).
  • point: one transaction writes the payer's expense, the recipient's earn, the order (successful, payment_status null) and two outbox rows addressed to Shukah (mirror_reduce_point, mirror_add_point). The expense runs first, so the overdraft guard is the balance check and nothing is left behind on failure (PS: packages/core/src/points/create-point-order.ts:193-258, PS: packages/core/src/points/apply-point-txn.ts:71-86). Nothing drains the outbox on k8s today, so Shukah does not see these legs until a drainer exists (PS: CLAUDE.md:202-205, PS: .env.example:95-97, PS: docs/plans/2026-09-10-point-order-write-path-design.md:811). The live calls of 2026-10-02 fit this: a point order on Locate did not change the Shukah balance, and one on Shukah did not change the Locate balance, within the seconds of the run. Whether either side catches up later was not checked.
  • Markup. On dev the payer pays 110 for 100, then 240, 360, 480 and 600. The design doc's "10 % markup" example is the first offer, and the others carry 20 % (PS: docs/plans/2026-08-03-point-order-flow-design.md:31).
  • What the order stores. An offer_snapshot of the amounts, the occasion label from the occasion row, the sender-side English description, and three fixed ids (expense_correlation_id, earn_correlation_id, transfer_id) (PS: packages/core/src/points/create-point-order.ts:119-160). order_no starts at 100000 from a counter, and correlation_id is a UUID v4 (PS: packages/core/src/points/order-no.ts:12,20-28, PS: packages/core/src/points/correlation-id.ts:8-10).
  • Free text. occasion goes into the stored description and, for point, into both ledger legs. It is not stored on the order as a field. For a checkout order credited later by the gateway hook, the recipient's text is built from the occasion row's English label, not from the client's free text. That last point is read from the code and no doc states it (PS: packages/core/src/points/create-point-order.ts:119-125,190, PS: packages/core/src/points/settle-point-order.ts:231).
  • order_type is gift when an occasion row was found (by its Shukah integer or its own id) and transfer otherwise (PS: packages/core/src/points/derive-order-type.ts:16-34). The point service's doc sample shows gift for a body with an occasion. The live checkout call had no occasion, so it is transfer.
  • No idempotency key. Every call allocates a new correlation_id and order_no (PS: packages/core/src/points/create-point-order.ts:109-113,165; PS: docs/integrations/2026-07-16-client-facing-point-apis.md:237-238).

Settlement and the gateway hook​

Statuses are Shukah's mt_point_order values kept as they are (PS: docs/plans/2026-07-29-point-orders-verbatim-shukah-values.md:19-34, PS: packages/core/src/db/models/point-order.model.ts:25,30-31). Locate-made orders reach only initial_order, successful and payment_failed (PS: packages/core/src/points/settle-point-order.ts:183-185). The gateway reports results to POST /internal/point-orders/webhooks/checkout, which is not an app route and needs a service key (PS: services/api/src/handlers/checkout-webhook.ts:9-41).

  • A captured payment whose amount and currency match credits the recipient one earn leg and writes one outbox row, and sets successful and CAPTURED. A payer expense is not written (PS: packages/core/src/points/settle-point-order.ts:117-119,195-328).
  • A declined payment on an initial_order sets payment_failed and FAILED (PS: packages/core/src/points/settle-point-order.ts:33-42,125-145).
  • A capture on a payment_failed order stays failed, and a refund is requested (PS: packages/core/src/points/settle-point-order.ts:121-123,331-392).
  • An amount or currency mismatch leaves the order initial_order, marks it halted and raises an alert. It is never settled automatically (PS: packages/core/src/points/settle-point-order.ts:201-224).
  • Settlement is safe to repeat: the credit is claimed by flipping the order out of initial_order (PS: packages/core/src/points/settle-point-order.ts:50-55).

The two pay routes​

  • Both start with loadPayableOrder: the flag, then an order lookup that includes the payer's id (so a missing order and someone else's give the same 404), then the payment type, the merchant check, successful, payment_failed, the payer's profile and the amount (PS: packages/core/src/points/pay-point-order.ts:42-102).
  • The payment service is the payment-api repo; it was not read for this page.
  • Card. The outbound call is POST {PAYMENT_API_URL}/client/api/v1/checkout/lp-<correlation_id>/payments/standalone. Session. It is POST {PAYMENT_API_URL}/client/api/v1/checkout/payment/lp-<correlation_id>/session. Both send the headers token (the client's raw token), x-from-application: LOCATE_CLIENT and x-transaction-type: POINT. The body has amount in SAR, customerId (the payer), customerName, customerPhone, and id and cvv (card) or rememberMe (session). The payer is the customer, never the recipient. The gateway reference is lp-<correlation_id> (PS: packages/core/src/points/payment-api-client.ts:125-164, PS: packages/core/src/points/pay-point-order.ts:29-33,134-174).
  • The only write is a debugging note (payment_provider and payment_reference), written on a best effort basis. No ledger write and no status change happen here (PS: packages/core/src/points/pay-point-order.ts:104-130).
  • The cvv is forwarded and never stored or logged by the point service (PS: services/api/src/middleware/request-logger.ts:5). order-v2 logs request bodies without redaction, including this one (see the proxy section).
  • Errors from the payment service. A 4xx is passed through with its own status and body. A transport failure, a timeout or a 5xx is not caught and becomes INTERNAL_ERROR 500, with nothing recorded (PS: services/api/src/handlers/pay-point-order-card.ts:38-44, PS: services/api/src/handlers/pay-point-order-session.ts:38-40, PS: packages/core/src/points/payment-api-client.ts:22-30,70-74,86-92,113-118, PS: docs/plans/2026-09-10-point-order-write-path-design.md:684).
  • Timeout chain: the app 20 s, order-v2 20 s, the payment service 10 s by default (PAYMENT_API_TIMEOUT_MS) (mobile/locate-client@e960c3a: src/shared/api/instances/orderApiV2.ts:16, PS: packages/core/src/points/payment-api-client.ts:7-20).
  • A repeated pay with card asks the payment service for a new payment on the same reference. The gateway does not make a reference unique, so a second capture is only alerted (PS: packages/core/src/points/settle-point-order.ts:169-181, PS: docs/plans/2026-09-21-point-order-manual-test-guide.md:446).
  • Whether the payment service accepts a Locate JWT in the token header: Not documented yet.

The flags​

POINT_ORDER_POINT_ENABLED is read as process.env.POINT_ORDER_POINT_ENABLED !== 'true' and POINT_ORDER_CARD_ENABLED the same way, so only the exact text true turns them on (PS: packages/core/src/points/create-point-order.ts:69-72, PS: packages/core/src/points/pay-point-order.ts:45-47). The flag values are baked into the image: the build copies /data/compose/point-api/.env into it (PS: Jenkinsfile-k8s:25-28,115-122, PS: Dockerfile:22-23). The probe on 2026-10-01 used two calls that write nothing: a point body with an all-zero offer id and the caller as recipient (the flag check and the self-payment check both run before any lookup), and a pay/session call on a random UUID (the flag check runs before the order lookup, and an empty body is valid) (PS: packages/core/src/points/create-point-order.ts:69-75, PS: packages/core/src/points/pay-point-order.ts:44-54, PS: services/api/src/handlers/pay-point-order-session.ts:22-24). The docs' claims about the state of the flags are in PS: .env.example:74-80,91-94, PS: CLAUDE.md:192-196, PS: docs/plans/2026-09-17-shukah-sync-idempotent-key.md:35-38, PS: docs/plans/2026-09-21-point-order-session-handoff.md:17, PS: docs/plans/2026-09-22-o16-objectid-identity-design.md:286 and PS: docs/integrations/2026-09-25-payment-api-hook-forwarding.md:199,748-753.

Description​

The route composes text and writes nothing. It looks up the offer (status: 'active', deleted_at: null), then the occasion only when there is no free text, then the caller's profile (PS: services/api/src/services/point-settings.service.ts:44-79). The wording is composeGiftDescription, which keeps Shukah's double space on purpose (PS: services/api/src/services/point-settings.mappers.ts:60-78). The ledger and order texts that are built when an order is created collapse that gap to one space, and do not carry the double space: those are a different family of sentences (PS: packages/core/src/points/order-reason.ts:10-28). The name and the phone mask follow displayName and maskPhone (PS: packages/core/src/points/order-reason.ts:10-18). The point service doc records that both bodies, with and without an occasion, matched Shukah on 2026-09-21 (PS: docs/integrations/2026-07-16-client-facing-point-apis.md:393-404). The 2026-09-21 manual-test guide says the proxy route for it was not yet merged; it is merged at d9e4e5f6 (PS: docs/plans/2026-09-21-point-order-manual-test-guide.md:444, OV2: src/modules/client/point/point.controller.ts:132-146).

App calls with no Locate route​

  • Sent gifts list. The app's "Sent gifts" tab reads GET /maeda/point-order (with page and per-page, and the x-pagination-* headers). Neither the point service nor order-v2 has a list route for point orders, and the migration registry marks it not replaced. The gift checkout opens that tab after paying, so a Locate-made order will not show there (mobile/locate-client@e960c3a: src/entities/points/model/api/services/points.service.ts:170-188, mobile/locate-client@e960c3a: src/features/SendedPointGifts/model/hooks/useSendedPointGifts.ts:20, mobile/locate-client@e960c3a: src/entities/points/model/api/controllers/points.controller.ts:151-152).
  • Other payment calls. order/checkout-payment (the Frames token), order/checkout-payment-stcpay, the PayFort and Hyperpay paths, and the cvv/token call to the payment service with the Shukah order id have no Locate counterpart. Locate offers a saved-card route and a session route; STC Pay is a wallet inside the session. The cvv/token call has no effect on a Locate order. Which Locate calls the app should use instead: Not documented yet (mobile/locate-client@e960c3a: src/widgets/PointGiftsCheckout/model/hooks/usePointGiftsCheckout.ts:80-120,288-352, PS: docs/plans/2026-09-10-point-order-write-path-design.md:382-398).
  • Single order. The app has getPointGiftOrderById for GET /maeda/point-order/{id}, but no screen calls it (mobile/locate-client@e960c3a: src/entities/points/model/api/services/points.service.ts:316-341, mobile/locate-client@e960c3a: src/entities/points/index.ts:21-22).

The app​

createPointGiftOrder reloads the common settings and sends payment_type point for a points payment, or the payment value from those settings for an online one (mobile/locate-client@e960c3a: src/entities/points/model/api/rtk/pointsApi.ts:408-421, mobile/locate-client@e960c3a: src/entities/points/model/api/dto/points.dto.ts:356-397). The order DTO turns the response into the app's order: it builds pointOrderId as String(point_order_id), reads amount, client_id, to_client_id and payment_point_order_id, and maps status through a table of Shukah's four words (mobile/locate-client@e960c3a: src/entities/points/model/api/dto/points.dto.ts:23-31,138-183). All the pay calls send order.pointOrderId (mobile/locate-client@e960c3a: src/widgets/PointGiftsCheckout/model/hooks/usePointGiftsCheckout.ts:51,80-120). The checkout looks the recipient up by phone and passes userId as to_client_id, then creates the order and pays; the success handler reloads the history and replaces the screen with the Sent gifts tab (mobile/locate-client@e960c3a: src/widgets/PointGiftsCheckout/model/hooks/usePointGiftsCheckout.ts:221-232,370-401).

Where the docs and the code disagree (code wins)​

  • The docs of 2026-09-21, 09-22 and 09-25 say both flags are unset in every environment. The probe of 2026-10-01 shows both on for dev. The doc of 2026-09-25 says nobody read the flag on a running host (PS: docs/integrations/2026-09-25-payment-api-hook-forwarding.md:748-753).
  • The 2026-09-21 manual-test guide says the description proxy route is not merged and that dev order-v2 calls localhost:4010. Both are stale (PS: docs/plans/2026-09-21-point-order-manual-test-guide.md:24-32,444).
  • The order-v2 reference doc says an unreachable auth service gives 500. The point service answers 401 (OV2: docs/locate-point-client-api-reference.md:201, PS: services/api/src/services/client-auth.service.ts:26-28).
  • The integration doc calls create "new capability, not a Shukah read-parity endpoint", and also says a retried POST matches "Shukah's own order-creation endpoint" (PS: docs/integrations/2026-07-16-client-facing-point-apis.md:178-182,237-238).
  • 2026-10-02: this page said a Shukah token and a Locate JWT both worked on these routes. They share the point service's token check, which gave a Locate JWT 401 on the other point routes on 2026-10-02 (PS: services/api/src/services/client-auth.service.ts:22-29). The Try it calls now use a Shukah token.
  • 2026-10-02: this page said Shukah's order carries occasion_id, as the app types it. Shukah's order rows have point_occasion_id (the sent-gifts list, captured on dev, 2026-10-02), so the app's occasionId is always undefined (mobile/locate-client@e960c3a: src/entities/points/model/api/dto/points.dto.ts:138-183).
  • 2026-10-02: this page said the pay calls send point_order_id as an integer. App 2.31.5 sends a string ("1252", "1253"; proxy captures on dev, 2026-10-02). Locate ignores it either way (PS: services/api/src/handlers/pay-point-order-card.ts:7-15, PS: services/api/src/handlers/pay-point-order-session.ts:9-16).
  • 2026-10-02: this page built the Shukah side of create from the app's types and the point service's docs, because create had not been called. Both sides were called on dev on 2026-10-02: Shukah answers 201 with twelve keys, Locate 200 with nine, and a point order on either side cut only that side's balance (captures point-order-create-card--shukah, point-order-create-card--locate-st, point-order-create-point--shukah, point-order-create-point--locate-st).

Sources​

  • locate-order-v2-api@d9e4e5f6: src/modules/client/point/point.controller.ts:6-10,110-201, src/modules/client/point/point.service.ts:64, src/main.ts:38
  • locate-order-v2-api@d9e4e5f6 docs: docs/locate-point-client-api-reference.md:46,48,201, docs/plans/2026-09-16-point-order-proxy-plan.md:21
  • locate-point-service@2c44ce3: services/api/src/routes/client.routes.ts:23-29, services/api/src/routes/client.routes.test.ts:43-75, services/api/src/handlers/create-point-order.ts:10-47, services/api/src/handlers/get-point-order.ts:6-12, services/api/src/handlers/point-order-description.ts:8-38, services/api/src/handlers/pay-point-order-card.ts:7-46, services/api/src/handlers/pay-point-order-session.ts:7-43, services/api/src/handlers/checkout-webhook.ts:9-41, services/api/src/services/point-orders.service.ts:3-15, services/api/src/services/point-settings.service.ts:44-79, services/api/src/services/point-settings.mappers.ts:60-78, services/api/src/lib/object-id.ts:12-14, services/api/src/services/client-auth.service.ts:26-28, services/api/src/middleware/request-logger.ts:5, packages/core/src/points/create-point-order.ts:66-268, packages/core/src/points/pay-point-order.ts:29-174, packages/core/src/points/payment-api-client.ts:7-30,70-74,86-92,113-164, packages/core/src/points/settle-point-order.ts:33-55,117-145,169-392, packages/core/src/points/apply-point-txn.ts:71-86, packages/core/src/points/derive-order-type.ts:16-34, packages/core/src/points/order-no.ts:12,20-28, packages/core/src/points/correlation-id.ts:8-10, packages/core/src/points/client-lookup.ts:20-35, packages/core/src/points/order-reason.ts:10-28, packages/core/src/db/models/point-order.model.ts:25,30-31, .env.example:74-97, CLAUDE.md:192-205, Jenkinsfile-k8s:25-28,115-122, Dockerfile:22-23
  • locate-point-service@2c44ce3 docs: docs/integrations/2026-07-16-client-facing-point-apis.md:178-182,237-238,263-299,393-404 (last changed 2026-09-22), docs/plans/2026-09-21-point-order-manual-test-guide.md:24-32,336-343,444,446 (last changed 2026-09-22), docs/plans/2026-09-10-point-order-write-path-design.md:382-398,513-518,684,692,811, docs/plans/2026-08-03-point-order-flow-design.md:31,33, docs/plans/2026-07-29-point-orders-verbatim-shukah-values.md:19-34, docs/plans/2026-09-17-shukah-sync-idempotent-key.md:35-38, docs/plans/2026-09-21-point-order-session-handoff.md:17, docs/plans/2026-09-22-o16-objectid-identity-design.md:286, docs/integrations/2026-09-25-payment-api-hook-forwarding.md:199,748-753
  • Mobile snapshot mobile/locate-client@e960c3a (branch development, 2026-02-26; the app's own code, not Locate): src/entities/points/model/api/services/points.service.ts:170-188,265-283,316-361, src/entities/points/model/api/dto/points.dto.ts:23-31,138-183,312-397, src/entities/points/model/api/rtk/pointsApi.ts:408-421, src/entities/points/model/api/controllers/points.controller.ts:151-152, src/entities/points/index.ts:21-22, src/widgets/PointGiftsCheckout/model/hooks/usePointGiftsCheckout.ts:51,80-120,221-232,288-352,370-401, src/widgets/SendPointGiftForm/ui/SendPointGiftForm/SendPointGiftForm.tsx:149-185, src/features/SendedPointGifts/model/hooks/useSendedPointGifts.ts:20, src/shared/api/instances/orderApiV2.ts:16
  • Dev captures, 2026-10-02 (parity audit): point-order-description--shukah, point-order-description--locate-st, point-order-description-shukahids--locate-st, point-order-list--shukah; the app 2.31.5 request bodies for checkout-payment-flow and checkout-payment-stcpay (captured by the user's proxy). The pay routes were not called on 2026-10-02.
  • Dev captures, 2026-10-02 (follow-up calls, approved by the user): point-order-create-card--shukah, point-order-create-card--locate-st, point-order-create-point--shukah, point-order-create-point--locate-st, and both balances before and after.
  • Dev captures, 2026-10-01: locate-point-order-create-checkout, locate-point-order-get, locate-point-order-description, shukah-point-order-description, locate-flag-probe-pay-by-point, locate-flag-probe-pay-session, probe-locate-point-order-create, probe-locate-point-order-get, probe-locate-point-order-description, probe-locate-point-order-pay-card, probe-locate-point-order-pay-session, locate-point-offers, locate-point-occasions