Skip to main content

Loyalty

Loyalty lets your customers earn points on a payment. You opt a transaction in when you create it, the customer pays as usual, and Ottu issues a reward to the customer's account at the loyalty provider as soon as the payment reaches the paid state. If you later refund, Ottu reverses the reward automatically.

How it works​

Earning points is a server-side opt-in on the Checkout API create call — the customer sees nothing different at checkout. Two fields make a transaction rewardable:

  • loyalty — {"enabled": true} marks the transaction as rewardable. It is identical for every provider.
  • The provider identifier — the customer field the loyalty provider uses to locate the account (never customer_id). For STC Qitaf it is customer_phone, the customer's Saudi mobile number.

You never name the provider in your payload. Ottu resolves the right loyalty backend from the gateway configuration on your pg_code, so adding future providers needs no change to your integration.

Two flows exist and they are independent:

  • Earning — this page. A server-side opt-in, no customer interaction.
  • Redeeming — the customer spends existing points at checkout. That runs through the Checkout SDK as a wallet-style payment method; see Earning vs. redeeming.

For how the feature works from the merchant's side — supported providers, what a reward is calculated on, refunding a points-funded order — see Loyalty for business.

Base URL

Every API call in this guide targets https://sandbox.ottu.net. Swap in your own merchant domain when you integrate.

Boost Your Integration

Ottu offers SDKs and tools to speed up your integration. See Getting Started for all available options.

When to Use​

  • Rewarding purchases — give customers loyalty points on every eligible payment, without building your own points ledger.
  • Campaign-tagged rewards — attach a campaign or program label to a reward for your own reporting.
  • Refund-safe loyalty — rewards unwind automatically when an order is refunded, so points liability tracks real revenue.
  • Reacting to a points return — your backend needs to know when redeemed points go back to a customer. See Refund and points return.

Setup​

Loyalty is configured by Ottu staff, not through the API. Before your first rewarded transaction, confirm with your Ottu account manager that:

  1. A Qitaf service is active on your merchant account — including the STC-issued credentials, client certificate, BranchId, and TerminalId.
  2. The service is linked to the specific gateway MID you pay through. This is the step most often missed. The reward is gated on the pg_code that actually settles the payment, so a transaction routed to an unlinked gateway earns nothing even with loyalty.enabled=true.
  3. The service currency matches your transactions. Each Qitaf service operates in exactly one currency — SAR in practice. A transaction in any other currency is skipped.

Guide​

Workflow​

Loyalty earn flow The merchant creates a Checkout API session with the loyalty flag and the customer's phone number. The customer pays through a payment gateway. When the transaction reaches paid, Ottu issues the reward to the customer's loyalty account at the loyalty provider, such as STC Qitaf. OTTU PLATFORM Merchant Backend Your server Customer Pays at checkout Checkout API Stores your loyalty intent Loyalty reward Issued once paid Payment gateway Settles the payment Loyalty provider e.g. STC Qitaf 1 create 2 pays customer pays through 3 reaches paid 4 issue points
  1. Merchant creates the session with loyalty: {"enabled": true} and the customer's customer_phone.
  2. Customer pays through the hosted checkout page, the Checkout SDK, or any other route — the flow is identical.
  3. Transaction reaches paid. Ottu checks the opt-in flag and the gateway's loyalty configuration.
  4. Reward is issued asynchronously in a background job, just after the payment is acknowledged. The customer's points appear in their loyalty account.

Step-by-Step​

Step 1 of 4
Step 1 of 4

Create the payment transaction

Send loyalty and customer_phone on the standard Checkout API create call, authenticated with your API key. The request below is the minimum: the required Checkout fields plus the two loyalty fields.

Create a rewardable payment transaction
curl --location 'https://sandbox.ottu.net/b/checkout/v1/pymt-txn/' \
--header 'Authorization: Api-Key <YOUR_API_KEY>' \
--header 'Content-Type: application/json' \
--data '{
"type": "e_commerce",
"amount": "50",
"currency_code": "SAR",
"pg_codes": ["<YOUR_QITAF_LINKED_PG_CODE>"],
"customer_phone": "+966566089459",
"loyalty": {"enabled": true}
}'

The response is a standard checkout session. The loyalty object is echoed back exactly as you sent it — that confirms Ottu stored your intent, not that a reward was issued.

Response (abridged)
{
"session_id": "8f2c1d5e9a7b4c3f6e0d2a8b1c4f7e9d",
"checkout_url": "https://sandbox.ottu.net/b/checkout/redirect/start/?session_id=8f2c1d5e9a7b4c3f6e0d2a8b1c4f7e9d",
"amount": "50.00",
"currency_code": "SAR",
"customer_phone": "+966566089459",
"loyalty": {"enabled": true},
"state": "created"
}

Use Cases​

Request fields​

