Elite Club Partner API Not a Public API

Partner API Documentation

Version 1.2. A dedicated, white-label B2B integration guide for the Bank's backend engineering team, enabling the Bank application to surface Elite Club offers to its own customers. This API is consumed exclusively by the Bank backend over a trusted server-to-server channel — it is never called by any mobile or public client.

Version
1.2
Base Path
/partner/v1
Integration Mode
Server-to-server, Bank backend only
Authentication
OAuth 2.0 Client Credentials

Quick Start

Use this sequence to complete your first integration call.

  1. Request an access token from POST /partner/v1/oauth/token using the client_id and client_secret issued to the Bank during onboarding.
  2. Cache the returned access_token for the duration of expires_in seconds (default 604800 / 1 week).
  3. Call any business endpoint with Authorization: Bearer <access_token>, identifying the customer via partner_user_id.
  4. Use the countries and categories arrays returned by the listing endpoints to drive filter menus in the Bank app — do not hardcode these values.
  5. At merchant checkout, merchant staff enter the outlet/hotel pincode on their side (it is never entered by the app user); the Bank backend then calls POST /partner/v1/offers/redeem to finalize the redemption and obtain an authorization code.
  6. When the token expires, request a new one from the same token endpoint. There is no separate refresh-token grant.
Bank Mobile App -> Bank Backend -> POST /partner/v1/oauth/token (client_id + client_secret) <- access_token (Bearer, expires_in: 604800) -> GET /partner/v1/offers/exclusive?partner_user_id=...&country=12,18&category=1,5&latest=1 (Authorization: Bearer) <- offers[] + countries[] + categories[] + pagination meta -> POST /partner/v1/offers/redeem (at merchant checkout, pincode entered by merchant staff) <- authcode Bank Backend -> Bank Mobile App

Quick Start Token Request

POST /partner/v1/oauth/token HTTP/1.1
Host: appapi.eliteclub.global
Content-Type: application/json

{
  "client_id": "bankx-prod-client",
  "client_secret": "••••••••••••••••••••••••••••",
  "grant_type": "client_credentials"
}

Quick Start Token Response

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJlbGl0ZS1wYXJ0bmVyIn0.signature",
  "token_type": "Bearer",
  "expires_in": 604800
}

1. Overview & Architecture

The Elite Club Partner API is a dedicated, white-label integration layer built exclusively for the Bank. It is not a public or general-purpose API, and it is not versioned or marketed alongside Elite Club's consumer-facing products. Its sole purpose is to let the Bank backend retrieve Elite Club offers and present them inside the Bank's own mobile application, under the Bank's own branding.

The Bank mobile application must never call Elite Club APIs directly. All requests must originate from the Bank backend over a trusted, authenticated, server-to-server channel.

1.1 Request Flow

Bank Mobile App | v Bank Backend | v Elite Club Partner API (/partner/v1) | v Elite Club Platform

1.2 Responsibilities

Bank Backend

  • Owns and issues partner_user_id for its own customers.
  • Obtains and caches OAuth2 access tokens.
  • Calls the Partner API on behalf of the customer.
  • Presents responses inside the Bank app, under Bank branding.

Elite Club Partner API

  • Authenticates the Bank backend via OAuth2 client credentials.
  • Maps partner_user_id to an internal Elite Club member, transparently.
  • Returns offers, merchant details, countries, and categories.
  • Enforces security and audit logging.

2. Base URL

https://appapi.eliteclub.global

All endpoints documented here are relative to this base URL, under the versioned path /partner/v1. For example, the offers listing endpoint is reachable at https://appapi.eliteclub.global/partner/v1/offers/exclusive.

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

StatusError CodeCause
400invalid_requestMissing or malformed field in the token request body.
401invalid_clientUnknown client_id or incorrect client_secret.
401invalid_tokenAccess token is malformed, expired, or has been revoked.
403ip_not_allowedRequest originated from an IP address outside the allowlist for this client_id.
403client_disabledThe client credentials have been deactivated by Elite Club.

4. Common Headers

HeaderRequiredDescription
AuthorizationYes, on protected endpointsBearer <access_token> issued by the token endpoint.
Content-TypeYesapplication/json for all requests.
AcceptRecommendedapplication/json.
X-Correlation-IdRecommendedA UUID generated by the Bank backend per request. Echoed back in meta.correlation_id and included in Elite Club's audit logs to support end-to-end tracing across both systems.

