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 ismoney_amount. The pay routes name the order bycorrelation_idin 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_typepointorcheckout. The app can also sendpayfortorhyperpay, and Locate answers400to 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
504the 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.
| Step | Call | What the app does |
|---|---|---|
| 1. Compose the message | Description | While 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 recipient | Client lookup | On submit, the app looks the recipient up by phone and takes the userId as to_client_id. |
| 3. Create the order | Create | The 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 points | none | The order is already paid when create returns. The app reloads the history, opens the Sent gifts tab and shows "Gift sent successfully". |
| 4b. Saved card | Pay with card | The 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 Pay | Pay with session | The 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 payment | none | On the SDK's success the app reloads the history and opens the Sent gifts tab. |
| Not used by any screen | Get order | The 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.
status | payment_status | Meaning | How it is reached |
|---|---|---|---|
initial_order | null | Created, 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. |
successful | null | Paid with points. The sender was charged and the recipient credited. | Create with point. |
successful | CAPTURED | Paid by card. The recipient was credited. | The gateway reports a captured payment, usually within seconds of the pay call. |
payment_failed | FAILED | The 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. |
paid | AUTHORIZED | Occur 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
| Call | Who | Everyone else |
|---|---|---|
| Create | The caller is always the payer. The payer is taken from the token, never from the body. | — |
| Get order | The payer or the recipient. | 404 point order not found, the same answer as for a missing order. |
| Pay with card, pay with session | The payer only. | 404, also for the recipient. |
| Description | Any 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 issuccessfulorpayment_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 thecorrelation_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.
| Flag | What it controls | Dev, probed 2026-10-01 | Production | What the app sees when it is off |
|---|---|---|---|---|
POINT_ORDER_POINT_ENABLED | Create 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_ENABLED | Both 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
POST /maeda/point-order✅ Replaced🚩 POINT_ORDER_POINT_ENABLED- 🔴 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
| Field | In | Type | Required | Constraints | Description |
|---|---|---|---|---|---|
Authorization | header | string | yes | Bearer <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-Type | header | string | yes | application/json. | The body is JSON. |
point_offer_id | body | string | yes | 24 hex characters, checked and never converted. A Shukah integer, a number or "3" is 400. | The offer id from the offers list. |
to_client_id | body | integer | yes | A 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_type | body | string | yes | Exactly 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_id | body | string or null | no | 24 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. |
occasion | body | string or null | no | Trimmed, 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 key | body | any | no | Dropped. | — |
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.
| Field | Type | Nullable | Format / constraints | Description |
|---|---|---|---|---|
correlation_id | string | no | A UUID v4, 36 characters. | The order id. It goes in the path of the get and pay routes. |
order_no | integer | yes | First value 100000. null only on orders copied from Shukah. | The human-readable order number. |
status | string | no | initial_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_status | string | yes | CAPTURED, FAILED, AUTHORIZED or null. null at creation. | The gateway's state. |
point_amount | number | no | The offer's point_amount. | The points the recipient gets. |
purchased_point_amount | number | yes | The offer's value. null when the offer has no points price. | The points the payer is charged when paying with points. |
money_amount | string | no | Exactly two decimals, for example "40.00". | The SAR the card is charged. |
payment_type | string | no | point or checkout. | Echo of the request. |
order_type | string | no | gift 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.
| HTTP | Body | When |
|---|---|---|
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. |
400 | The same shape, message to_client_id <id> does not exist. | The recipient is not a known client. Not captured. |
400 | The same shape, message point occasion <id> not found or inactive. | The occasion id is unknown or inactive. Not captured. |
400 | The 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. |
400 | The 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. |
401 | The 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.
| Aspect | Shukah | Locate | Impact on the app |
|---|---|---|---|
| URL | POST $SHUKAH_BASE_URL/maeda/point-order | POST $ORDER_BASE_URL/api/v2/client/point-orders | Change the base URL and the path. |
| Token | Shukah 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 status | 201. | 200 (captured on dev, 2026-10-02). | None when the app accepts any 2xx. Code that tests for exactly 201 must change. |
payment_type | The 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 body | Integers 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 keys | Twelve 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 id | point_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-gift | Allowed. | Allowed for checkout, 400 for point. | None for checkout. |
| Status words | initial_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. |
| Idempotency | None. | None. | The same risk on both. |
| Error body | Not documented yet. | The point-service envelope under Errors. | Anything that matches on error text must change. |
| Shukah's view of the order | Not 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 order | Order 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 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
| Field | In | Type | Required | Constraints | Description |
|---|---|---|---|---|---|
Authorization | header | string | yes | Bearer <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_id | path | string | yes | Any 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 key | query | — | no | Forwarded 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
| HTTP | Body | When |
|---|---|---|
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. |
401 | The 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
POST /maeda/point-order/get-description✅ Replaced- 🟡 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
| Field | In | Type | Required | Constraints | Description |
|---|---|---|---|---|---|
Authorization | header | string | yes | Bearer <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-Type | header | string | yes | application/json. | The body is JSON. |
point_offer_id | body | string | yes | 24 hex characters. A Shukah integer or a number is 400. | The offer id. |
point_occasion_id | body | string or null | no | 24 hex characters. A missing key, null, "", "0" and 0 mean none. Any other non-hex value is 400. | The occasion id. |
occasion | body | string or null | no | Trimmed, 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:
| Field | Type | Nullable | Format / constraints | Description |
|---|---|---|---|---|
description | string | no | You 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.
occasionis the free text when it is not empty after trimming. Otherwise it is the occasion's English label whenpoint_occasion_idis 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. pointsis the offer'spoint_amount, what the recipient gets, notpurchased_point_amount.- The giver name is the caller's first and last name joined by a space, or
Locate userwhen 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
| HTTP | Body | When |
|---|---|---|
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. |
400 | The 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. |
400 | The 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. |
401 | The 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
| Aspect | Shukah | Locate | Impact on the app |
|---|---|---|---|
| URL | POST $SHUKAH_BASE_URL/maeda/point-order/get-description | POST $ORDER_BASE_URL/api/v2/client/point-orders/description | Change the base URL and the path. |
| Token | Shukah 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 body | Integers 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. |
| Sentence | Same 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 body | Not 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
POST /maeda/order/checkout-payment-standalone✅ Replaced🚩 POINT_ORDER_CARD_ENABLED- 🔴 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
| Field | In | Type | Required | Constraints | Description |
|---|---|---|---|---|---|
Authorization | header | string | yes | Bearer <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-Type | header | string | yes | application/json. | The body is JSON. |
correlation_id | path | string | yes | Trimmed. Spaces only gives 400 correlation_id is required. | The order's correlation_id. |
id | body | string | yes | Trimmed, at least 1 character. A non-string is 400. | The saved card's id (src_…). The app's card.tokenName. |
cvv | body | string | no | A 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 key | body | any | no | Accepted 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.
| HTTP | Body | When |
|---|---|---|
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. |
400 | The same shape, message this point order is not paid by card. | The order's payment_type is not checkout. Not captured. |
400 | The 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. |
400 | The 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. |
409 | The 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 status | The 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. |
401 | The 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
| Aspect | Shukah | Locate | Impact on the app |
|---|---|---|---|
| URL | POST $SHUKAH_BASE_URL/maeda/order/checkout-payment-standalone | POST $ORDER_BASE_URL/api/v2/client/point-orders/{correlation_id}/pay/card | Change 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 id | point_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. |
| Token | Shukah 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 dev | 422 {"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. |
| Flag | None. | POINT_ORDER_CARD_ENABLED: on for dev (probed 2026-10-01), production not confirmed. | If the flag is off, every call is 403. |
| Payment errors | The payment service's own 4xx. | Passed through unchanged with the same status and body. | None: the app already handles that shape. |
| Response | The 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 payments | The 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 body | Not 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
POST /maeda/order/checkout-payment-flow✅ Replaced🚩 POINT_ORDER_CARD_ENABLED- 🔴 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
| Field | In | Type | Required | Constraints | Description |
|---|---|---|---|---|---|
Authorization | header | string | yes | Bearer <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-Type | header | string | no | application/json. An empty or missing body is valid and means {}. | The body is JSON. |
correlation_id | path | string | yes | Trimmed. Spaces only gives 400 correlation_id is required. | The order's correlation_id. |
rememberMe | body | boolean | no | true, 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 key | body | any | no | Accepted 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.
| HTTP | Body | When |
|---|---|---|
400 | {"success":false,"error":{"code":"VALIDATION_ERROR","message":"Invalid pay-by-session body","details":{…}}} | rememberMe is not a boolean-like value. Not captured. |
400 | The 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. |
400 | The 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 status | The 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. |
401 | The 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
| Aspect | Shukah | Locate | Impact on the app |
|---|---|---|---|
| URL | POST $SHUKAH_BASE_URL/maeda/order/checkout-payment-flow | POST $ORDER_BASE_URL/api/v2/client/point-orders/{correlation_id}/pay/session | Change 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_type | App 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 id | A 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 dev | App 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. |
| Flag | None. | POINT_ORDER_CARD_ENABLED: on for dev (probed 2026-10-01), production not confirmed. | If the flag is off, every call is 403. |
| Response | Not 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 body | Not 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 takecorrelation_idin the path, so the app must keep it from create. - Create has no idempotency key: after a
504the app never learns the order id, and a retry can make a second order or, forpoint, 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) | Forwarded | Limit |
|---|---|---|---|
POST /api/v2/client/point-orders | POST /api/client/point-orders | Authorization and the body as received | 10 s |
GET /api/v2/client/point-orders/:correlation_id | GET /api/client/point-orders/<id>, with the id passed through encodeURIComponent | Authorization and the whole query (ignored upstream) | 10 s |
POST /api/v2/client/point-orders/description | POST /api/client/point-orders/description | Authorization and the body | 10 s |
POST /api/v2/client/point-orders/:correlation_id/pay/card | POST /api/client/point-orders/<id>/pay/card | Authorization and the body | 20 s |
POST /api/v2/client/point-orders/:correlation_id/pay/session | POST /api/client/point-orders/<id>/pay/session | Authorization and the body | 20 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
createPointOrderruns 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 sharedclientscollection (PS: packages/core/src/points/client-lookup.ts:20-35). checkout: one order insert and a counter increment fororder_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_statusnull) 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: apointorder 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_snapshotof 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_nostarts at 100000 from a counter, andcorrelation_idis 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.
occasiongoes into the stored description and, forpoint, into both ledger legs. It is not stored on the order as a field. For acheckoutorder 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_typeisgiftwhen an occasion row was found (by its Shukah integer or its own id) andtransferotherwise (PS: packages/core/src/points/derive-order-type.ts:16-34). The point service's doc sample showsgiftfor a body with an occasion. The livecheckoutcall had no occasion, so it istransfer.- No idempotency key. Every call allocates a new
correlation_idandorder_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
successfulandCAPTURED. 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_ordersetspayment_failedandFAILED(PS: packages/core/src/points/settle-point-order.ts:33-42,125-145). - A capture on a
payment_failedorder 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 same404), 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-apirepo; 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 isPOST {PAYMENT_API_URL}/client/api/v1/checkout/payment/lp-<correlation_id>/session. Both send the headerstoken(the client's raw token),x-from-application: LOCATE_CLIENTandx-transaction-type: POINT. The body hasamountin SAR,customerId(the payer),customerName,customerPhone, andidandcvv(card) orrememberMe(session). The payer is the customer, never the recipient. The gateway reference islp-<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_providerandpayment_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
cvvis 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
4xxis passed through with its own status and body. A transport failure, a timeout or a5xxis not caught and becomesINTERNAL_ERROR500, 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
tokenheader: 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(withpageandper-page, and thex-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 thecvv/tokencall 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. Thecvv/tokencall 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
getPointGiftOrderByIdforGET /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 answers401(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
401on 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 havepoint_occasion_id(the sent-gifts list, captured on dev, 2026-10-02), so the app'soccasionIdis alwaysundefined(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_idas 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
201with twelve keys, Locate200with nine, and apointorder on either side cut only that side's balance (capturespoint-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:38locate-order-v2-api@d9e4e5f6docs:docs/locate-point-client-api-reference.md:46,48,201,docs/plans/2026-09-16-point-order-proxy-plan.md:21locate-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-23locate-point-service@2c44ce3docs: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(branchdevelopment, 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 forcheckout-payment-flowandcheckout-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