3. Authentication
The Partner API uses the OAuth 2.0 Client Credentials grant. This is a machine-to-machine flow — there is no end-user login or redirect step. The Bank backend authenticates as itself, then identifies individual customers separately via partner_user_id on each business request.
3.1 Request an Access Token
POST
/partner/v1/oauth/token
Description
Exchanges Bank-issued client credentials for a short-lived access token.
Authentication
None (this is the entry point). Request body carries credentials.
Headers
Content-Type: application/json
Possible Errors
400 Bad Request, 401 Unauthorized, 403 Forbidden.
Request Body
{
"client_id": "bankx-prod-client",
"client_secret": "••••••••••••••••••••••••••••",
"grant_type": "client_credentials"
}
Response 200 OK
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 604800
}
3.2 Using the Access Token
Every protected endpoint requires the token on every request:
Authorization: Bearer <access_token>
3.3 Token Lifetime & Renewal
- Access tokens are valid for 604800 seconds (1 week), reflected in
expires_in.
- There is no refresh token. When a token expires, request a new one from
/partner/v1/oauth/token using the same client credentials.
- The Bank backend should cache the token in memory or a shared cache and request a new one proactively — for example, 5 minutes before
expires_in elapses — rather than on every request.
- Requests made with an expired or invalid token receive 401 Unauthorized.
3.4 Transport Security
HTTPS is mandatory on every environment. Plain HTTP requests are rejected. TLS 1.2 or higher is required; older protocol versions and weak cipher suites are not accepted.
3.5 IP Whitelisting
Each client_id is bound to a set of allowlisted source IP addresses or CIDR ranges, configured during onboarding. Requests originating from a non-allowlisted IP are rejected with 403 Forbidden, even if the credentials are otherwise valid. IP ranges are managed per environment (UAT and production are independent).
3.6 Authentication Errors
| Status | Error Code | Cause |
| 400 | invalid_request | Missing or malformed field in the token request body. |
| 401 | invalid_client | Unknown client_id or incorrect client_secret. |
| 401 | invalid_token | Access token is malformed, expired, or has been revoked. |
| 403 | ip_not_allowed | Request originated from an IP address outside the allowlist for this client_id. |
| 403 | client_disabled | The client credentials have been deactivated by Elite Club. |
10. Endpoint Details
GET
/partner/v1/offers/exclusive
Returns the standard tier of Elite Club offers available to the Bank's customers, together with the discovery lists (countries, categories) needed to build filter UI.
Query Parameters
| Parameter | Type | Required | Description |
partner_user_id | string | Yes | Bank-issued customer identifier. |
country | integer or CSV list of integers | No | Filter by one or more country ids from the countries list (for example country=12,18). |
category | integer or CSV list of integers | No | Filter by one or more category ids from the categories list (for example category=1,5). |
search | string | No | Free-text search across offer title, description, and merchant name. |
latest | boolean (0/1) | No | When 1, returns only offers created in the last 90 days. |
language | string | No | See Common Request Parameters. |
latitude | number | No | See Common Request Parameters. |
longitude | number | No | See Common Request Parameters. |
page | integer | No | Default 1. |
limit | integer | No | Default 20, maximum 50. |
Example Request
GET /partner/v1/offers/exclusive?partner_user_id=BANKX-88213&country=12,18&category=1,5&latest=1&page=1&limit=20 HTTP/1.1
Host: appapi.eliteclub.global
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Accept: application/json
Example Response — 200 OK
{
"success": true,
"message": "Offers retrieved successfully.",
"data": {
"countries": [
{ "id": 12, "name": "United Arab Emirates" },
{ "id": 18, "name": "Saudi Arabia" }
],
"categories": [
{ "id": 1, "name": "Restaurants & Cafes" },
{ "id": 2, "name": "Fitness & Wellness" },
{ "id": 3, "name": "Other Services" },
{ "id": 4, "name": "Lifestyle & Activities" },
{ "id": 5, "name": "Hotels & Resorts" },
{ "id": 6, "name": "Beach Clubs" }
],
"offers": [
{
"offer_id": 10432,
"title": "50% Off Any Main Course",
"name": "Business Bay Grill – Chef's Choice",
"description": "Enjoy 50% off any single main course at Business Bay Grill, Dubai.",
"category": { "id": 1, "name": "Restaurants & Cafes", "icon_url": "https://ecsystem.eliteclub.global/uploads/icons/restaurants-cafes.png" },
"country": { "id": 12, "name": "United Arab Emirates" },
"offer_type": "discount",
"discount": {
"type": "percentage",
"percentage": 50,
"fixed_amount": null,
"currency": "AED"
},
"estimated_saving": { "amount": 45.00, "currency": "AED" },
"best_seller": true,
"merchant": {
"type": "outlet",
"id": 552,
"name": "Business Bay Grill",
"address": "Bay Avenue, Business Bay, Dubai, UAE",
"latitude": 25.1857,
"longitude": 55.2631
,"currency": "AED"
},
"image_url": "https://ecsystem.eliteclub.global/uploads/files/Business%20Bay%20Grill-cover().jpg",
"distance_km": 3.42
}
]
},
"meta": {
"pagination": {
"current_page": 1,
"per_page": 20,
"total_pages": 8,
"total_records": 152
},
"correlation_id": "c3f6a8e2-4a41-4e2a-9d10-2f5b6b6a9b10",
"generated_at": "2026-07-15T10:32:00Z"
}
}
The listing response returns a summarized offer object for compact rendering in list views. Call
Get Offer Details for the complete object, including redemption limits, terms, and availability schedule.
Possible Errors
401 Unauthorized, 403 Forbidden, 422 Validation Error.
GET
/partner/v1/offers/premium
Returns the premium tier of Elite Club offers. The request and response contract is identical to Get Exclusive Offers — only the underlying set of offers differs.
Query Parameters
Identical to GET /partner/v1/offers/exclusive: partner_user_id (required), multi-select country, multi-select category, search, latest (last 90 days), latitude, longitude, page, limit.
Example Request
GET /partner/v1/offers/premium?partner_user_id=BANKX-88213&page=1&limit=20 HTTP/1.1
Host: appapi.eliteclub.global
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Accept: application/json
Response Shape
Identical to Get Exclusive Offers: data.countries, data.categories, data.offers[], and the standard meta.pagination block.
Possible Errors
401 Unauthorized, 403 Forbidden, 422 Validation Error.
GET
/partner/v1/offers/{offer_id}
Returns the complete detail record for a single offer, including merchant information, redemption limits, terms, and availability.
Path Parameters
| Parameter | Type | Required | Description |
offer_id | integer | Yes | Identifier of the offer, as returned in a listing response. |
Query Parameters
Example Request
GET /partner/v1/offers/10432?partner_user_id=BANKX-88213&latitude=25.2048&longitude=55.2708 HTTP/1.1
Host: appapi.eliteclub.global
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Accept: application/json
Example Response — 200 OK
{
"success": true,
"message": "Offer retrieved successfully.",
"data": {
"offer_id": 10432,
"title": "50% Off Any Main Course",
"name": "Business Bay Grill – Chef's Choice",
"description": "Enjoy 50% off any single main course at Business Bay Grill, Dubai.",
"category": { "id": 1, "name": "Restaurants & Cafes", "icon_url": "https://ecsystem.eliteclub.global/uploads/icons/restaurants-cafes.png" },
"country": { "id": 12, "name": "United Arab Emirates" },
"offer_type": "discount",
"discount": {
"type": "percentage",
"percentage": 50,
"fixed_amount": null,
"currency": "AED"
},
"estimated_saving": { "amount": 45.00, "currency": "AED" },
"validity": {
"duration_type": "limited",
"from_date": "2026-01-01",
"to_date": "2026-12-31",
"valid_any_day": false,
"available_days": [
{ "day": "Mon", "from": "12:00", "to": "23:00" },
{ "day": "Tue", "from": "12:00", "to": "23:00" },
{ "day": "Wed", "from": "12:00", "to": "23:00" }
]
},
"redemption_limits": {
"quantity_per_customer_type": "quantity",
"quantity_per_customer": 2,
"quantity_per_visit": 1,
"redeemed_count_for_customer": 0
},
"best_seller": true,
"merchant": {
"type": "outlet",
"id": 552,
"name": "Business Bay Grill",
"address": "Bay Avenue, Business Bay, Dubai, UAE",
"latitude": 25.1857,
"longitude": 55.2631,
"currency": "AED"
},
"image_url": "https://ecsystem.eliteclub.global/uploads/files/Business%20Bay%20Grill-cover().jpg",
"kids_policy": "",
"kids_policy_ar": "",
"terms": [
"One redemption per visit.",
"Not valid in conjunction with other offers or promotions."
],
"distance_km": 3.42
},
"meta": {
"correlation_id": "7bd80858-1e17-45be-a307-f5412fb66831",
"generated_at": "2026-07-15T10:32:00Z"
}
}
Possible Errors
401 Unauthorized, 403 Forbidden, 404 Not Found (offer does not exist or is no longer active), 422 Validation Error.
POST
/partner/v1/offers/redeem
Finalizes redemption of an offer for a customer at the merchant location, and returns an authorization code as proof of redemption. This is a financial-value, state-changing operation and must be called only once merchant staff has entered the redemption pincode at checkout — never speculatively or in advance.
pincode is entered by merchant staff at the outlet or hotel, not by the app user. The Bank app must never prompt its own customer to type in a pincode — it should be captured on the merchant's side (e.g. a staff-facing device or terminal) and passed by the Bank backend as part of this request.
Request Fields
Every redemption request always includes partner_user_id, offer_id, pincode, quantity, and either hotel_id or outlet_id (whichever matches the offer's merchant.type). paid_amount, check_number, guests_number, and currency are required only when the offer being redeemed is a Discount offer (offer_type: "discount") — they do not apply to BOGOF offers (offer_type: "bogof") and should be omitted for those.
| Field | Type | Required When | Description |
partner_user_id | string | Always | Bank-issued identifier of the customer redeeming the offer. |
offer_id | integer | Always | Identifier of the offer being redeemed. |
pincode | string | Always | Verification code entered by merchant staff at the point of sale — not by the app user — to confirm the redemption is taking place at their outlet or hotel. |
quantity | integer | Always | Number of units being redeemed in this transaction. |
hotel_id | integer | Only when the offer's merchant.type is hotel | Must match the offer's merchant.id. |
outlet_id | integer | Only when the offer's merchant.type is outlet | Must match the offer's merchant.id. |
paid_amount | number | Only for Discount offers | Amount actually paid by the customer at checkout, before the discount is netted off. |
check_number | string | Only for Discount offers | Merchant's bill or check reference number for the transaction. |
guests_number | integer | Only for Discount offers | Number of guests/covers on the bill. |
currency | string | Only for Discount offers | ISO currency code of paid_amount. |
paid_amount, check_number, guests_number, and currency are ignored (and should not be sent) for BOGOF offers, since no bill is being discounted — the customer simply receives the complimentary item.
Example Request — Discount Offer
POST /partner/v1/offers/redeem HTTP/1.1
Host: appapi.eliteclub.global
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
{
"partner_user_id": "BANKX-88213",
"offer_id": 10432,
"pincode": "4821",
"quantity": 1,
"outlet_id": 552,
"paid_amount": 90.00,
"check_number": "A1001",
"guests_number": 2,
"currency": "AED"
}
Example Request — BOGOF Offer
POST /partner/v1/offers/redeem HTTP/1.1
Host: appapi.eliteclub.global
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
{
"partner_user_id": "BANKX-88213",
"offer_id": 20115,
"pincode": "7734",
"quantity": 1,
"hotel_id": 88
}
Example Response — 200 OK
{
"success": true,
"message": "Offer redeemed successfully.",
"data": {
"authcode": "EC-D-98341",
"redeemed_at": "2026-07-15T13:42:00Z"
},
"meta": {
"correlation_id": "2de939fd-25a0-4f89-b336-e2f8b1d8c2de"
}
}
authcode is the final authorization proof of successful redemption and should be displayed to merchant staff to complete the checkout.
Possible Errors
| Status | Error Code | Cause |
| 404 | offer_not_found | offer_id does not exist or is inactive. |
| 422 | invalid_pincode | pincode does not match the specified hotel_id/outlet_id. |
| 422 | validation_error | A conditionally required field is missing for the offer's offer_type, or neither/both of hotel_id/outlet_id were supplied. |
| 409 | insufficient_quantity | The offer's total available quantity has been exhausted. |
| 409 | redemption_limit_exceeded | This partner_user_id has reached their maximum allowed redemptions for this offer. |
| 403 | membership_cap_reached | The mapped Elite Club member has reached their overall redemption cap across all offers. |
Redemption Notes
- This is a financial-value operation and must be performed only when the customer is physically at the merchant location.
- On failure, no redemption is recorded and the offer's available quantity is left unchanged.
hotel_id/outlet_id must match the merchant referenced by the offer — sending the wrong one returns validation_error.
Error Response Examples
401 — invalid_token
{
"success": false,
"message": "Access token is invalid or has expired.",
"data": null,
"meta": {
"error_code": "invalid_token",
"correlation_id": "f2a84f84-7d31-49ef-9328-f266e9d4dc62"
}
}
404 — resource_not_found
{
"success": false,
"message": "The requested offer could not be found.",
"data": null,
"meta": {
"error_code": "resource_not_found",
"correlation_id": "9170f7bb-35d8-4768-b2cc-0d73fa8f4a97"
}
}
422 — validation_error
{
"success": false,
"message": "Validation failed.",
"data": null,
"meta": {
"error_code": "validation_error",
"errors": {
"partner_user_id": ["The partner_user_id field is required."],
"limit": ["The limit must not be greater than 50."]
},
"correlation_id": "7bd80858-1e17-45be-a307-f5412fb66831"
}
}
409 — insufficient_quantity
{
"success": false,
"message": "This offer has no remaining quantity available.",
"data": null,
"meta": {
"error_code": "insufficient_quantity",
"correlation_id": "3af1c9a0-6b0e-4e39-9a2a-1f9d2e6b7c44"
}
}
409 — redemption_limit_exceeded
{
"success": false,
"message": "This customer has reached the maximum allowed redemptions for this offer.",
"data": null,
"meta": {
"error_code": "redemption_limit_exceeded",
"remaining_quantity": 0,
"correlation_id": "8e2b9f11-2c34-4a4e-8b39-5a6f0c2d9e77"
}
}
18. Sandbox Environment
A self-contained sandbox is available so the Bank's engineering team can build and test their integration end-to-end before real credentials, real merchant data, or a production go-live date are in place. The sandbox implements the exact same request and response contract documented above, but every response is static mock data — no request ever touches real Elite Club members, merchants, or redemption records.
https://appapi.eliteclub.global/partner-sandbox/v1
The sandbox is for integration testing only. It is not rate-limited or access-restricted the way production is, and its data resets to the fixed catalog below on every request — nothing is persisted. Never point production traffic at it.
18.1 Sandbox Endpoints
POST/partner-sandbox/v1/oauth/token
GET/partner-sandbox/v1/offers/exclusive
GET/partner-sandbox/v1/offers/premium
GET/partner-sandbox/v1/offers/{offer_id}
POST/partner-sandbox/v1/offers/redeem
GET/partner-sandbox/v1/offers/countries
GET/partner-sandbox/v1/offers/categories
18.2 Discovery Lists
The sandbox exposes two discovery endpoints so the Bank can populate filter menus without relying on the offer listing responses:
GET /partner-sandbox/v1/offers/countries returns the fixed country list used by the sandbox offer catalog.
GET /partner-sandbox/v1/offers/categories returns the fixed category list used by the sandbox offer catalog.
- Both endpoints return the standard response envelope with
success: true, a short status message, data as an array of { id, name } objects, and meta: null.
| Endpoint | Returned data |
GET /partner-sandbox/v1/offers/countries | [{ "id": 12, "name": "United Arab Emirates" }, { "id": 18, "name": "Saudi Arabia" }] |
GET /partner-sandbox/v1/offers/categories | [{ "id": 1, "name": "Restaurants & Cafes" }, { "id": 2, "name": "Fitness & Wellness" }, { "id": 3, "name": "Other Services" }, { "id": 4, "name": "Lifestyle & Activities" }, { "id": 5, "name": "Hotels & Resorts" }, { "id": 6, "name": "Beach Clubs" }] |
18.3 Simplified Authentication
POST /partner-sandbox/v1/oauth/token accepts any client_id / client_secret value, as long as grant_type is client_credentials and all three fields are present — it always returns the same mock access_token, valid for the standard expires_in of 604800 seconds.
- Every other sandbox endpoint only checks that an
Authorization: Bearer <token> header is present — any non-empty token is accepted. Omitting the header returns the same 401 invalid_token shape as production, so error handling can still be tested.
- There is no IP allowlist on the sandbox.
18.4 Fixed Offer Catalog
The sandbox always serves the same four offers, split across tiers, so responses are predictable while you build against them.
| offer_id | Tier | offer_type | Merchant | Country | Category |
10432 | Exclusive | discount | outlet_id 552 — Business Bay Grill | United Arab Emirates | Restaurants & Cafes |
20115 | Exclusive | bogof | hotel_id 88 — Grand Beach Resort | United Arab Emirates | Hotels & Resorts |
30678 | Premium | discount | outlet_id 781 — FitZone Riyadh | Saudi Arabia | Fitness & Wellness |
40921 | Premium | discount | outlet_id 640 — Skyline Spa & Wellness | United Arab Emirates | Lifestyle & Activities |
Any other offer_id returns 404 offer_not_found, exactly as production would for an unknown or inactive offer.
18.5 Deterministic Test Triggers for Redeem
Use these reserved input values against POST /partner-sandbox/v1/offers/redeem to deliberately exercise each error path documented in Redeem Offer, without needing to reverse-engineer real business conditions.
| To trigger | Send |
422 invalid_pincode | pincode: "0000" |
409 insufficient_quantity | quantity greater than 10 |
409 redemption_limit_exceeded | partner_user_id: "BANKX-LIMIT-REACHED" |
403 membership_cap_reached | partner_user_id: "BANKX-CAP-REACHED" |
404 offer_not_found | any offer_id not in the catalog above |
200 success | any other combination of valid, matching fields |
A successful sandbox redemption always returns a freshly generated authcode and redeemed_at timestamp, exactly matching the production response shape.