5. Common Response Format

Every endpoint returns a single, consistent envelope, regardless of success or failure.

5.1 Success Envelope

{
  "success": true,
  "message": "Request processed successfully.",
  "data": {},
  "meta": {}
}

5.2 Error Envelope

{
  "success": false,
  "message": "Human-readable description of the error.",
  "data": null,
  "meta": {
    "error_code": "VALIDATION_ERROR",
    "correlation_id": "c3f6a8e2-4a41-4e2a-9d10-2f5b6b6a9b10"
  }
}
Always branch on the success boolean, not on HTTP status alone, and log meta.correlation_id alongside your own request id for support and reconciliation.

6. Pagination & Filtering

6.1 Pagination Parameters

ParameterTypeDefaultDescription
pageinteger11-indexed page number.
limitinteger20Items per page. Maximum 50.

6.2 Pagination Meta

"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"
}

6.3 Filtering

Listing endpoints accept multi-select country and category filters, plus a free-text search parameter and a latest filter. country and category are integer identifiers and support multiple values in a single request (for example country=12,18 and category=1,5) — do not hardcode them. Each listing response includes the full, current set of valid countries and categories for the Bank to render as filter options and to pass back on subsequent calls. When latest=1 is supplied, only offers created in the last 90 days are returned.

7. User Identification

The OAuth2 access token identifies the Bank as the calling system. It does not identify an individual customer. Each business request additionally carries:

partner_user_id
  • partner_user_id is generated and owned by the Bank — it can be an account number, a hashed customer id, or any stable identifier chosen by the Bank.
  • Elite Club internally maps partner_user_id to its own member record. This mapping is transparent to the Bank.
  • The Bank never needs to know, store, or handle Elite Club's internal member identifiers.
  • partner_user_id should remain stable for the lifetime of the customer relationship; changing it will be treated as a different customer.

8. Common Request Parameters

Every business (non-authentication) endpoint accepts the following parameters:

ParameterTypeRequiredDescription
partner_user_idstringYesBank-issued identifier for the customer making the request.
languagestringNoResponse language. Supported values are en and ar. Defaults to en. When ar is supplied, offer title, name, description, country.name, and merchant.name/merchant.address are returned in Arabic.
latitudenumberNoCustomer's current latitude. Used only to compute distance_km to nearby merchants.
longitudenumberNoCustomer's current longitude. Used only to compute distance_km to nearby merchants.
If latitude/longitude are omitted, offer objects are returned without a distance_km field rather than with a null value.

9. Endpoint Summary

Bootstrap

POST/partner/v1/oauth/token

Offers (Bearer Token Required)

GET/partner/v1/offers/exclusive
GET/partner/v1/offers/premium
GET/partner/v1/offers/{offer_id}
POST/partner/v1/offers/redeem

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

ParameterTypeRequiredDescription
partner_user_idstringYesBank-issued customer identifier.
countryinteger or CSV list of integersNoFilter by one or more country ids from the countries list (for example country=12,18).
categoryinteger or CSV list of integersNoFilter by one or more category ids from the categories list (for example category=1,5).
searchstringNoFree-text search across offer title, description, and merchant name.
latestboolean (0/1)NoWhen 1, returns only offers created in the last 90 days.
languagestringNoSee Common Request Parameters.
latitudenumberNoSee Common Request Parameters.
longitudenumberNoSee Common Request Parameters.
pageintegerNoDefault 1.
limitintegerNoDefault 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

ParameterTypeRequiredDescription
offer_idintegerYesIdentifier of the offer, as returned in a listing response.

Query Parameters

ParameterTypeRequiredDescription
partner_user_idstringYesBank-issued customer identifier.
languagestringNoSee Common Request Parameters.
latitudenumberNoSee Common Request Parameters.
longitudenumberNoSee Common Request 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.