The loyalty object has three fields: enabled (required), reference (your own campaign or program label, defaults to order_no) and metadata (opaque key/value pairs echoed back in the response). Neither reference nor metadata reaches the loyalty provider — they are recorded against the transaction for your reconciliation.

Full loyalty object
{
"loyalty": {
"enabled": true,
"reference": "LOY-CAMP-SPRING-9182",
"metadata": {
"campaign_id": "spring2026",
"segment": "vip"
}
}
}

Field-by-field types, limits and behaviour — including every accepted customer_phone format — are in the API Reference below.

When no reward is issued​

A skipped reward never fails the payment: the transaction still reaches paid and the API returns no error. Each case below has its own link so you can point a teammate straight at it.

Flag missing, false, or misspelled​

loyalty.enabled is absent, false, or the key is misspelled. No reward request is sent.

Provider identifier empty or invalid​

For Qitaf, customer_phone is empty or not a valid Saudi number. No reward request is sent, and a wrong-but-valid number rewards whoever owns it — an invalid one is rejected rather than misdirected.

Gateway not linked​

The pg_code that settles the payment has no Qitaf service linked. See Setup.

Currency mismatch​

The transaction currency differs from the Qitaf service currency (SAR).

Authorization-only transaction​

Only immediate-capture purchases earn. An authorization that is captured later does not trigger a reward.

Funded entirely by redeemed points​

A payment paid completely with redeemed loyalty points earns nothing — points never earn more points.

Reward already exists​

A transaction is rewarded once. Ottu never issues a duplicate.

Refund and Points Return​

Refunding a transaction touches loyalty in two independent ways, and your integration only has to react to the second:

  • Earned points are reversed automatically. The reversal is amount-matched to the money you refund — see step 4 of the guide.
  • Redeemed points come back automatically. When a customer paid with redeemed points, the refund that leaves nothing owed through the gateway also returns those points to their account, in the same request. A points-only order is returned from the dashboard with Refund on the parent.
Refunding an order paid with points The merchant refunds the gateway portion of a split order through Ottu. Ottu refunds the payment gateway, then reverses the redeemed points at STC Qitaf, then answers the merchant with both records booked. Two operation webhooks follow, the points return first, and Ottu finally reduces the earned reward. Merchant Dashboard or API Ottu Refund handling Payment gateway Card portion STC Qitaf Loyalty provider 1 Refund gateway portion 2 Refund 70 SAR 3 Refunded Nothing left owed through the gateway 4 Reverse redeemed points (30 SAR) 5 Reversed 6 Response: both records exist 7 Webhook: points return (30 SAR) 8 Webhook: gateway refund (70 SAR) 9 Reduce the earned reward

The order, trigger and amount rules (gateway money first, points second, always in full, only once) are operator-facing and are documented for the business side in Refunding a points-funded order. The step-by-step dashboard workflow is in Refunding Orders Paid with Qitaf Points.

The points come back on their own — the API does not return them directly

The automatic return follows the same refund permission and the same maker-checker approval as the gateway refund that triggers it. The public Operations API has no call that returns loyalty points: a refund with destination: "wallet" is rejected on a points-funded order. What your integration does see is the outcome, through the webhooks below.

What your webhooks receive​

The points return emits the same two notifications as a gateway refund, so nothing new has to be parsed.

Do not rely on the arrival order