FieldTypeRequired WhenDescription
partner_user_idstringAlwaysBank-issued identifier of the customer redeeming the offer.
offer_idintegerAlwaysIdentifier of the offer being redeemed.
pincodestringAlwaysVerification 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.
quantityintegerAlwaysNumber of units being redeemed in this transaction.
hotel_idintegerOnly when the offer's merchant.type is hotelMust match the offer's merchant.id.
outlet_idintegerOnly when the offer's merchant.type is outletMust match the offer's merchant.id.
paid_amountnumberOnly for Discount offersAmount actually paid by the customer at checkout, before the discount is netted off.
check_numberstringOnly for Discount offersMerchant's bill or check reference number for the transaction.
guests_numberintegerOnly for Discount offersNumber of guests/covers on the bill.
currencystringOnly for Discount offersISO 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

StatusError CodeCause
404offer_not_foundoffer_id does not exist or is inactive.
422invalid_pincodepincode does not match the specified hotel_id/outlet_id.
422validation_errorA conditionally required field is missing for the offer's offer_type, or neither/both of hotel_id/outlet_id were supplied.
409insufficient_quantityThe offer's total available quantity has been exhausted.
409redemption_limit_exceededThis partner_user_id has reached their maximum allowed redemptions for this offer.
403membership_cap_reachedThe 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.

11. The Offer Object — Field Dictionary

The Detail column indicates whether a field is present in the summarized listing object, the full detail object, or both.

FieldTypePresent InDescription
offer_idintegerBothUnique identifier of the offer.
titlestringBothMarketing headline for the offer.
namestringBothDisplay name of the specific deal.
descriptionstring, nullableBothFull descriptive text.
categoryobjectBoth{ id, name, icon_url } — one of the values from the discovery categories list.
countryobjectBoth{ id, name } — one of the values from the discovery countries list.
offer_typestring enumBothdiscount or bogof (buy-one-get-one-free).
discount.typestring enum, nullableBothpercentage or fixed.
discount.percentagenumber, nullableBothDiscount percentage, when discount.type is percentage.
discount.fixed_amountnumber, nullableBothDiscount amount, when discount.type is fixed.
discount.currencystringBothISO currency code for the offer's pricing.
estimated_savingobjectBoth{ amount, currency } — approximate saving for display purposes.
best_sellerbooleanBothWhether the offer is flagged as a best seller.
validity.duration_typestring enumDetail onlylimited or unlimited.
validity.from_date / to_datestring (date), nullableDetail onlyOffer validity window, ISO 8601 date.
validity.valid_any_daybooleanDetail onlyIf true, available_days is omitted — the offer is valid every day.
validity.available_days[]array of objectDetail only{ day, from, to }. day is one of SunSat; from/to are local time strings.
redemption_limits.quantity_per_customer_typestring enumDetail onlyunlimited or quantity.
redemption_limits.quantity_per_customerinteger, nullableDetail onlyMaximum redemptions allowed for this customer, when limited.
redemption_limits.quantity_per_visitinteger, nullableDetail onlyMaximum redemptions allowed per single visit.
redemption_limits.redeemed_count_for_customerintegerDetail onlyNumber of times this specific partner_user_id has already redeemed the offer.
merchant.typestring enumBothhotel or outlet.
merchant.idintegerBothIdentifier of the hotel or outlet.
merchant.namestringBothMerchant display name.
merchant.addressstringBothMerchant address.
merchant.latitude / longitudenumber, nullableBothMerchant location.
image_urlstring (URL)BothOffer image.
kids_policy / kids_policy_arstringDetail onlyMerchant's kids policy, English and Arabic.
terms[]array of string, nullableDetail onlyRedemption terms and conditions.
distance_kmnumberBothPresent only when latitude/longitude were supplied on the request.
This response omits Elite Club's internal loyalty and engagement fields (such as loyalty point balances and aggregate interaction counters) — the Bank's customers are not Elite Club members and do not need this information. The one exception is redeemed_count_for_customer, which is scoped to the identified partner_user_id and is useful for enforcing per-customer redemption limits in the Bank app.

12. HTTP Status Codes

StatusMeaningTypical Cause
200OKRequest validated and processed successfully.
400Bad RequestMalformed request syntax, e.g. invalid JSON body.
401UnauthorizedMissing, invalid, or expired access token; invalid client credentials.
403ForbiddenSource IP not on the allowlist, or client/customer not entitled to this resource.
404Not FoundThe requested offer or resource does not exist or is no longer active.
409ConflictThe request conflicts with the current state of a resource.
422Validation ErrorOne or more request parameters failed validation.
500Internal Server ErrorUnexpected error on the Elite Club platform.
503Service UnavailableElite Club platform is temporarily unavailable, e.g. during maintenance.

13. Error Codes

invalid_request invalid_client invalid_token token_expired ip_not_allowed client_disabled resource_not_found validation_error conflict internal_error service_unavailable offer_not_found invalid_pincode insufficient_quantity redemption_limit_exceeded membership_cap_reached

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"
  }
}

14. Security

  • OAuth 2.0 Client Credentials — the sole authentication mechanism; no end-user credentials are ever handled by Elite Club.
  • HTTPS everywhere — every environment enforces TLS; plain HTTP is rejected.
  • TLS 1.2+ — minimum supported protocol version, with modern cipher suites only.
  • IP Whitelisting — each client_id is restricted to pre-approved source IP ranges.
  • Short-lived access tokens — 1-week expiry, no long-lived or refresh tokens, limiting the blast radius of a leaked token.
  • Audit logging — every request is logged with client_id, endpoint, timestamp, and correlation_id for traceability and incident investigation.
  • Correlation IDs — the Bank should generate and send X-Correlation-Id on every request so that a single customer action can be traced across both systems.
  • Redemption audit trail — every call to /partner/v1/offers/redeem is logged with authcode, partner_user_id, and correlation_id for financial reconciliation and dispute investigation.
  • Versioning — the API is explicitly versioned in the URL (/partner/v1), so future breaking changes never silently affect existing integrations.
client_secret must be stored in a secrets manager on the Bank side and never embedded in mobile app code, logs, or client-side configuration.

15. Versioning Policy

  • Current version: /partner/v1.
  • Backwards-compatible additions (new optional fields, new endpoints) may be introduced without a version bump.
  • Breaking changes are released under a new path, e.g. /partner/v2, with advance notice to the Bank.
  • Deprecated versions remain available for an agreed transition period before retirement.

16. Best Practices

  • Cache access tokens for their full lifetime; do not request a new token on every API call.
  • Generate a fresh X-Correlation-Id per request and log it alongside your own request identifiers.
  • Read country and category filter values from the discovery lists in the listing responses — do not hardcode ids; both filters support multi-select values in a single request.
  • Use latest=1 when the Bank app needs a "latest offers" view limited to offers created in the last 90 days.
  • Treat partner_user_id as an opaque, stable identifier; do not reuse it across different customers.
  • Always branch on the success field in the response body, not solely on HTTP status.
  • Fetch full offer details lazily (only when a customer opens an offer), and use the listing endpoints for browsing.
  • Only call /partner/v1/offers/redeem once merchant staff has entered the redemption pincode at checkout — never pre-emptively, and never source it from the app user.
  • Send only the fields relevant to the offer's offer_type on redemption — omit paid_amount, check_number, guests_number, and currency for BOGOF offers.

17. Integration Checklist

Client credentials provisioned
IP allowlist configured for UAT and production
Token caching and renewal implemented
Exclusive and Premium offers listing implemented
Offer details view implemented
Redeem flow implemented for both Discount and BOGOF offers
Pagination and filtering handled
Error handling and retry/backoff implemented
Correlation id propagation verified end-to-end

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.
EndpointReturned 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_idTieroffer_typeMerchantCountryCategory
10432Exclusivediscountoutlet_id 552 — Business Bay GrillUnited Arab EmiratesRestaurants & Cafes
20115Exclusivebogofhotel_id 88 — Grand Beach ResortUnited Arab EmiratesHotels & Resorts
30678Premiumdiscountoutlet_id 781 — FitZone RiyadhSaudi ArabiaFitness & Wellness
40921Premiumdiscountoutlet_id 640 — Skyline Spa & WellnessUnited Arab EmiratesLifestyle & 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 triggerSend
422 invalid_pincodepincode: "0000"
409 insufficient_quantityquantity greater than 10
409 redemption_limit_exceededpartner_user_id: "BANKX-LIMIT-REACHED"
403 membership_cap_reachedpartner_user_id: "BANKX-CAP-REACHED"
404 offer_not_foundany offer_id not in the catalog above
200 successany 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.