On a split order the gateway refund and the points return happen in the same request, and the points-return operation webhook is sent before the webhook of the gateway refund that triggered it. Match each webhook on its own fields — operation, amount, and pg_response (the points return carries Qitaf's code and its reversal operation_id) — rather than assuming the gateway refund arrives first.

1. The operation webhook (your operations_webhook_url, see Operation events). amount is the points value, result is success, and pg_response carries Qitaf's own answer — code: 0 and the reversal reference in operation_id:

Operation webhook after the points were returned (abridged)
{
"operation": "refund",
"result": "success",
"success": true,
"amount": "30.00",
"order_no": "ORD-10042",
"session_id": "00656aba4360da24a5fbb64ea60687eec6707888",
"reference_number": "qitaf-RKJ8U",
"pg_code": "<YOUR_QITAF_LINKED_PG_CODE>",
"pg_response": {
"code": 0,
"description": "Success",
"operation_id": "a7049ee7-9abc-4b0c-9629-efcd43aec5c8",
"request_date": "2026-09-10T17:52:11"
},
"txn": {
"amount": "30.00",
"currency_code": "SAR",
"session_id": "fee4615c97bf456dab7829039e66098431d959b1",
"state": "refunded"
},
"initiator": {"username": "ops.user", "first_name": "Ops", "last_name": "User"},
"timestamp_utc": "2026-09-10 14:52:12",
"signature": "<HMAC>"
}

2. The parent's payment webhook (see Payment events), whose transactions[] lists every child. After both halves of the refund it shows the original points leg, the gateway refund, and the points return — the last one is the record you are looking for: state: refunded with the points amount.

Parent payment webhook after a full refund of a 100 SAR split order (abridged)
{
"session_id": "00656aba4360da24a5fbb64ea60687eec6707888",
"order_no": "ORD-10042",
"state": "paid",
"amount": "100.00",
"paid_amount": "70.000",
"refunded_amount": "100.000",
"currency_code": "SAR",
"transactions": [
{"amount": "30.00", "currency_code": "SAR", "session_id": "fee4615c97bf456dab7829039e66098431d959b1", "state": "refunded"},
{"amount": "70.00", "currency_code": "SAR", "session_id": "ee430d081612bef7d9bbd30ba0efeb314f3087b9", "state": "refunded"},
{"amount": "30.00", "currency_code": "SAR", "session_id": "d6947c0b5244fbb3ce4da992be4f69195ab82ae5", "state": "paid"}
]
}
paid_amount is the gateway money, not the order total

On a points-funded order paid_amount reports what the gateway settled (70.000 above); the points leg is the paid child of 30.00. Once everything is refunded, refunded_amount reaches the full 100.000 while paid_amount stays at 70.000. Reconcile on refunded_amount and the transactions[] list, not on paid_amount alone.

Both webhooks are signed the usual way — verify them exactly as described in Verify signatures.

API Reference​

Loyalty has no endpoint of its own — it is two fields on the standard Checkout API create call, so the full Checkout API reference is deliberately not repeated here. The schema below is the loyalty object, with every field, type, and constraint. For the surrounding request, see Create Payment Transaction.

 enabledbooleanrequired

Required field. Set to true to opt this transaction into loyalty rewards. Send it together with the provider's identifier field — for STC Qitaf, customer_phone — on every transaction you intend to reward. Neither field is validated: if either is missing or invalid, no reward is issued and no error is returned.

The customer earns points when both conditions hold: enabled is true, and the pg_code that settles the transaction is linked to an active wallet service whose provider declares the post-purchase reward capability (currently STC Qitaf). The reward is issued once the transaction reaches the paid state.

Omitting the loyalty object entirely — or setting enabled to false — sends no reward request, even for a fully paid transaction. Ottu returns no error and no warning in that case, so a missing flag looks identical to a successful reward. See Loyalty for the full behaviour.

 metadata object

Optional field. Opaque key/value pairs stored alongside the transaction and echoed back in the checkout response.

Not transmitted to any loyalty provider. Ottu persists metadata on the payment transaction, but no provider adapter reads it. Use it for merchant-side correlation only — campaign IDs, segment codes, or audit references you resolve in your own system.

 property name*any

Optional field. Opaque key/value pairs stored alongside the transaction and echoed back in the checkout response.

Not transmitted to any loyalty provider. Ottu persists metadata on the payment transaction, but no provider adapter reads it. Use it for merchant-side correlation only — campaign IDs, segment codes, or audit references you resolve in your own system.

 referencestring

Optional field. Merchant-side loyalty reference — a campaign ID, program code, or your own audit label. Maximum 128 characters. Defaults to the transaction's order_no when omitted.

Not transmitted to Qitaf. Ottu records this value against the reward operation for your own reconciliation, but the Qitaf earn API has no general-purpose reference field, so nothing is sent to STC. Treat reference as an internal label, not a value the loyalty provider will see. Set it to something you can resolve in your own system — a campaign ID or internal batch key; the default (order_no) is usually the sensible choice.

Possible values: <= 128 characters

The provider identifier is the customer_phone request field (find it under the request body). Its description there lists every accepted and rejected Saudi mobile format.

Best Practices​

  • Send both fields together, every time. Treat loyalty.enabled and a canonical customer_phone as a pair. Neither is validated, so a missing one costs a reward with no signal.
  • Validate the phone number before you call Ottu. A regex check for ^(\+966|00966|966|0)?5\d{8}$ on your side catches the failure while you can still fix it.
  • Confirm the gateway linkage during onboarding. Send one test transaction per pg_code you intend to reward through and verify with Ottu that the reward landed. A gateway missing its Qitaf link is silent in production.
  • Reconcile out of band. Because the API exposes no reward outcome, periodically reconcile your rewarded orders against STC's own reporting rather than assuming success.
  • Use reference deliberately. Set it to something you can resolve in your own system — a campaign ID or internal batch key. It defaults to order_no, which is usually the sensible choice.

FAQ​

What's Next?​

  • Loyalty for business — supported providers, what a reward is calculated on, and refunding a points-funded order.
  • Checkout API — the create call that carries loyalty and customer_phone.
  • Payment Methods API — discovering the pg_codes your loyalty service is linked to.
  • Operations — refunds, and how they trigger reward reversal.
  • Operation events — the webhook that reports a points return.
  • M-Wallet — the related wallet-credit flow, including multi-wallet stacking with Qitaf.