# Authentication Source: https://docs.rmz.gg/embed-api/authentication OTP-based customer authentication flow for embed checkout. The Embed API uses an OTP (one-time password) flow to authenticate customers. This enables verified checkout without requiring the customer to have an existing account. ## Authentication Flow Send the customer's phone number to receive an OTP code via WhatsApp (with SMS fallback). Submit the 4-digit code. If the customer exists, they are authenticated immediately. If new, they proceed to registration. Provide name and email to create the customer account. The returned Sanctum token is used for authenticated checkout endpoints. *** ## Start Authentication Sends an OTP code to the customer's phone number via WhatsApp, falling back to SMS if WhatsApp delivery fails. ``` POST /api/embed/auth/start ``` ### Headers | Header | Required | Description | | -------------- | -------- | ----------------------------- | | `X-Embed-Key` | Yes | Your store's embed public key | | `Content-Type` | Yes | `application/json` | ### Request Body | Field | Type | Required | Description | | -------------- | ------- | -------- | ------------------------------------------------- | | `product_id` | integer | Yes | Product ID (must exist and be active) | | `country_code` | string | Yes | Phone country code (e.g., `966`, `+966`, `00966`) | | `phone` | string | Yes | Phone number without country code | ```bash cURL theme={null} curl -X POST "https://embed.rmz.gg/api/embed/auth/start" \ -H "X-Embed-Key: your_embed_public_key" \ -H "Content-Type: application/json" \ -d '{ "product_id": 42, "country_code": "966", "phone": "501234567" }' ``` ```javascript JavaScript theme={null} const response = await fetch("https://embed.rmz.gg/api/embed/auth/start", { method: "POST", headers: { "X-Embed-Key": "your_embed_public_key", "Content-Type": "application/json" }, body: JSON.stringify({ product_id: 42, country_code: "966", phone: "501234567" }) }); const { data } = await response.json(); // Store session_token for subsequent requests console.log("Session:", data.session_token); console.log("Resend available in:", data.resend_cooldown, "seconds"); ``` ```python Python theme={null} import requests response = requests.post( "https://embed.rmz.gg/api/embed/auth/start", headers={ "X-Embed-Key": "your_embed_public_key", "Content-Type": "application/json" }, json={ "product_id": 42, "country_code": "966", "phone": "501234567" } ) data = response.json()["data"] session_token = data["session_token"] ``` ```php PHP theme={null} $response = Http::withHeaders([ 'X-Embed-Key' => 'your_embed_public_key', ])->post('https://embed.rmz.gg/api/embed/auth/start', [ 'product_id' => 42, 'country_code' => '966', 'phone' => '501234567', ]); $data = $response->json()['data']; $sessionToken = $data['session_token']; ``` ### Success Response (200) ```json theme={null} { "success": true, "data": { "session_token": "abc123def456...", "expires_in": 300, "resend_cooldown": 30, "masked_phone": "+966 *****4567" } } ``` | Field | Type | Description | | ----------------- | ------- | -------------------------------------------------------- | | `session_token` | string | Session identifier for subsequent auth requests | | `expires_in` | integer | Session expiry in seconds (default: 300 = 5 minutes) | | `resend_cooldown` | integer | Seconds to wait before requesting a resend (default: 30) | | `masked_phone` | string | Partially masked phone number for display | ### Error Responses | Status | Error Code | Description | | ------ | --------------------- | --------------------------------------------------------- | | 403 | `PHONE_BLOCKED` | Phone number is blacklisted | | 429 | `RATE_LIMIT_EXCEEDED` | Too many OTP requests (includes `retry_after` in seconds) | | 422 | - | Validation error | | 404 | - | Product not found | *** ## Resend OTP Resend the verification code via SMS. The original code is replaced with a new one. ``` POST /api/embed/auth/resend ``` ### Request Body | Field | Type | Required | Description | | --------------- | ------ | -------- | ----------------------------------------- | | `session_token` | string | Yes | The session token from the start response | ```bash cURL theme={null} curl -X POST "https://embed.rmz.gg/api/embed/auth/resend" \ -H "X-Embed-Key: your_embed_public_key" \ -H "Content-Type: application/json" \ -d '{"session_token": "abc123def456..."}' ``` ```javascript JavaScript theme={null} const response = await fetch("https://embed.rmz.gg/api/embed/auth/resend", { method: "POST", headers: { "X-Embed-Key": "your_embed_public_key", "Content-Type": "application/json" }, body: JSON.stringify({ session_token: sessionToken }) }); ``` ### Success Response (200) ```json theme={null} { "success": true, "data": { "message": "Verification code sent via SMS", "resend_cooldown": 30 } } ``` ### Error Responses | Status | Error Code | Description | | ------ | --------------------- | ----------------------------------------------- | | 404 | - | Session not found | | 400 | `SESSION_EXPIRED` | Session has expired, start again | | 400 | - | Session already verified | | 429 | `COOLDOWN_ACTIVE` | Cooldown period active (includes `retry_after`) | | 429 | `RATE_LIMIT_EXCEEDED` | Too many resend attempts | Resends always go via SMS, not WhatsApp. The verification code is regenerated on each resend, so only the latest code is valid. *** ## Verify OTP Submit the 4-digit OTP code to verify the customer's phone number. ``` POST /api/embed/auth/verify ``` ### Request Body | Field | Type | Required | Description | | --------------- | ------ | -------- | ----------------------------------------- | | `session_token` | string | Yes | The session token from the start response | | `code` | string | Yes | The 4-digit verification code | ```bash cURL theme={null} curl -X POST "https://embed.rmz.gg/api/embed/auth/verify" \ -H "X-Embed-Key: your_embed_public_key" \ -H "Content-Type: application/json" \ -d '{ "session_token": "abc123def456...", "code": "1234" }' ``` ```javascript JavaScript theme={null} const response = await fetch("https://embed.rmz.gg/api/embed/auth/verify", { method: "POST", headers: { "X-Embed-Key": "your_embed_public_key", "Content-Type": "application/json" }, body: JSON.stringify({ session_token: sessionToken, code: "1234" }) }); const result = await response.json(); if (result.data.status === "authenticated") { // Existing customer - save token and proceed to checkout const token = result.data.token; } else if (result.data.status === "needs_registration") { // New customer - show registration form } ``` ### Success Response - Existing Customer (200) If the customer already exists in this store (or in another store on the platform), they are authenticated immediately: ```json theme={null} { "success": true, "data": { "status": "authenticated", "customer": { "id": 1234, "firstName": "Ahmed", "lastName": "Ali", "email": "ahmed@example.com", "phone": "501234567", "country_code": "966" }, "token": "1|abc123xyz..." } } ``` ### Success Response - New Customer (200) If no customer record exists for this phone number, the response indicates registration is needed: ```json theme={null} { "success": true, "data": { "status": "needs_registration", "session_token": "abc123def456..." } } ``` ### Error Responses | Status | Error Code | Description | | ------ | ---------------------- | -------------------------------------------------------------------------- | | 404 | - | Session not found | | 400 | `SESSION_EXPIRED` | Session has expired | | 400 | - | Session already verified | | 400 | `INVALID_CODE` | Wrong code (includes `remaining_attempts` and optional `cooldown_seconds`) | | 400 | `MAX_ATTEMPTS_REACHED` | Too many failed attempts, request a new code | | 429 | `COOLDOWN_ACTIVE` | Must wait before retrying (includes `retry_after`) | **Invalid code response example:** ```json theme={null} { "success": false, "message": "Invalid verification code", "error_code": "INVALID_CODE", "remaining_attempts": 2, "cooldown_seconds": null } ``` Customers get 3 attempts per OTP code. After exhausting all attempts, they must request a new code via the resend endpoint. *** ## Complete Registration Register a new customer after successful OTP verification. Only callable when the verify response returned `status: "needs_registration"`. ``` POST /api/embed/auth/complete ``` ### Request Body | Field | Type | Required | Description | | --------------- | ------ | -------- | ---------------------------------------- | | `session_token` | string | Yes | The verified session token | | `firstName` | string | Yes | Customer first name (max 100 characters) | | `lastName` | string | Yes | Customer last name (max 100 characters) | | `email` | string | Yes | Customer email address | ```bash cURL theme={null} curl -X POST "https://embed.rmz.gg/api/embed/auth/complete" \ -H "X-Embed-Key: your_embed_public_key" \ -H "Content-Type: application/json" \ -d '{ "session_token": "abc123def456...", "firstName": "Ahmed", "lastName": "Ali", "email": "ahmed@example.com" }' ``` ```javascript JavaScript theme={null} const response = await fetch("https://embed.rmz.gg/api/embed/auth/complete", { method: "POST", headers: { "X-Embed-Key": "your_embed_public_key", "Content-Type": "application/json" }, body: JSON.stringify({ session_token: sessionToken, firstName: "Ahmed", lastName: "Ali", email: "ahmed@example.com" }) }); const { data } = await response.json(); const token = data.token; // Save for checkout ``` ### Success Response (200) ```json theme={null} { "success": true, "data": { "customer": { "id": 5678, "firstName": "Ahmed", "lastName": "Ali", "email": "ahmed@example.com", "phone": "501234567", "country_code": "966" }, "token": "1|xyz789abc..." } } ``` ### Error Responses | Status | Description | | ------ | -------------------------------------------------------- | | 400 | Invalid or expired session | | 400 | Registration not allowed for this session (already used) | | 422 | Validation error (missing fields, invalid email) | *** ## Validate Token Check if a previously saved token is still valid for a specific product. Use this to restore a customer's session without re-authenticating. ``` POST /api/embed/auth/validate ``` ### Headers | Header | Required | Description | | --------------- | -------- | ----------------------------- | | `Authorization` | Yes | `Bearer ` | | `X-Embed-Key` | Yes | Your store's embed public key | ### Request Body | Field | Type | Required | Description | | ------------ | ------- | -------- | -------------------------------------------- | | `product_id` | integer | Yes | The product ID to validate the token against | ```bash cURL theme={null} curl -X POST "https://embed.rmz.gg/api/embed/auth/validate" \ -H "Authorization: Bearer 1|abc123xyz..." \ -H "X-Embed-Key: your_embed_public_key" \ -H "Content-Type: application/json" \ -d '{"product_id": 42}' ``` ```javascript JavaScript theme={null} const response = await fetch("https://embed.rmz.gg/api/embed/auth/validate", { method: "POST", headers: { "Authorization": `Bearer ${savedToken}`, "X-Embed-Key": "your_embed_public_key", "Content-Type": "application/json" }, body: JSON.stringify({ product_id: 42 }) }); const { data } = await response.json(); if (data.valid) { // Token is valid, skip auth flow console.log("Welcome back,", data.customer.firstName); } ``` ### Success Response (200) ```json theme={null} { "success": true, "data": { "valid": true, "customer": { "id": 1234, "firstName": "Ahmed", "lastName": "Ali", "email": "ahmed@example.com", "phone": "501234567", "country_code": "966" } } } ``` ### Error Responses | Status | Description | | ------ | ------------------------------------------------------------------------ | | 401 | Invalid or expired token | | 403 | Token not valid for this product (customer belongs to a different store) | | 422 | Invalid product ID | Store the authentication token in `localStorage` and call this endpoint when the embed widget loads. If the token is valid, skip the OTP flow entirely and go straight to checkout. # Checkout Source: https://docs.rmz.gg/embed-api/checkout Create checkout sessions and initiate payments via the Embed API. The Embed API supports two checkout modes: **authenticated checkout** (using a Sanctum token from the OTP flow) and **guest checkout** (providing customer details directly). Both modes create a checkout session and return payment options. ## Authenticated Checkout Create a checkout session for an authenticated customer. Customer information is retrieved from the token. ``` POST /api/embed/checkout ``` ### Headers | Header | Required | Description | | --------------- | -------- | ----------------------------------------------------- | | `Authorization` | Yes | `Bearer ` (must have `embed:checkout` ability) | | `X-Embed-Key` | Yes | Your store's embed public key | | `Content-Type` | Yes | `application/json` | ### Request Body | Field | Type | Required | Description | | ------------------- | ------- | -------- | ------------------------------------------------------------ | | `product_id` | integer | Yes | The product ID | | `quantity` | integer | Yes | Purchase quantity (min: 1) | | `coupon_code` | string | No | Coupon code to apply | | `customer_note` | string | No | Customer note (max 500 characters) | | `notice` | string | No | Alternative field for customer note | | `subscription_plan` | integer | No | Subscription variant ID (required for subscription products) | | `fields` | object | No | Custom product field values (keyed by field index) | ```bash cURL theme={null} curl -X POST "https://embed.rmz.gg/api/embed/checkout" \ -H "Authorization: Bearer 1|abc123xyz..." \ -H "X-Embed-Key: your_embed_public_key" \ -H "Content-Type: application/json" \ -d '{ "product_id": 42, "quantity": 1, "coupon_code": "SAVE20" }' ``` ```javascript JavaScript theme={null} const response = await fetch("https://embed.rmz.gg/api/embed/checkout", { method: "POST", headers: { "Authorization": `Bearer ${token}`, "X-Embed-Key": "your_embed_public_key", "Content-Type": "application/json" }, body: JSON.stringify({ product_id: 42, quantity: 1, coupon_code: "SAVE20" }) }); const { data } = await response.json(); if (data.type === "free_order") { // Order created, no payment needed console.log("Order created:", data.order_id); } else { // Redirect to payment or show payment methods console.log("Payment methods:", data.payment_methods); } ``` ```python Python theme={null} import requests response = requests.post( "https://embed.rmz.gg/api/embed/checkout", headers={ "Authorization": f"Bearer {token}", "X-Embed-Key": "your_embed_public_key", "Content-Type": "application/json" }, json={ "product_id": 42, "quantity": 1, "coupon_code": "SAVE20" } ) data = response.json()["data"] ``` ```php PHP theme={null} $response = Http::withHeaders([ 'Authorization' => "Bearer {$token}", 'X-Embed-Key' => 'your_embed_public_key', ])->post('https://embed.rmz.gg/api/embed/checkout', [ 'product_id' => 42, 'quantity' => 1, 'coupon_code' => 'SAVE20', ]); $data = $response->json()['data']; ``` ### Success Response - Payment Required (200) ```json theme={null} { "success": true, "data": { "type": "payment_required", "checkout_id": 789, "checkout_url": "ck_abc123", "payment_url": "https://store.rmz.gg/checkout/ck_abc123", "amount": "126.50", "subtotal": "149.00", "discount_amount": "29.80", "tax_amount": "17.88", "payment_methods": [ { "id": "card", "name_ar": "بطاقة ائتمان", "name_en": "Credit Card", "icon": "card" }, { "id": "mada", "name_ar": "مدى", "name_en": "Mada", "icon": "mada" }, { "id": "applepay", "name_ar": "Apple Pay", "name_en": "Apple Pay", "icon": "applepay" } ] } } ``` ### Success Response - Free Order (200) When the total is zero (e.g., 100% discount coupon), the order is created immediately: ```json theme={null} { "success": true, "data": { "type": "free_order", "order_id": 456, "message": "Order created successfully" } } ``` ### Available Payment Methods | ID | Description | | ---------- | ----------------------------- | | `card` | Credit Card (Visa/Mastercard) | | `mada` | Mada debit card | | `stcpay` | STC Pay | | `applepay` | Apple Pay | | `paypal` | PayPal | | `bank` | Bank Transfer | | `coinbase` | Cryptocurrency | Available payment methods depend on the store's configuration and the order amount. Not all methods are available for every store. *** ## Guest Checkout Create a checkout session without OTP authentication. Customer details are provided directly in the request body. ``` POST /api/embed/guest/checkout ``` ### Headers | Header | Required | Description | | -------------- | -------- | ----------------------------- | | `X-Embed-Key` | Yes | Your store's embed public key | | `Content-Type` | Yes | `application/json` | ### Request Body | Field | Type | Required | Description | | ----------------------- | ------- | -------- | ---------------------------------- | | `product_id` | integer | Yes | The product ID | | `quantity` | integer | Yes | Purchase quantity (min: 1) | | `customer_first_name` | string | Yes | Customer first name (max 100) | | `customer_last_name` | string | Yes | Customer last name (max 100) | | `customer_email` | string | Yes | Customer email address | | `customer_phone` | string | Yes | Customer phone number (max 20) | | `customer_country_code` | string | Yes | Phone country code (max 10) | | `coupon_code` | string | No | Coupon code to apply | | `customer_note` | string | No | Customer note (max 500 characters) | | `fields` | object | No | Custom product field values | ```bash cURL theme={null} curl -X POST "https://embed.rmz.gg/api/embed/guest/checkout" \ -H "X-Embed-Key: your_embed_public_key" \ -H "Content-Type: application/json" \ -d '{ "product_id": 42, "quantity": 1, "customer_first_name": "Ahmed", "customer_last_name": "Ali", "customer_email": "ahmed@example.com", "customer_phone": "501234567", "customer_country_code": "966" }' ``` ```javascript JavaScript theme={null} const response = await fetch("https://embed.rmz.gg/api/embed/guest/checkout", { method: "POST", headers: { "X-Embed-Key": "your_embed_public_key", "Content-Type": "application/json" }, body: JSON.stringify({ product_id: 42, quantity: 1, customer_first_name: "Ahmed", customer_last_name: "Ali", customer_email: "ahmed@example.com", customer_phone: "501234567", customer_country_code: "966" }) }); const { data } = await response.json(); ``` The response format is identical to the authenticated checkout response above. *** ## Initiate Payment After creating a checkout session, initiate the actual payment with the customer's chosen payment method. ### Authenticated Payment ``` POST /api/embed/payment/initiate ``` Requires `Authorization: Bearer ` with `embed:checkout` ability. ### Guest Payment ``` POST /api/embed/guest/payment/initiate ``` No authentication required. ### Request Body (Both Endpoints) | Field | Type | Required | Description | | ---------------- | ------ | -------- | -------------------------------------------------------------------------- | | `checkout_url` | string | Yes | The `checkout_url` from the checkout response | | `payment_method` | string | Yes | One of: `card`, `mada`, `stcpay`, `applepay`, `paypal`, `bank`, `coinbase` | ```bash cURL theme={null} curl -X POST "https://embed.rmz.gg/api/embed/payment/initiate" \ -H "Authorization: Bearer 1|abc123xyz..." \ -H "X-Embed-Key: your_embed_public_key" \ -H "Content-Type: application/json" \ -d '{ "checkout_url": "ck_abc123", "payment_method": "card" }' ``` ```javascript JavaScript theme={null} const response = await fetch("https://embed.rmz.gg/api/embed/payment/initiate", { method: "POST", headers: { "Authorization": `Bearer ${token}`, "X-Embed-Key": "your_embed_public_key", "Content-Type": "application/json" }, body: JSON.stringify({ checkout_url: "ck_abc123", payment_method: "card" }) }); const { data } = await response.json(); if (data.type === "redirect") { window.location.href = data.redirect_url; } else if (data.type === "bank_transfer") { // Show bank details to customer console.log("Bank:", data.bank_details); } ``` ### Response Types The response varies by payment method: **Card, Mada, STC Pay, Apple Pay, Coinbase, PayPal (with PayPal.me link)** ```json theme={null} { "success": true, "data": { "type": "redirect", "redirect_url": "https://..." } } ``` Redirect the customer to `redirect_url` to complete payment. **PayPal (manual, email only)** ```json theme={null} { "success": true, "data": { "type": "paypal_manual", "paypal_email": "merchant@paypal.com", "amount": 149.00, "checkout_url": "ck_abc123" } } ``` **Bank Transfer** ```json theme={null} { "success": true, "data": { "type": "bank_transfer", "bank_details": { "bank_name": "Al Rajhi Bank", "account_name": "Store Owner Name", "account_number": "1234567890", "iban": "SA1234567890123456789012" }, "amount": 149.00, "checkout_url": "ck_abc123" } } ``` ### Error Responses | Status | Description | | ------ | -------------------------------------------- | | 404 | Checkout session expired or not found | | 400 | Order already created for this checkout | | 400 | Payment method not configured for this store | | 422 | Invalid payment method | Checkout sessions expire after a period of time. If a customer takes too long, you may need to create a new checkout session. *** ## Product Fields Products can have custom fields that customers fill in during checkout. Pass field values in the `fields` object, keyed by the field's array index from the product data. ### Field Types | Type | Value Format | Notes | | ---------- | ------------ | --------------------------------------------------------------------- | | `text` | string | Free text input | | `number` | number | Numeric input | | `date` | string | Date string | | `datetime` | string | Date and time string | | `color` | string | Color value | | `select` | integer | Index of the selected option (may add to price) | | `image` | file | Upload via `field_files.{index}` (JPG, PNG, GIF, max 10MB) | | `file` | file | Upload via `field_files.{index}` (PDF, DOC, DOCX, TXT, ZIP, max 20MB) | Select fields can include price add-ons. The add-on amount is added to the unit price before calculating the total. Check the product info response to see if options have prices. # Coupons Source: https://docs.rmz.gg/embed-api/coupons Apply coupon codes and calculate discounts for embed checkout. Validate a coupon code against a specific product and quantity, and get the calculated discount amount. ## Apply Coupon This is a public endpoint. It requires the `X-Embed-Key` header but no customer authentication. ``` POST /api/embed/apply-coupon ``` ### Headers | Header | Required | Description | | -------------- | -------- | ----------------------------- | | `X-Embed-Key` | Yes | Your store's embed public key | | `Content-Type` | Yes | `application/json` | ### Request Body | Field | Type | Required | Description | | ------------ | ------- | -------- | ------------------------------------- | | `code` | string | Yes | The coupon code to validate | | `product_id` | integer | Yes | The product ID to apply the coupon to | | `quantity` | integer | Yes | The purchase quantity (min: 1) | ### Example Request ```bash cURL theme={null} curl -X POST "https://embed.rmz.gg/api/embed/apply-coupon" \ -H "X-Embed-Key: your_embed_public_key" \ -H "Content-Type: application/json" \ -d '{ "code": "SAVE20", "product_id": 42, "quantity": 1 }' ``` ```javascript JavaScript theme={null} const response = await fetch("https://embed.rmz.gg/api/embed/apply-coupon", { method: "POST", headers: { "X-Embed-Key": "your_embed_public_key", "Content-Type": "application/json" }, body: JSON.stringify({ code: "SAVE20", product_id: 42, quantity: 1 }) }); const { data } = await response.json(); console.log("Discount:", data.discount_amount); ``` ```python Python theme={null} import requests response = requests.post( "https://embed.rmz.gg/api/embed/apply-coupon", headers={ "X-Embed-Key": "your_embed_public_key", "Content-Type": "application/json" }, json={ "code": "SAVE20", "product_id": 42, "quantity": 1 } ) data = response.json()["data"] print(f"Discount: {data['discount_amount']}") ``` ```php PHP theme={null} $response = Http::withHeaders([ 'X-Embed-Key' => 'your_embed_public_key', ])->post('https://embed.rmz.gg/api/embed/apply-coupon', [ 'code' => 'SAVE20', 'product_id' => 42, 'quantity' => 1, ]); $data = $response->json()['data']; echo "Discount: " . $data['discount_amount']; ``` ### Success Response (200) ```json theme={null} { "success": true, "data": { "coupon_id": 15, "code": "SAVE20", "type": "percentage", "amount": 20.0, "discount_amount": 29.80, "payment_restrictions": [] } } ``` ### Response Fields | Field | Type | Description | | ---------------------- | ------- | ------------------------------------------------------------------------------------ | | `coupon_id` | integer | Internal coupon ID | | `code` | string | The coupon code | | `type` | string | Coupon type: `percentage` or `fixed` | | `amount` | float | The coupon value (percentage or fixed amount) | | `discount_amount` | float | The calculated discount for the given product and quantity | | `payment_restrictions` | array | List of payment methods this coupon is restricted to (empty array = no restrictions) | If `payment_restrictions` is not empty, the customer can only use the listed payment methods when this coupon is applied. Display the restrictions in your UI to avoid confusion at checkout. ### Error Responses **Validation error (422)** ```json theme={null} { "success": false, "message": "Invalid request", "errors": { "code": ["The code field is required."], "product_id": ["The product id field is required."], "quantity": ["The quantity field is required."] } } ``` **Invalid coupon (400)** ```json theme={null} { "success": false, "message": "Invalid coupon" } ``` **Coupon expired or inactive (400)** ```json theme={null} { "success": false, "message": "Coupon is not active" } ``` **Coupon usage limit reached (400)** ```json theme={null} { "success": false, "message": "Coupon has reached maximum uses" } ``` **Minimum cart value not met (400)** ```json theme={null} { "success": false, "message": "Minimum order value is 100" } ``` **Product not eligible (400)** ```json theme={null} { "success": false, "message": "This coupon is not valid for this product" } ``` # Embed API Overview Source: https://docs.rmz.gg/embed-api/overview Embed a checkout widget for any RMZ product on external websites using the Embed API. RMZ provides two ways to embed checkout on external websites: 1. **Widget script** (recommended) — a single script tag + button with data attributes. The widget handles the entire UI and purchase flow automatically. 2. **Direct API** — HTTP endpoints for building a fully custom checkout experience. Most integrations should use the **widget script**. See the [Embedding Checkout guide](/guides/embedding-checkout) for a complete walkthrough. ## Widget Script (Recommended) Add the script and a button to any HTML page: ```html theme={null} ``` | Attribute | Required | Description | | ------------------ | -------- | -------------------------------------------- | | `data-rmz-product` | Yes | Product ID from your dashboard | | `data-rmz-key` | Yes | Embed public key (starts with `rmz_pk_`) | | `data-rmz-theme` | No | `auto`, `light`, or `dark` (default: `auto`) | Configure your embed key and allowed domains in **Dashboard > Settings > Embed**. *** ## Direct API If you need full control over the UI, use the API endpoints below. ### Base URL ``` https://embed.rmz.gg/api/embed ``` ### Use Cases * Build a fully custom checkout UI * Integrate embed purchasing into a native mobile app * Create a headless embed flow with your own design ## Authentication The Embed API uses two authentication mechanisms depending on the endpoint: ### 1. Embed Key (Public Endpoints) All requests must include your store's embed public key. Pass it as a header or query parameter: ``` X-Embed-Key: your_embed_public_key ``` You can find your embed key in **Dashboard > Settings > Embed**. The embed key is public and safe to expose in client-side code. It only grants access to embed-scoped endpoints and is validated against your store's allowed origins. ### 2. Sanctum Token (Authenticated Endpoints) After a customer completes the OTP authentication flow, they receive a Sanctum Bearer token with limited abilities (`embed:checkout`, `embed:validate`). This token is used for authenticated checkout and token validation endpoints. ``` Authorization: Bearer 1|abc123xyz... X-Embed-Key: your_embed_public_key ``` Embed tokens are scoped to embed-only abilities. They cannot be used to access Storefront API or other platform endpoints. ## Origin Validation When you configure allowed origins in your embed settings, the API validates the `Origin` or `Referer` header of every request against that list. Requests from unauthorized domains are rejected with a `403` status. If no allowed origins are configured, requests from any origin are accepted. ## Rate Limits The Embed API enforces multi-layer rate limiting to prevent abuse: | Scope | Limit | Window | | ------ | -------------- | -------- | | Per IP | 30 requests | 1 minute | | Per IP | 200 requests | 1 hour | | Per IP | 500 requests | 1 day | | Global | 5,000 requests | 1 minute | Rate limit information is included in response headers: ``` X-RateLimit-Limit: 30 X-RateLimit-Remaining: 27 Retry-After: 60 ``` When a rate limit is exceeded, the API returns `429 Too Many Requests`: ```json theme={null} { "success": false, "message": "Too many requests. Please try again later.", "error_code": "RATE_LIMIT_EXCEEDED", "retry_after": 60 } ``` ### Additional Auth Rate Limits The OTP authentication endpoints have separate, stricter rate limits: | Scope | Limit | Window | | -------------------- | ----------- | ---------- | | OTP start per IP | 50 requests | 1 day | | OTP start per phone | 10 requests | 1 day | | OTP resend per IP | 20 requests | 1 day | | OTP resend per phone | 3 requests | 10 minutes | ## CORS Embed endpoints return permissive CORS headers since they are designed to be called from external websites via iframes or JavaScript: ``` Access-Control-Allow-Origin: Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS Access-Control-Allow-Headers: Content-Type, Authorization, X-Embed-Key, X-Requested-With, Accept, Origin Access-Control-Allow-Credentials: false Access-Control-Max-Age: 86400 Access-Control-Expose-Headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After Vary: Origin ``` Preflight `OPTIONS` requests return `204 No Content` with the appropriate headers. ## Response Format All Embed API endpoints return JSON with a consistent structure: ```json theme={null} { "success": true, "data": { ... } } ``` Error responses include an error code and message: ```json theme={null} { "success": false, "message": "Human-readable error message", "error_code": "MACHINE_READABLE_CODE" } ``` ## Endpoints at a Glance Fetch product details for the embed widget. Apply coupon codes and calculate discounts. OTP-based customer authentication flow. Create checkout sessions and initiate payments. Check whether a payment has completed. # Payment Status Source: https://docs.rmz.gg/embed-api/payment-status Check whether a payment has been completed for a checkout session. After initiating a payment and redirecting the customer, poll this endpoint to determine when the payment is confirmed and the order is created. ## Check Payment Status This is a public endpoint. It does not require the `X-Embed-Key` header or authentication. ``` GET /api/embed/payment/status/{checkoutUrl} ``` ### Path Parameters | Parameter | Type | Description | | ------------- | ------ | --------------------------------------------------- | | `checkoutUrl` | string | The `checkout_url` value from the checkout response | ### Example Request ```bash cURL theme={null} curl -X GET "https://embed.rmz.gg/api/embed/payment/status/ck_abc123" \ -H "Accept: application/json" ``` ```javascript JavaScript theme={null} const response = await fetch( "https://embed.rmz.gg/api/embed/payment/status/ck_abc123" ); const { data } = await response.json(); if (data.is_paid) { console.log("Payment complete! Order ID:", data.order_id); } else { console.log("Payment pending..."); } ``` ```python Python theme={null} import requests response = requests.get( "https://embed.rmz.gg/api/embed/payment/status/ck_abc123" ) data = response.json()["data"] if data["is_paid"]: print(f"Order created: {data['order_id']}") ``` ```php PHP theme={null} $response = Http::get('https://embed.rmz.gg/api/embed/payment/status/ck_abc123'); $data = $response->json()['data']; if ($data['is_paid']) { echo "Order ID: " . $data['order_id']; } ``` ### Success Response (200) **Payment completed:** ```json theme={null} { "success": true, "data": { "is_paid": true, "order_id": 456 } } ``` **Payment pending:** ```json theme={null} { "success": true, "data": { "is_paid": false, "order_id": null } } ``` ### Response Fields | Field | Type | Description | | ---------- | --------------- | --------------------------------------------------------- | | `is_paid` | boolean | `true` if payment was successful and an order was created | | `order_id` | integer \| null | The order ID if payment is complete, otherwise `null` | ### Error Response **Checkout not found (404)** ```json theme={null} { "success": false, "message": "Checkout not found" } ``` ## Polling Strategy After redirecting the customer to the payment page, poll this endpoint to detect when payment completes: ```javascript theme={null} async function waitForPayment(checkoutUrl, maxAttempts = 60) { for (let i = 0; i < maxAttempts; i++) { const response = await fetch( `https://embed.rmz.gg/api/embed/payment/status/${checkoutUrl}` ); const { data } = await response.json(); if (data.is_paid) { return data.order_id; } // Wait 3 seconds between polls await new Promise(resolve => setTimeout(resolve, 3000)); } throw new Error("Payment timeout"); } ``` Poll every 2-3 seconds. For card payments (Mada, Visa, Apple Pay), confirmation is typically near-instant after the customer completes the payment page. For bank transfers and cryptocurrency, it may take longer. This endpoint does not require authentication or an embed key, so the checkout URL should be treated as a secret. Do not expose it in URLs that could be shared publicly. # Product Info Source: https://docs.rmz.gg/embed-api/product-info Fetch product details for the embed checkout widget. Retrieve product information to display in your embedded checkout widget. Returns pricing, stock, images, store details, and tax settings. ## Get Product This is a public endpoint. It requires the `X-Embed-Key` header but no customer authentication. ``` GET /api/embed/product/{productId} ``` ### Path Parameters | Parameter | Type | Description | | ----------- | ------- | ------------------------------ | | `productId` | integer | The ID of the product to fetch | ### Headers | Header | Required | Description | | ------------- | -------- | ----------------------------- | | `X-Embed-Key` | Yes | Your store's embed public key | ### Example Request ```bash cURL theme={null} curl -X GET "https://embed.rmz.gg/api/embed/product/42" \ -H "X-Embed-Key: your_embed_public_key" \ -H "Accept: application/json" ``` ```javascript JavaScript theme={null} const response = await fetch("https://embed.rmz.gg/api/embed/product/42", { headers: { "X-Embed-Key": "your_embed_public_key", "Accept": "application/json" } }); const { data } = await response.json(); console.log(data.name, data.actual_price); ``` ```python Python theme={null} import requests response = requests.get( "https://embed.rmz.gg/api/embed/product/42", headers={ "X-Embed-Key": "your_embed_public_key", "Accept": "application/json" } ) data = response.json()["data"] print(data["name"], data["actual_price"]) ``` ```php PHP theme={null} $response = Http::withHeaders([ 'X-Embed-Key' => 'your_embed_public_key', ])->get('https://embed.rmz.gg/api/embed/product/42'); $data = $response->json()['data']; echo $data['name'] . ' - ' . $data['actual_price']; ``` ### Success Response (200) ```json theme={null} { "success": true, "data": { "id": 42, "name": "Premium Digital Course", "description": "A comprehensive guide to...", "price": 199.00, "discount_price": 149.00, "actual_price": 149.00, "is_discounted": true, "image": "https://cdn.rmz.gg/products/abc123.jpg", "extra_images": [ "https://cdn.rmz.gg/products/img2.jpg", "https://cdn.rmz.gg/products/img3.jpg" ], "stock": 50, "in_stock": true, "type": "code", "min_qty": 1, "max_qty": 10, "store": { "id": 7, "name": "My Digital Store", "logo": "https://cdn.rmz.gg/stores/logo.png", "currency": "SAR", "language": "ar" }, "tax_enabled": true, "tax_rate": 15.0 } } ``` ### Response Fields | Field | Type | Description | | ---------------- | -------------- | ------------------------------------------------------------------------------------------ | | `id` | integer | Product ID | | `name` | string | Product display name | | `description` | string | Product description (may contain HTML) | | `price` | float | Original listed price | | `discount_price` | float \| null | Discounted price, or `null` if no discount | | `actual_price` | float | The effective price the customer pays (discount price if active, otherwise original price) | | `is_discounted` | boolean | Whether a discount is currently active | | `image` | string \| null | Primary product image URL via CDN | | `extra_images` | array | Additional product image URLs | | `stock` | integer | Number of items available | | `in_stock` | boolean | Whether stock is greater than zero | | `type` | string | Product type: `code`, `file`, `subscription`, `service`, `card` | | `min_qty` | integer | Minimum purchase quantity | | `max_qty` | integer | Maximum purchase quantity (capped at stock for `code` products, max 10) | | `store.id` | integer | Store ID | | `store.name` | string | Store display name | | `store.logo` | string | Store logo URL | | `store.currency` | string | Store currency code (e.g., `SAR`, `USD`) | | `store.language` | string | Store language (`ar`, `en`) | | `tax_enabled` | boolean | Whether tax is enabled for this store | | `tax_rate` | float | Tax rate as a percentage (e.g., `15.0` for 15% VAT) | ### Error Responses **Product not found (404)** ```json theme={null} { "success": false, "message": "Product not found" } ``` Only active products (status = enabled) are returned. Disabled or draft products will return a 404 response. # Configuration Source: https://docs.rmz.gg/fivem/configuration All config.lua settings for the RMZ FiveM resource, including polling behavior and debug mode. All configuration is in the `config.lua` file in the resource root directory. ## Settings Reference | Setting | Type | Default | Description | | --------------------- | ------- | -------------------------------- | --------------------------------------------------------------------------------- | | `Config.SecretKey` | string | `''` (required) | Secret key from your RMZ dashboard. Get it from **Benefits** > **FiveM Servers**. | | `Config.ApiUrl` | string | `https://fivem.rmz.gg/api/fivem` | RMZ API URL. Do not change unless instructed by RMZ support. | | `Config.PollInterval` | number | `10` | Seconds between each poll for new commands. Minimum: 5. Recommended: 10-15. | | `Config.Debug` | boolean | `false` | Enable verbose debug messages in the server console. Useful for troubleshooting. | | `Config.NotifyPlayer` | boolean | `true` | Send an in-game notification to the player when a command is executed for them. | ## Full config.lua ```lua theme={null} Config = {} -- RMZ API Configuration -- Get your secret key from: RMZ Dashboard > Benefits > FiveM Servers Config.SecretKey = 'rmz_fivem_YOUR_SECRET_KEY_HERE' -- RMZ API URL (do not change unless instructed) Config.ApiUrl = 'https://fivem.rmz.gg/api/fivem' -- Poll interval in seconds (how often to check for new commands) -- Minimum: 5, Recommended: 10-15 Config.PollInterval = 10 -- Enable debug prints in server console Config.Debug = false -- Enable player notifications when commands are executed Config.NotifyPlayer = true ``` ## Polling Behavior * The resource polls the RMZ API every `Config.PollInterval` seconds for pending commands * Commands are executed immediately via the server console * After execution, RMZ is notified of the result (success or failure) * If the confirmation callback fails, commands will be retried on the next poll cycle Setting `PollInterval` too low (below 5 seconds) may trigger rate limiting (30 requests per minute). A value of 10-15 seconds works well for most servers. ## Online-Required Commands If you enable **"Execute only when player is online"** in the benefit settings on the RMZ dashboard: * Commands are **not** executed during regular polling cycles * When the target player joins the server, their pending commands are fetched and executed immediately * This is useful for commands that require the player to be present (e.g. giving items directly to their inventory) ## Security * The secret key is sent only in the `X-Fivem-Secret` HTTP header -- never exposed in URLs or logs * You can restrict API access to a specific server IP address from the RMZ dashboard * You can regenerate the secret key at any time if it is compromised Never share your secret key. Do not commit your modified `config.lua` to a public repository. Add `config.lua` to your `.gitignore`. # Installation Source: https://docs.rmz.gg/fivem/installation Download, configure, and start the RMZ FiveM resource on your game server. ## Installation Steps Choose one of the following methods: **Option A -- Git clone:** ```bash theme={null} cd resources git clone https://github.com/Rmz-App/rmz-fivem.git rmz-fivem ``` **Option B -- Download ZIP:** 1. Go to [github.com/Rmz-App/rmz-fivem](https://github.com/Rmz-App/rmz-fivem) 2. Click the green **Code** button and select **Download ZIP** 3. Extract the folder into your server's `resources/` directory 4. Rename the folder to `rmz-fivem` if needed 1. Go to your [RMZ dashboard](https://app.rmz.gg) 2. Navigate to **Benefits** > **FiveM Servers** 3. Click **Add Server** 4. Copy the generated secret key Open `config.lua` in the resource folder and set your secret key: ```lua theme={null} Config.SecretKey = 'rmz_fivem_YOUR_SECRET_KEY_HERE' ``` See [Configuration](/fivem/configuration) for all available settings. Add the following line to your `server.cfg`: ``` ensure rmz-fivem ``` Restart the server (or run `restart rmz-fivem` in the console). You should see the following output in your server console: ``` [RMZ] ============================================= [RMZ] RMZ FiveM Integration v1.0.0 [RMZ] Poll interval: 10s [RMZ] Debug: false [RMZ] Notifications: true [RMZ] ============================================= ``` If you see this output, the resource is running and polling for commands. ## Verify the Connection After starting the resource, check the **FiveM Servers** page in your RMZ dashboard: * **Connected** (green) -- the resource is polling successfully * **Disconnected** (grey) -- no poll received in the last 2 minutes Never share your secret key or commit your modified `config.lua` to a public repository. Add it to your `.gitignore` file. ## Next Steps Adjust poll interval, debug mode, and notifications Create products with FiveM commands # FiveM Integration Overview Source: https://docs.rmz.gg/fivem/overview Automatically execute server commands when customers purchase products from your RMZ store. Deliver VIP, cash, vehicles, and more to players instantly. The RMZ FiveM integration is a server resource that connects your FiveM game server to your RMZ store. When a customer buys a product, the configured commands are automatically executed on your server -- delivering items, ranks, currency, or anything else your server supports. ## How It Works A customer buys a product from your RMZ store and enters their FiveM ID (Steam ID, license, etc.) at checkout via a custom field. Based on the FiveM benefit attached to the product, RMZ queues the configured server commands with the customer's details. The `rmz-fivem` resource running on your server polls the RMZ API every few seconds for pending commands. Pending commands are executed via the server console, and the player receives an in-game notification. ## Requirements | Requirement | Details | | ------------------- | --------------------------------------------------- | | **RMZ Plan** | Plus+ subscription or higher | | **FiveM Server** | Running a recent build | | **Internet Access** | Server must be able to reach `https://fivem.rmz.gg` | ## What You Can Do * Deliver VIP ranks, in-game currency, vehicles, items, or any custom command * Automatically revoke benefits when orders are refunded or cancelled * Queue commands for offline players and execute them when they join * Monitor connection status from your RMZ dashboard ## Resources Download, configure, and start the resource All config.lua settings explained Create products with FiveM benefits and commands Common errors and debug mode GitHub repository: [github.com/Rmz-App/rmz-fivem](https://github.com/Rmz-App/rmz-fivem) ## File Structure ``` rmz-fivem/ ├── fxmanifest.lua # Resource manifest ├── config.lua # Configuration (secret key, API URL, etc.) ├── server/ │ └── main.lua # Polls API and executes commands ├── client/ │ └── main.lua # In-game player notifications └── README.md ``` # Product Setup Source: https://docs.rmz.gg/fivem/product-setup Create products with FiveM benefits, configure commands with variables, and set up revoke actions. To deliver items, ranks, or currency to players when they purchase from your store, you need to set up a product with a custom field for the player's FiveM ID and attach a FiveM benefit with the commands to execute. ## Step 1: Add a Custom Field When creating or editing a product in your RMZ dashboard, add a custom field that asks the customer for their FiveM identifier. | Field Setting | Example Value | | ------------- | ------------------------ | | Field name | `FiveM ID` or `Steam ID` | | Field type | Text input | | Required | Yes | The customer fills this in at checkout. The value is available in your commands as the `{player_id}` variable. ## Step 2: Create a FiveM Benefit 1. In your dashboard, go to **Benefits** > **Create Benefit** 2. Select type **FiveM Commands** 3. Select the target server(s) where commands should execute 4. Enter the commands to run when a purchase is completed ## Command Variables Use these variables in your commands -- they are replaced automatically with the actual values at execution time: | Variable | Description | Example Value | | ---------------- | --------------------------------------- | ----------------------- | | `{player_id}` | Player identifier from the custom field | `steam:110000xxxxxxxxx` | | `{order_id}` | RMZ order ID | `1234` | | `{product_id}` | Product ID | `56` | | `{product_name}` | Product name | `VIP Package` | | `{quantity}` | Purchased quantity | `1` | ## Command Examples ``` /give_vip {player_id} /add_cash {player_id} 5000 /add_car {player_id} adder /set_rank {player_id} vip {quantity} ``` You can add multiple commands per benefit. They execute in order. Each command is run via the server console, so use the same syntax you would type into the server console manually. ## Revoke Commands (Optional) Define commands that execute automatically when an order is **refunded** or **cancelled**. This lets you undo the benefits that were given. ``` /remove_vip {player_id} /remove_cash {player_id} 5000 ``` Revoke commands use the same variables as regular commands. They are executed on the same server(s) configured in the benefit. ## Step 3: Attach the Benefit to Your Product After creating the FiveM benefit: 1. Go to the product settings 2. In the **Benefits** section, attach the FiveM benefit you created 3. Save the product When a customer purchases this product and the order is completed, the commands will be queued and executed on your FiveM server. ## Full Example A "VIP Package" product setup: | Configuration | Value | | ------------------- | -------------------------------------------------------------- | | **Product name** | VIP Package | | **Custom field** | "FiveM ID" (required text field) | | **Benefit type** | FiveM Commands | | **Target server** | My FiveM Server | | **Commands** | `/give_vip {player_id}` and `/add_cash {player_id} 10000` | | **Revoke commands** | `/remove_vip {player_id}` and `/remove_cash {player_id} 10000` | When a customer enters their Steam ID at checkout and completes the purchase, your server runs: ``` /give_vip steam:110000xxxxxxxxx /add_cash steam:110000xxxxxxxxx 10000 ``` If the order is later refunded: ``` /remove_vip steam:110000xxxxxxxxx /remove_cash steam:110000xxxxxxxxx 10000 ``` # Troubleshooting Source: https://docs.rmz.gg/fivem/troubleshooting Common errors, debug mode, and connection status indicators for the RMZ FiveM integration. ## Common Errors | Console Message | Cause | Fix | | ----------------------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `ERROR: Secret key not configured!` | The secret key in `config.lua` is empty or still set to the placeholder value | Set your actual secret key from the RMZ dashboard in `config.lua` | | `API Error: 401` | The secret key is invalid, or the server has been disabled in the dashboard | Verify the key matches what is shown in **Benefits** > **FiveM Servers** in your RMZ dashboard | | `API Error: 403` | The server's IP address does not match the IP whitelist configured in the dashboard | Check the IP restriction setting in your RMZ dashboard, or remove the IP restriction to allow any IP | | `API Error: 429` | Rate limit exceeded (30 requests per minute) | Increase `Config.PollInterval` in `config.lua` to a higher value (e.g. 15 or 20 seconds) | | `Error executing command: ...` | The command is invalid, or the target resource/script is not running | Test the command manually in your server console to confirm it works | ## Console Commands The resource registers two server console commands for testing: | Command | Description | | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `rmz_test` | Tests the connection to the RMZ API and reports the result. Shows the API URL, a preview of the secret key, and the number of pending commands. | | `rmz_echo ` | Prints a message to the console prefixed with `[RMZ-ECHO]`. Use this as a target command in the RMZ dashboard to verify that command execution is working end-to-end. | ``` rmz_test rmz_echo Hello from RMZ! ``` ## Enable Debug Mode Debug mode prints detailed information about every API poll and command execution to the server console. Enable it by setting `Config.Debug = true` in `config.lua`: ```lua theme={null} Config.Debug = true ``` Then restart the resource: ``` restart rmz-fivem ``` Check the server console for lines prefixed with `[RMZ]`. These will show: * Each poll request and response * Commands being executed and their results * Confirmation callbacks to the RMZ API Disable debug mode in production (`Config.Debug = false`) to keep your console clean. Only enable it when actively troubleshooting an issue. ## Connection Status On the **FiveM Servers** page in your RMZ dashboard, each server shows a connection indicator: | Status | Indicator | Meaning | | ---------------- | --------- | --------------------------------------------------------------------------------------------------------- | | **Connected** | Green | The resource is polling successfully. A poll was received within the last 2 minutes. | | **Disconnected** | Grey | No poll has been received in the last 2 minutes. The resource may not be running or cannot reach the API. | ## Resource Not Starting If the resource does not start or you see no `[RMZ]` output in the console: 1. Confirm the resource folder is named `rmz-fivem` and is inside your `resources/` directory 2. Confirm `ensure rmz-fivem` is in your `server.cfg` 3. Check that `config.lua` has a valid secret key set 4. Verify your server can reach `https://fivem.rmz.gg` (check firewall rules) ## Commands Not Executing If orders are completing but commands are not running on the server: Set `Config.Debug = true` and restart the resource. Look for `[RMZ]` messages. Are polls succeeding? Are commands being received? In the RMZ dashboard, confirm the FiveM benefit is attached to the product. If "Execute only when player is online" is enabled, commands wait until the player joins. Test with this setting disabled first. Copy the exact command from the debug output and run it in your server console. If it fails there, the issue is with the command itself, not the RMZ integration. ## Security Tips * Regenerate your secret key immediately if it is ever exposed publicly * Use IP whitelisting in the dashboard to restrict which server IPs can poll * Do not commit `config.lua` with your real secret key to any public repository # Authentication Source: https://docs.rmz.gg/getting-started/authentication Understand how authentication works across all RMZ APIs. # Authentication RMZ uses different authentication methods depending on the API you are calling. This page covers all patterns. ## Merchant API — Bearer Token The Merchant API uses **Laravel Sanctum** tokens. You generate a token from your dashboard and include it in every request. ``` Authorization: Bearer YOUR_API_TOKEN ``` ### Getting a Token 1. Go to [Settings > API Keys](https://app.rmz.gg/settings/api) in your dashboard 2. Click **Generate Token** 3. Copy and store the token securely Tokens have full read/write access to your store. Never expose them in client-side code, public repositories, or logs. ### Example Request ```bash theme={null} curl -X GET "https://merchant-api.rmz.gg/shawarma/store" \ -H "Authorization: Bearer 1|AbCdEfGhIjKlMnOpQrStUvWxYz" \ -H "Accept: application/json" ``` ### Invalid Token Response ```json theme={null} { "message": "Unauthenticated." } ``` *** ## Storefront API — OTP Authentication The Storefront API authenticates **customers** (not merchants) using a phone/email OTP flow. This is a three-step process. ### Step 1: Start Authentication Send the customer's phone number to begin the OTP flow: ```bash theme={null} curl -X POST "https://front.rmz.gg/api/auth/start" \ -H "Content-Type: application/json" \ -d '{ "country_code": "966", "phone": "501234567" }' ``` Response: ```json theme={null} { "success": true, "message": "Verification code sent successfully", "data": { "session_token": "auth_abc123xyz" } } ``` ### Step 2: Verify OTP Submit the OTP code the customer received: ```bash theme={null} curl -X POST "https://front.rmz.gg/api/auth/verify" \ -H "Content-Type: application/json" \ -d '{ "session_token": "auth_abc123xyz", "code": "1234" }' ``` For **existing customers**, you receive a Bearer token immediately: ```json theme={null} { "success": true, "data": { "type": "authenticated", "token": "1|abc123xyz...", "cart_token": "cart_xyz789", "customer": { "id": 123, "first_name": "Ahmed", "last_name": "Ali" } } } ``` For **new customers**, registration is required: ```json theme={null} { "success": true, "data": { "type": "new", "requires_registration": true, "session_token": "auth_abc123xyz" } } ``` ### Step 3: Complete Registration (New Customers) ```bash theme={null} curl -X POST "https://front.rmz.gg/api/auth/complete" \ -H "Content-Type: application/json" \ -d '{ "session_token": "auth_abc123xyz", "email": "ahmed@example.com", "firstName": "Ahmed", "lastName": "Ali" }' ``` ### Using the Token Once authenticated, include the token in subsequent requests: ```bash theme={null} curl -H "Authorization: Bearer 1|abc123xyz..." \ https://front.rmz.gg/api/customer/profile ``` ### Guest Cart Unauthenticated users can still manage a cart using the `X-Cart-Token` header. See [Guest Cart](/storefront-api/authentication/guest-cart) for details. *** ## Embed API — Embed Key The Embed API uses an `X-Embed-Key` header for authentication. The embed key is tied to a specific product and store. ``` X-Embed-Key: YOUR_EMBED_KEY ``` *** ## License API — No Auth Header The License Verification API does not use authentication headers. Instead, the `product_id` in the request body identifies the product, and the `license_key` is the credential being verified. ```bash theme={null} curl -X POST "https://license.rmz.gg/verify" \ -H "Content-Type: application/json" \ -d '{ "product_id": 123, "license_key": "MYAPP-XXXX-XXXX-XXXX-XXXX", "hwid": "machine-hardware-id" }' ``` *** ## Supported Country Codes OTP authentication in the Storefront API supports these country codes: | Code | Country | | ---- | ------------ | | 966 | Saudi Arabia | | 973 | Bahrain | | 971 | UAE | | 974 | Qatar | | 968 | Oman | | 965 | Kuwait | # Environments Source: https://docs.rmz.gg/getting-started/environments Base URLs and environments for all RMZ APIs. # Environments RMZ APIs are available in production. All base URLs use the `rmz.gg` domain. ## Base URLs | API | Base URL | | ------------------------ | -------------------------------------- | | **Merchant API** | `https://merchant-api.rmz.gg/shawarma` | | **Storefront API** | `https://front.rmz.gg/api` | | **License Verification** | `https://license.rmz.gg` | | **FiveM API** | `https://fivem.rmz.gg/api/fivem` | | **Embed API** | `https://embed.rmz.gg/api/embed` | ## Merchant API ``` https://merchant-api.rmz.gg/shawarma ``` All Merchant API endpoints are prefixed under `/shawarma`. For example: * `GET https://merchant-api.rmz.gg/shawarma/store` * `GET https://merchant-api.rmz.gg/shawarma/orders` * `POST https://merchant-api.rmz.gg/shawarma/orders` ## Storefront API ``` https://front.rmz.gg/api ``` The Storefront API identifies the store by the request's **origin domain**. Your custom storefront must send requests from the store's configured domain. For example: * `GET https://front.rmz.gg/api/products` * `POST https://front.rmz.gg/api/cart/add` * `POST https://front.rmz.gg/api/checkout` ## License Verification API ``` https://license.rmz.gg ``` A single endpoint for verifying license keys: * `POST https://license.rmz.gg/verify` ## FiveM API ``` https://fivem.rmz.gg/api/fivem ``` Used by the FiveM resource to poll for pending commands: * `GET https://fivem.rmz.gg/api/fivem/queue` * `DELETE https://fivem.rmz.gg/api/fivem/queue` * `GET https://fivem.rmz.gg/api/fivem/queue/online/{playerId}` The FiveM resource handles communication with this API automatically. You do not need to call these endpoints directly. ## Embed API ``` https://embed.rmz.gg/api/embed ``` The Embed API allows external websites to embed a checkout widget. For example: * `GET https://embed.rmz.gg/api/embed/product/{productId}` * `POST https://embed.rmz.gg/api/embed/checkout` ## Required Headers All APIs expect JSON: ``` Content-Type: application/json Accept: application/json ``` The Merchant API additionally requires: ``` Authorization: Bearer YOUR_API_TOKEN ``` The Storefront API uses cart tokens for cart operations: ``` X-Cart-Token: YOUR_CART_TOKEN ``` # Error Handling Source: https://docs.rmz.gg/getting-started/error-handling Understand error formats and HTTP status codes across RMZ APIs. # Error Handling All RMZ APIs use standard HTTP status codes and return structured JSON error responses. ## Merchant API Error Format Error responses from the Merchant API include an `error: true` field: ```json theme={null} { "message": "Product not found: 999", "data": null, "api": "rmz.shawarma", "timestamp": 1699999999, "error": true } ``` ## Storefront API Error Format The Storefront API uses `success: false`: ```json theme={null} { "success": false, "message": "Error message", "data": null } ``` ## License API Error Format The License API returns an error code string: ```json theme={null} { "success": false, "error": "LICENSE_NOT_FOUND", "message": "License not found" } ``` ## HTTP Status Codes | Code | Meaning | When It Happens | | ----- | --------------------- | ------------------------------------- | | `200` | Success | Request completed successfully | | `201` | Created | Resource created (e.g., new order) | | `400` | Bad Request | Validation error or malformed request | | `401` | Unauthorized | Missing or invalid authentication | | `403` | Forbidden | Authenticated but not authorized | | `404` | Not Found | Resource does not exist | | `422` | Unprocessable Entity | Validation failed | | `429` | Too Many Requests | Rate limit exceeded | | `500` | Internal Server Error | Unexpected server error | ## Validation Errors When request validation fails (400 or 422), the response includes field-level errors: ```json theme={null} { "message": "The given data was invalid.", "errors": { "products": ["The products field is required."], "products.0.identifier": ["The products.0.identifier field is required."] } } ``` ## Best Practices Always check the HTTP status code first, then parse the response body for details. 1. **Check status codes** — do not assume every response is successful 2. **Parse error messages** — they contain actionable information 3. **Handle validation errors** — iterate the `errors` object to display field-specific messages 4. **Log errors** — include the timestamp and request details for debugging 5. **Handle network errors** — timeouts and connection failures are not JSON responses # Introduction Source: https://docs.rmz.gg/getting-started/introduction Learn about the RMZ platform and its developer ecosystem. # Introduction to RMZ RMZ is a multi-tenant e-commerce platform built for **digital products** in the MENA region. Merchants create stores to sell digital codes, software licenses, subscriptions, online courses, and services — all with built-in payment processing, customer management, and hosted storefronts. ## What You Can Build ### Custom Storefronts Use the **Storefront API** or **Storefront SDK** to build a fully custom shopping experience. Browse products, manage carts, process checkout, and handle customer authentication — all headlessly. ### Store Automation Use the **Merchant API** to programmatically manage your store. List orders, create orders on behalf of customers, update statuses, and pull statistics into your own dashboards or CRMs. ### Embedded Checkout Use the **Embed API** to drop a quick-purchase widget onto any external website. Customers can buy a product without leaving your site. ### Software Licensing Use the **License Verification API** to protect your software. Customers purchase license keys from your store, and your software verifies them with a single API call. Supports hardware ID locking, IP locking, activation limits, and end-to-end encryption. ### FiveM Scripts Use the **FiveM integration** to sell and protect FiveM server scripts with automatic license key generation and verification. ## Platform Highlights * **Digital-first** — purpose-built for digital products, not physical goods * **MENA-focused** — supports Saudi Arabia, UAE, Bahrain, Qatar, Oman, and Kuwait * **Multi-payment** — integrated payment gateways for the region * **Arabic-native** — full Arabic language support and RTL layouts * **Multi-tenant** — each merchant gets an isolated store with its own subdomain or custom domain ## APIs at a Glance | API | Base URL | Auth | Purpose | | -------------- | -------------------------------------- | -------------------------- | ----------------------------- | | Merchant API | `https://merchant-api.rmz.gg/shawarma` | Bearer token | Store management | | Storefront API | `https://front.rmz.gg/api` | OTP + Bearer token | Customer-facing shopping | | Embed API | `https://embed.rmz.gg/api/embed` | X-Embed-Key header | Embedded checkout widget | | License API | `https://license.rmz.gg` | None (product\_id in body) | Software license verification | | FiveM API | `https://fivem.rmz.gg` | None (product\_id in body) | FiveM license verification | ## Next Steps Make your first API call in 5 minutes. Understand how auth works across all APIs. # Quickstart Source: https://docs.rmz.gg/getting-started/quickstart Make your first Merchant API call in under 5 minutes. # Quickstart This guide walks you through making your first Merchant API call. By the end, you will have retrieved your store statistics using a Bearer token. ## Prerequisites * An RMZ store on the **RMZ+ plan** * An API token (generated from your dashboard) ## Step 1: Get Your API Token 1. Log in to your [RMZ Dashboard](https://app.rmz.gg) 2. Go to **Settings > API Keys** 3. Click **Generate Token** and copy the value Your API token grants full access to your store data. Keep it secret and never expose it in client-side code. ## Step 2: Make Your First Request Use the token to fetch your store information: ```bash cURL theme={null} curl -X GET "https://merchant-api.rmz.gg/shawarma/store" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Accept: application/json" ``` ```javascript JavaScript theme={null} const response = await fetch("https://merchant-api.rmz.gg/shawarma/store", { headers: { "Authorization": "Bearer YOUR_API_TOKEN", "Accept": "application/json" } }); const data = await response.json(); console.log(data); ``` ```python Python theme={null} import requests response = requests.get( "https://merchant-api.rmz.gg/shawarma/store", headers={ "Authorization": "Bearer YOUR_API_TOKEN", "Accept": "application/json" } ) print(response.json()) ``` ```php PHP theme={null} $ch = curl_init("https://merchant-api.rmz.gg/shawarma/store"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer YOUR_API_TOKEN", "Accept: application/json" ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); print_r($response); ``` ## Step 3: Check the Response A successful response looks like this: ```json theme={null} { "message": null, "data": { "id": 1, "name": "My Store", "slug": "my-store", "logo": "https://...", "description": "..." }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` ## Step 4: Get Store Statistics Now fetch your store stats: ```bash cURL theme={null} curl -X GET "https://merchant-api.rmz.gg/shawarma/store/statics" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ```javascript JavaScript theme={null} const stats = await fetch("https://merchant-api.rmz.gg/shawarma/store/statics", { headers: { "Authorization": "Bearer YOUR_API_TOKEN" } }).then(r => r.json()); console.log(`Total sales: ${stats.data.sales_total}`); console.log(`Orders: ${stats.data.sales_count}`); console.log(`Customers: ${stats.data.customers_count}`); ``` ```json Response theme={null} { "message": null, "data": { "sales_total": 125750.50, "sales_count": 342, "customers_count": 156, "products_count": 23, "categories_count": 5, "subscribers_count": 45, "pages_count": 3, "coupons_count": 8, "duration": "lifetime" }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` ## What's Next Fetch and filter your store orders. Create orders programmatically via the API. Learn about all auth patterns across RMZ APIs. Handle errors and edge cases gracefully. # Rate Limiting Source: https://docs.rmz.gg/getting-started/rate-limiting Understand rate limits across all RMZ APIs. # Rate Limiting All RMZ APIs enforce rate limits to ensure fair usage and platform stability. When you exceed a limit, you receive a `429 Too Many Requests` response. ## Limits by API ### Merchant API | Scope | Limit | | ------------- | -------------------------------- | | All endpoints | 60 requests per minute per token | ### Storefront API | Scope | Limit | | ----------------------------------------- | ------------------------------------ | | General API calls | 60 requests per minute | | Authentication start (`POST /auth/start`) | 50 sessions per day per IP | | Phone authentication | 10 attempts per day per phone number | | OTP verification (`POST /auth/verify`) | 5 attempts per minute per IP | | OTP resend (`POST /auth/resend`) | 3 resends per 10 minutes | ### License Verification API | Scope | Limit | | ------------- | ----------------------------- | | All endpoints | 60 requests per minute per IP | ## Handling Rate Limits When rate-limited, the API returns: ```json theme={null} { "message": "Too Many Requests" } ``` **HTTP Status:** `429` ### Best Practices 1. **Implement exponential backoff** — wait 1s, then 2s, then 4s before retrying 2. **Cache responses** — avoid re-fetching data that has not changed 3. **Batch where possible** — use pagination instead of fetching one item at a time 4. **Monitor your usage** — track 429 responses in your logs ### Retry Example ```javascript JavaScript theme={null} async function fetchWithRetry(url, options, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { const response = await fetch(url, options); if (response.status !== 429) return response; const waitTime = Math.pow(2, i) * 1000; await new Promise(resolve => setTimeout(resolve, waitTime)); } throw new Error("Rate limit exceeded after retries"); } ``` ```python Python theme={null} import time import requests def fetch_with_retry(url, headers, max_retries=3): for i in range(max_retries): response = requests.get(url, headers=headers) if response.status_code != 429: return response time.sleep(2 ** i) raise Exception("Rate limit exceeded after retries") ``` # Building a Custom Storefront Source: https://docs.rmz.gg/guides/building-a-custom-storefront End-to-end guide to building a headless storefront using the RMZ Storefront SDK and Next.js. Build a fully custom storefront for your RMZ store using the Storefront SDK and Next.js. Your store handles products, payments, orders, and customer management — you control the entire frontend experience. ## Prerequisites * An RMZ store with products and at least one payment method configured * API keys from **Dashboard > Settings > API Keys** (public key, and secret key for server-side operations) * Node.js 18+ installed * Your custom domain configured in the store dashboard ## Quick Start with the Template The fastest way to get started is to clone the official Next.js template: ```bash theme={null} git clone https://github.com/Dokan-E-Commerce/rmz-storefront-nextjs my-storefront cd my-storefront ``` ```bash theme={null} npm install ``` The template already includes the Storefront SDK as a dependency. Create a `.env.local` file in the project root: ```bash theme={null} NEXT_PUBLIC_RMZ_PUBLIC_KEY=pk_your_public_key_here RMZ_SECRET_KEY=sk_your_secret_key_here NEXT_PUBLIC_API_URL=https://front.rmz.gg/api ``` Never expose your `RMZ_SECRET_KEY` in client-side code. It should only be used in server-side API routes and server components. ```bash theme={null} npm run dev ``` Open `http://localhost:3000` to see your storefront. ## Starting from Scratch If you prefer to build from an existing Next.js project: ### 1. Install the SDK ```bash theme={null} npm install rmz-storefront-sdk ``` ### 2. Initialize the SDK Create a shared SDK instance for your application: ```typescript theme={null} // lib/rmz.ts import { createStorefrontSDK } from 'rmz-storefront-sdk'; // Client-side SDK (safe for browser) export const sdk = createStorefrontSDK({ publicKey: process.env.NEXT_PUBLIC_RMZ_PUBLIC_KEY!, environment: 'production' }); // Server-side SDK (with HMAC authentication) export const serverSDK = createStorefrontSDK({ publicKey: process.env.NEXT_PUBLIC_RMZ_PUBLIC_KEY!, secretKey: process.env.RMZ_SECRET_KEY!, environment: 'production' }); ``` The SDK defaults to `https://front.rmz.gg/api` as the API URL. You only need to set `apiUrl` if you are using a custom domain. ### 3. Fetch and Display Products ```tsx theme={null} // app/page.tsx import { serverSDK } from '@/lib/rmz'; export default async function Home() { const { data: products } = await serverSDK.products.getAll({ per_page: 12 }); return (
{products.map((product) => (
{product.name}

{product.name}

{product.actual_price} {product.currency}

View Details
))}
); } ``` ### 4. Build the Cart ```tsx theme={null} // components/AddToCart.tsx 'use client'; import { sdk } from '@/lib/rmz'; import { useState } from 'react'; export function AddToCart({ productId }: { productId: number }) { const [loading, setLoading] = useState(false); async function handleAddToCart() { setLoading(true); try { const cart = await sdk.cart.addItem(productId, 1); // Store cart token for subsequent requests localStorage.setItem('cart_token', cart.cart_token); alert('Added to cart!'); } catch (error) { console.error('Failed to add to cart:', error); } finally { setLoading(false); } } return ( ); } ``` ### 5. Implement Checkout ```tsx theme={null} // app/checkout/page.tsx 'use client'; import { sdk } from '@/lib/rmz'; import { useState } from 'react'; export default function Checkout() { const [loading, setLoading] = useState(false); async function handleCheckout(paymentMethod: string) { setLoading(true); try { const checkout = await sdk.checkout.create({ payment_method: paymentMethod, }); if (checkout.redirect_url) { window.location.href = checkout.redirect_url; } } catch (error) { console.error('Checkout failed:', error); } finally { setLoading(false); } } return (

Checkout

); } ``` ### 6. Customer Authentication ```tsx theme={null} // app/auth/page.tsx 'use client'; import { sdk } from '@/lib/rmz'; import { useState } from 'react'; export default function Auth() { const [step, setStep] = useState<'phone' | 'otp' | 'done'>('phone'); const [phone, setPhone] = useState(''); const [otp, setOtp] = useState(''); async function startAuth() { await sdk.auth.startOTP({ country_code: '966', phone: phone, }); setStep('otp'); } async function verifyOTP() { const result = await sdk.auth.verifyOTP({ code: otp }); // Store the Bearer token localStorage.setItem('auth_token', result.token); setStep('done'); } if (step === 'phone') { return (
setPhone(e.target.value)} placeholder="Phone number" />
); } if (step === 'otp') { return (
setOtp(e.target.value)} placeholder="Enter code" />
); } return

Authenticated!

; } ``` ## Deploying ### Vercel (Recommended) ```bash theme={null} npm install -g vercel vercel ``` Set your environment variables in the Vercel dashboard under **Settings > Environment Variables**. ### Other Platforms The storefront is a standard Next.js app and can be deployed to any platform that supports Node.js: * **Netlify**: Add a `netlify.toml` with the Next.js plugin * **AWS Amplify**: Connect your Git repository * **Docker**: Use the official Next.js Docker example * **Self-hosted**: Run `npm run build && npm start` ## Domain Configuration After deploying, configure your custom domain: 1. Go to **Dashboard > Settings > Domains** 2. Add your storefront domain (e.g., `store.yourdomain.com`) 3. Update your DNS records as instructed 4. The Storefront API will accept requests from this domain automatically For the best SEO and performance, use server-side rendering (SSR) or static site generation (SSG) for product and category pages. Use client-side rendering only for interactive elements like cart and checkout. ## Next Steps Full SDK API reference with all available methods. Direct API reference if you prefer raw HTTP requests. # Embedding Checkout Source: https://docs.rmz.gg/guides/embedding-checkout Add an RMZ product checkout widget to any external website. The Embed feature lets you sell a product directly from any website — your blog, landing page, or marketing site. RMZ provides a ready-made widget script that handles the entire purchase flow. ## Quick Start (Widget) The fastest way to embed checkout is using the RMZ widget script. This is the same code generated by your dashboard under **Settings > Embed**. ### 1. Add the script and button ```html theme={null} ``` That's it. When a customer clicks the button, a checkout modal opens with the full purchase flow — product info, authentication, payment. ### 2. Button attributes | Attribute | Required | Description | | ------------------ | -------- | -------------------------------------------------------- | | `data-rmz-product` | Yes | Your product ID from the dashboard | | `data-rmz-key` | Yes | Your embed public key (starts with `rmz_pk_`) | | `data-rmz-theme` | No | Theme mode: `auto`, `light`, or `dark` (default: `auto`) | You can style the button with any CSS — the embed script only uses the data attributes. The button text is also fully customizable. ### 3. Multiple products on one page Add multiple buttons with different product IDs — they all share the same script: ```html theme={null} ``` *** ## Setup in Dashboard Go to **Dashboard > Settings > Embed** and toggle the feature on. Choose the product you want to embed from the dropdown. Under **Security Settings**, copy the **Public Key** (format: `rmz_pk_1_...`). You can regenerate it at any time, but you'll need to update the code on all sites using it. Under **Allowed Domains**, add the domains where the widget will be used (e.g., `https://example.com` or `*.example.com`). Leave empty to allow all origins. Copy the generated HTML snippet and paste it into your website. If you regenerate the public key, you must update the `data-rmz-key` attribute on all sites using the embed code. *** ## JavaScript Events The widget dispatches custom DOM events you can listen for: ```javascript theme={null} // Modal opened document.addEventListener('rmz:modal:open', (e) => { console.log('Product:', e.detail.productId); }); // Modal closed document.addEventListener('rmz:modal:close', () => { console.log('Modal closed'); }); // Checkout completed document.addEventListener('rmz:checkout:complete', (e) => { console.log('Order ID:', e.detail.orderId); }); // Checkout error document.addEventListener('rmz:checkout:error', (e) => { console.log('Error:', e.detail.message); }); ``` *** ## Advanced: Direct API Integration If you need full control over the UI, you can call the [Embed API](/embed-api/overview) directly instead of using the widget script. This lets you build a completely custom checkout experience. ### Base URL ``` https://embed.rmz.gg/api/embed ``` ### Flow 1. Fetch product info: `GET /api/embed/product/{productId}` 2. Authenticate customer via OTP: `POST /api/embed/auth/start` → `POST /api/embed/auth/verify` 3. Create checkout: `POST /api/embed/checkout` (or `POST /api/embed/guest/checkout`) 4. Initiate payment: `POST /api/embed/payment/initiate` 5. Poll for completion: `GET /api/embed/payment/status/{checkoutUrl}` ### Example: Fetch product and create checkout ```javascript theme={null} const EMBED_KEY = "rmz_pk_1_DIO7CqCNSyYtZSTVqqjU9VkT"; const PRODUCT_ID = 61200; const API = "https://embed.rmz.gg/api/embed"; // 1. Get product info const product = await fetch(`${API}/product/${PRODUCT_ID}`, { headers: { "X-Embed-Key": EMBED_KEY } }).then(r => r.json()); // 2. Start OTP auth const auth = await fetch(`${API}/auth/start`, { method: "POST", headers: { "X-Embed-Key": EMBED_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ product_id: PRODUCT_ID, country_code: "966", phone: "501234567" }) }).then(r => r.json()); // 3. Verify OTP (user enters code) const verify = await fetch(`${API}/auth/verify`, { method: "POST", headers: { "X-Embed-Key": EMBED_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ session_token: auth.data.session_token, code: "1234" }) }).then(r => r.json()); const token = verify.data.token; // 4. Create checkout const checkout = await fetch(`${API}/checkout`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, "X-Embed-Key": EMBED_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ product_id: PRODUCT_ID, quantity: 1 }) }).then(r => r.json()); // 5. Initiate payment if (checkout.data.type !== "free_order") { const payment = await fetch(`${API}/payment/initiate`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, "X-Embed-Key": EMBED_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ checkout_url: checkout.data.checkout_url, payment_method: checkout.data.payment_methods[0].id }) }).then(r => r.json()); if (payment.data.type === "redirect") { window.open(payment.data.redirect_url, "_blank"); } } ``` See the full [Embed API reference](/embed-api/overview) for all endpoints and parameters. # License Integration Source: https://docs.rmz.gg/guides/license-integration Add license key verification to your software using the RMZ licensing system. RMZ provides a complete software licensing system. Customers buy a license key from your store, and your software verifies it with a single API call. This guide covers setup, implementation, and best practices. ## How It Works In your store dashboard, create a product with type **Software License**. Configure the lock type, max activations, and pricing plans. When a customer buys the product, a license key is automatically generated and delivered (e.g., `MYAPP-XXXX-XXXX-XXXX-XXXX`). Your application calls the RMZ license verification API with the key. The API validates the key and auto-activates the device. You receive the license status, plan details, activation count, and expiry information. ## Reference Repository Full working examples in Python, PHP, JavaScript, and Lua (FiveM) are available at: ``` https://github.com/Rmz-App/rmz-license-examples ``` *** ## Step 1: Create a License Product In your store dashboard, go to **Products > New Product** and configure: | Setting | Description | | ------------------- | -------------------------------------------------------------------------------------- | | **Type** | Software License | | **Key prefix** | A short prefix for generated keys (e.g., `MYAPP` produces `MYAPP-XXXX-XXXX-XXXX-XXXX`) | | **Lock type** | How keys are bound to devices (see below) | | **Max activations** | How many devices can use a single key (0 = unlimited) | | **Plans** | Duration-based pricing tiers (monthly, yearly, lifetime, etc.) | | **E2EE** | Optional end-to-end encryption for API responses | ### Lock Types Key-only verification. No device binding. Any device can use the key. Best for simple products. Locks to a hardware ID (machine fingerprint). Each unique HWID uses an activation slot. Best for desktop software. Locks to the caller's IP address. Each unique IP uses a slot. Best for server-side applications. *** ## Step 2: Implement Verification The verification endpoint validates the key and auto-activates the device in a single call. ``` POST https://license.rmz.gg/verify ``` ### Request | Field | Type | Required | Description | | ------------- | ------- | ----------- | ---------------------------------- | | `product_id` | integer | Yes | Your product ID from the dashboard | | `license_key` | string | Yes | The customer's license key | | `hwid` | string | Conditional | Required when lock type is `hwid` | ### Implementation Examples ```javascript Node.js theme={null} async function verifyLicense(licenseKey, hwid) { const response = await fetch("https://license.rmz.gg/verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ product_id: YOUR_PRODUCT_ID, license_key: licenseKey, hwid: hwid // Optional for 'none' and 'ip' lock types }) }); const result = await response.json(); if (result.success) { console.log("License valid!"); console.log("Product:", result.data.product.name); console.log("Status:", result.data.status); console.log("Expires:", result.data.expires_at || "Never (lifetime)"); console.log("Activations:", `${result.data.activations.current}/${result.data.activations.max || "unlimited"}`); return result.data; } else { console.error("License invalid:", result.error, result.message); return null; } } ``` ```python Python theme={null} import requests def verify_license(license_key, hwid=None): payload = { "product_id": YOUR_PRODUCT_ID, "license_key": license_key, } if hwid: payload["hwid"] = hwid response = requests.post( "https://license.rmz.gg/verify", json=payload ) result = response.json() if result.get("success"): data = result["data"] print(f"License valid! Product: {data['product']['name']}") print(f"Status: {data['status']}") print(f"Expires: {data['expires_at'] or 'Never (lifetime)'}") return data else: print(f"License invalid: {result['error']} - {result['message']}") return None ``` ```php PHP theme={null} YOUR_PRODUCT_ID, 'license_key' => $licenseKey, ]; if ($hwid) { $payload['hwid'] = $hwid; } $response = json_decode(file_get_contents( 'https://license.rmz.gg/verify', false, stream_context_create([ 'http' => [ 'method' => 'POST', 'header' => 'Content-Type: application/json', 'content' => json_encode($payload), ] ]) ), true); if ($response['success']) { echo "License valid! " . $response['data']['product']['name'] . "\n"; return $response['data']; } echo "Invalid: " . $response['error'] . "\n"; return null; } ``` ```lua FiveM (Lua) theme={null} PerformHttpRequest("https://license.rmz.gg/verify", function(code, text) local result = json.decode(text) if code == 200 and result.success then print("License valid: " .. result.data.product.name) print("Status: " .. result.data.status) -- Allow resource to start else print("License invalid: " .. (result.error or "unknown")) -- Stop resource StopResource(GetCurrentResourceName()) end end, "POST", json.encode({ product_id = YOUR_PRODUCT_ID, license_key = GetConvar("license_key", ""), hwid = GetConvar("sv_licenseKeyToken", "") }), { ["Content-Type"] = "application/json" }) ``` ### Success Response ```json theme={null} { "success": true, "data": { "status": "active", "lock_type": "hwid", "product": { "id": 123, "name": "My Script Pro" }, "plan": { "duration": "annually", "duration_label": "سنه", "start_date": "23-03-2026", "end_date": "23-03-2027", "is_active": true }, "expires_at": "2027-03-23T12:00:00.000000Z", "expires_in_days": 365, "activations": { "current": 1, "max": 3, "remaining": 2 }, "metadata": null } } ``` ### Error Codes | Code | HTTP Status | Description | | ------------------- | ----------- | ---------------------------------------------- | | `LICENSE_NOT_FOUND` | 404 | Key does not exist for this product | | `LICENSE_EXPIRED` | 403 | License has passed its expiry date | | `LICENSE_REVOKED` | 403 | Permanently revoked by the store owner | | `LICENSE_SUSPENDED` | 403 | Temporarily suspended | | `HWID_REQUIRED` | 403 | Lock type is `hwid` but no `hwid` was provided | | `ACTIVATION_LIMIT` | 429 | All device activation slots are used | *** ## Step 3: Generate Hardware IDs For `hwid` lock type, generate a consistent machine fingerprint by combining hardware-specific values: ```javascript theme={null} // Node.js example const crypto = require("crypto"); const os = require("os"); function generateHWID() { const components = [ os.hostname(), os.cpus()[0]?.model, os.totalmem().toString(), os.platform(), os.arch() ]; return crypto .createHash("sha256") .update(components.join("|")) .digest("hex"); } ``` Choose hardware components that are stable across reboots but unique per machine. Avoid components that change frequently (like available memory or uptime). *** ## Step 4: Handle E2EE (Optional) When E2EE is enabled on your license product, all API responses are encrypted with AES-256-GCM. This prevents response tampering and man-in-the-middle spoofing. ### Encrypted Response Format ```json theme={null} { "encrypted": true, "payload": "base64-encoded-ciphertext", "nonce": "base64-encoded-12-byte-nonce", "tag": "base64-encoded-16-byte-auth-tag" } ``` ### Decryption ```javascript Node.js theme={null} const crypto = require("crypto"); function decryptResponse(encrypted, encryptionKey) { const key = Buffer.from(encryptionKey, "hex"); // 64 hex chars = 32 bytes const nonce = Buffer.from(encrypted.nonce, "base64"); const tag = Buffer.from(encrypted.tag, "base64"); const ciphertext = Buffer.from(encrypted.payload, "base64"); const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce); decipher.setAuthTag(tag); const decrypted = Buffer.concat([ decipher.update(ciphertext), decipher.final() ]); return JSON.parse(decrypted.toString("utf8")); } ``` ```python Python theme={null} from cryptography.hazmat.primitives.ciphers.aead import AESGCM import base64, json def decrypt_response(encrypted, encryption_key): key = bytes.fromhex(encryption_key) # 64 hex chars = 32 bytes nonce = base64.b64decode(encrypted["nonce"]) tag = base64.b64decode(encrypted["tag"]) ciphertext = base64.b64decode(encrypted["payload"]) aesgcm = AESGCM(key) plaintext = aesgcm.decrypt(nonce, ciphertext + tag, None) return json.loads(plaintext.decode("utf-8")) ``` ```php PHP theme={null} function decryptResponse(array $encrypted, string $encryptionKey): array { $key = hex2bin($encryptionKey); // 64 hex chars = 32 bytes $nonce = base64_decode($encrypted['nonce']); $tag = base64_decode($encrypted['tag']); $ciphertext = base64_decode($encrypted['payload']); $plaintext = openssl_decrypt( $ciphertext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $nonce, $tag ); return json_decode($plaintext, true); } ``` Never hardcode the encryption key as a plain string in your source code. Derive it at runtime from multiple values or use an obfuscation layer. *** ## Best Practices ### Verification Frequency * Verify the license **once on application startup** * Optionally re-verify periodically (e.g., every 24 hours) for subscription products * Cache the verification result locally to handle temporary network outages * **Rate limit:** The API allows 60 requests per minute per IP ### Security Recommendations 1. **Use HWID lock type** for desktop applications to prevent key sharing 2. **Enable E2EE** to prevent response tampering (someone injecting `"success": true` via a local proxy) 3. **Obfuscate your code** to make it harder to find and bypass the verification logic 4. **Embed verification deep** in your application flow, not in a single easily-patchable function 5. **Combine with integrity checks** to detect binary modification 6. **Do not store the encryption key** as a visible string literal ### Graceful Degradation Handle network failures gracefully: ```javascript theme={null} async function verifyWithFallback(licenseKey, hwid) { try { const result = await verifyLicense(licenseKey, hwid); if (result) { // Cache the result locally saveToCache(licenseKey, result); } return result; } catch (networkError) { // Network failure — check local cache const cached = loadFromCache(licenseKey); if (cached && cached.status === "active") { console.log("Using cached license validation"); return cached; } // No cache available — block or allow with warning return null; } } ``` *** ## Managing Licenses in the Dashboard From your store dashboard under **License Keys**, you can: * View all issued licenses with search and filters * Create manual licenses (for testing, promotional use) * Revoke, suspend, or reactivate individual licenses * Reset device activations (frees all slots) * View activation history and verification logs ### License Lifecycle | Event | Effect | | ------------------ | --------------------------------------- | | Customer purchases | Key auto-generated and delivered | | Order completed | License activated, expiry set from plan | | Order cancelled | All order licenses revoked | | Order refunded | All order licenses revoked | | Expiry date passes | Marked expired (checked hourly) | For complete API reference details, see the [Licensing documentation](/licensing/overview). # Subscription Integration Source: https://docs.rmz.gg/guides/subscriptions End-to-end guide to integrating RMZ subscriptions into your application, including storefront purchases, SaaS billing, webhooks, and the customer portal. This guide covers everything you need to integrate RMZ's subscription and billing system into your application. Whether you are building a storefront with recurring products or a SaaS platform that needs subscription management, this guide will walk you through the full flow. ## Prerequisites * An RMZ store with an **RMZ+** subscription plan * At least one subscription product created in your store dashboard * API keys from **Dashboard > Settings > API Keys** * A webhook endpoint to receive subscription events *** ## Overview RMZ subscriptions support: * **Multiple billing cycles**: monthly, quarterly, semi-annual, annual, biennial, and more * **Free trials**: optional trial periods before the first charge * **Auto-renewal**: automatic charging via saved payment cards * **Dunning**: retry logic for failed payments (up to 3 attempts over 7 days) * **Plan changes**: upgrades (immediate with proration) and downgrades (at next period) * **Customer portal**: hosted self-service portal for subscription management ### Subscription Lifecycle ``` created → trialing → active → renewed (repeats) │ ↓ │ past_due ──► expired │ ↓ │ active (if payment recovered) │ ↕ │ paused ──► active (on unpause/resume) │ └──► canceled / expired (from trialing) active / trialing / past_due / paused → canceled (immediate) or cancel_at_period_end → expired canceled → active (resume, if the paid period has not yet ended) expired → (terminal, no further transitions) ``` Allowed state transitions (enforced server-side by `SubscriptionStatus::canTransitionTo()`): | From | Allowed Next States | | ---------- | ------------------------------------------- | | `trialing` | `active`, `past_due`, `canceled`, `expired` | | `active` | `past_due`, `paused`, `canceled`, `expired` | | `past_due` | `active`, `canceled`, `expired` | | `paused` | `active`, `canceled`, `expired` | | `canceled` | `active` (resume while period still valid) | | `expired` | — (terminal) | ### Status Reference | Status | Access Granted | Description | | ---------- | :------------: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `trialing` | Yes | Customer is in a free trial period | | `active` | Yes | Subscription is active and paid | | `past_due` | Yes | Payment failed, retries in progress (grace period) | | `paused` | No | Subscription is on hold ("freeze time"). Access is **suspended** while paused and auto-renewal is halted. At pause time, the days remaining in the current period are banked in `metadata.paused_remaining_days`. When the subscription is unpaused back to `active`, those banked days are added to the new period end, so the customer does not lose any paid time. | | `canceled` | No | Subscription was canceled immediately. Can be resumed back to `active` while the originally paid period has not yet ended. | | `expired` | No | Subscription period ended or all retries exhausted. Terminal state. | *** ## Storefront Flow If you are building a custom storefront (headless), customers purchase subscription products through the standard checkout flow. ### 1. Display Subscription Products Fetch products with type `subscription` and display their variants: ```javascript theme={null} const { data: product } = await sdk.products.getBySlug('pro-plan'); // Each variant represents a billing cycle product.subscription_variants.forEach(variant => { console.log(`${variant.durationText}: ${variant.price} SAR`); // e.g., "شهر: 49 SAR", "سنه: 399 SAR" }); ``` ### 2. Add to Cart and Checkout ```javascript theme={null} // Add the subscription variant to the cart await sdk.cart.addItem(product.id, 1, { variant_id: selectedVariant.id }); // Create checkout const checkout = await sdk.checkout.create({ payment_method: 'card' }); // Redirect to payment window.location.href = checkout.redirect_url; ``` ### 3. After Purchase Once payment completes, a subscription is automatically created. Listen for the `subscription.created` webhook to provision access in your system. ### 4. View Customer Subscriptions Authenticated customers can view their subscriptions: ```javascript theme={null} const { data: subscriptions } = await sdk.orders.getSubscriptions(); subscriptions.forEach(sub => { console.log(`Status: ${sub.status}`); console.log(`Renews: ${sub.current_period_end}`); console.log(`Auto-renew: ${sub.auto_renew}`); }); ``` *** ## SaaS Integration Flow If you are building a SaaS product and want to use RMZ for subscription billing, use the Merchant API to create checkout sessions programmatically. ### 1. Create a Checkout Session When a user wants to subscribe in your application, create a checkout session on your server: ```javascript theme={null} // Your server-side code const response = await fetch( "https://merchant-api.rmz.gg/shawarma/subscriptions/checkout-sessions", { method: "POST", headers: { "Authorization": `Bearer ${process.env.RMZ_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ product_id: 102, variant_id: 15, customer: { country_code: user.countryCode, phone: user.phone, firstName: user.firstName, lastName: user.lastName, email: user.email }, metadata: { external_user_id: user.id, plan: "pro" }, success_url: "https://yourapp.com/subscription/success", cancel_url: "https://yourapp.com/pricing" }) } ); const { data } = await response.json(); // Redirect user to data.checkout_url ``` ### 2. Handle the Webhook Set up a webhook to handle subscription events: ```javascript theme={null} app.post("/webhooks/rmz", async (req, res) => { const { event, event_id, data } = req.body; const subscription = data.subscription; const metadata = subscription.metadata; // Your custom data from checkout session switch (event) { case "subscription.created": // Use metadata.external_user_id to link RMZ subscription to your user await provisionAccess(metadata.external_user_id, subscription.id); break; case "subscription.renewed": await extendAccess(metadata.external_user_id, subscription.current_period_end); break; case "subscription.canceled": case "subscription.expired": await revokeAccess(metadata.external_user_id); break; case "subscription.past_due": await showPaymentWarning(metadata.external_user_id); break; } res.status(200).json({ received: true }); }); ``` ### 3. Manage Subscriptions via API Cancel, extend, or query subscriptions from your server: ```javascript theme={null} // Cancel at end of period await fetch(`https://merchant-api.rmz.gg/shawarma/subscriptions/${subId}/cancel`, { method: "POST", headers: { "Authorization": `Bearer ${process.env.RMZ_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ effective: "end_of_period" }) }); // Extend by 7 days (courtesy) await fetch(`https://merchant-api.rmz.gg/shawarma/subscriptions/${subId}/extend`, { method: "POST", headers: { "Authorization": `Bearer ${process.env.RMZ_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ days: 7 }) }); ``` *** ## Webhook Events Reference Subscribe to these events to keep your system in sync: | Event | When to Use | | ----------------------------- | ------------------------------------------- | | `subscription.created` | Provision access for new subscribers | | `subscription.activated` | Upgrade trial users to full access | | `subscription.renewed` | Confirm recurring payment and extend access | | `subscription.renewal_failed` | Alert customer about payment issues | | `subscription.past_due` | Show warning banners, send dunning emails | | `subscription.expired` | Revoke access | | `subscription.canceled` | Trigger retention flows | | `subscription.updated` | Handle plan changes and extensions | See the full [Webhook Events](/webhooks/events) documentation for payload examples. *** ## Customer Billing Portal The billing portal is a hosted page where customers can self-manage their subscriptions. You do not need to build any subscription management UI. ### Create a Portal Session ```javascript theme={null} // Server-side: generate a portal URL for the customer const response = await fetch( "https://merchant-api.rmz.gg/shawarma/portal-sessions", { method: "POST", headers: { "Authorization": `Bearer ${process.env.RMZ_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ customer: { country_code: "966", phone: "512345678" }, return_url: "https://yourapp.com/account" }) } ); const { data } = await response.json(); // Redirect customer to data.url ``` ### Portal Capabilities In the portal, customers can: * View all their subscriptions and status * Cancel a subscription (end of period or immediately) * Change subscription plan (upgrade or downgrade) * Update their payment method * View payment history and invoices See the [Portal Sessions API](/merchant-api/portal-sessions/create-portal-session) documentation for details. *** ## Auto-Renewal and Dunning When a customer has a saved payment card and `auto_renew` is enabled, RMZ automatically charges the card before the subscription period ends. ### Renewal Process 1. **3 days before period end**: RMZ attempts to charge the saved card 2. **If payment succeeds**: Subscription is renewed, `subscription.renewed` webhook fires 3. **If payment fails**: Subscription enters `past_due`, retry schedule begins ### Retry Schedule | Attempt | Timing | Webhook Event | | ------- | --------------------------- | ----------------------------- | | 1st | Immediately (at period end) | `subscription.renewal_failed` | | 2nd | 3 days after first failure | `subscription.renewal_failed` | | 3rd | 7 days after first failure | `subscription.renewal_failed` | | Final | After 3rd failure | `subscription.expired` | During the retry period, the subscription remains in `past_due` status. Access is still granted during this grace period to avoid disrupting the customer. Use the `subscription.renewal_failed` webhook to send the customer an email asking them to update their payment method. Include a link to the billing portal. *** ## Managing Subscriptions via API ### List Subscriptions ```bash theme={null} curl -X GET "https://merchant-api.rmz.gg/shawarma/subscriptions" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ### Get Subscription Details ```bash theme={null} curl -X GET "https://merchant-api.rmz.gg/shawarma/subscriptions/501" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ### Cancel a Subscription ```bash theme={null} curl -X POST "https://merchant-api.rmz.gg/shawarma/subscriptions/501/cancel" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"effective": "end_of_period"}' ``` ### Extend a Subscription ```bash theme={null} curl -X POST "https://merchant-api.rmz.gg/shawarma/subscriptions/501/extend" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"days": 7}' ``` See the full [Merchant API Subscriptions](/merchant-api/subscriptions/list-subscriptions) documentation for all available endpoints. *** ## Best Practices Never rely solely on API polling. Subscribe to webhook events and process them to keep your system in sync with subscription state. Your webhook handler may receive the same event more than once. Use the `X-RMZ-REQUEST-ID` header to deduplicate. Do not immediately revoke access when a subscription enters `past_due`. The customer still has access during the retry period. Default to `end_of_period` cancellation to preserve the customer experience. Only use `immediate` when necessary (e.g., policy violations). *** ## Next Steps Full list of subscription webhook events with payload examples. Complete API reference for subscription management. Create portal sessions for customer self-service. Learn how to set up and verify webhooks. # Webhook Integration Source: https://docs.rmz.gg/guides/webhook-integration Set up and consume RMZ webhooks to react to store events in real time. This guide walks through the complete process of setting up an RMZ webhook, building a receiver endpoint, verifying signatures, and handling common scenarios. ## Prerequisites * An RMZ store with an **RMZ+** subscription plan * A publicly accessible HTTPS endpoint to receive webhooks * Basic knowledge of your server framework (Express, Flask, Laravel, etc.) *** ## Step 1: Create a Webhook in the Dashboard Go to **Dashboard > Settings > Webhooks**. Click **Add Webhook** and fill in the configuration: | Field | Example Value | | ----- | ----------------------------------------- | | Name | Order Notifications | | Event | `order.created` | | URL | `https://api.yourdomain.com/webhooks/rmz` | | Tries | 3 | After saving, the webhook's **secret key** (28 characters) is shown. Copy and store it securely — you will need it to verify signatures. Toggle the webhook to **Enabled** when you are ready to receive events. *** ## Step 2: Build Your Receiver Endpoint Your endpoint must accept HTTP POST requests, verify the signature, process the payload, and return a `200` status quickly. ```javascript Node.js (Express) theme={null} const express = require("express"); const crypto = require("crypto"); const app = express(); // IMPORTANT: Use raw body for signature verification app.post( "/webhooks/rmz", express.raw({ type: "application/json" }), async (req, res) => { const signature = req.headers["signature"]; const requestId = req.headers["x-rmz-request-id"]; const rawBody = req.body.toString(); // 1. Verify signature const expectedSig = crypto .createHmac("sha256", process.env.RMZ_WEBHOOK_SECRET) .update(rawBody) .digest("hex"); if (!crypto.timingSafeEqual( Buffer.from(signature || ""), Buffer.from(expectedSig) )) { console.error("Invalid webhook signature"); return res.status(401).json({ error: "Invalid signature" }); } // 2. Parse payload const payload = JSON.parse(rawBody); console.log(`Received ${payload.event} (request: ${requestId})`); // 3. Respond immediately res.status(200).json({ received: true }); // 4. Process asynchronously processWebhook(payload, requestId); } ); async function processWebhook(payload, requestId) { switch (payload.event) { case "order.created": const order = payload.data; console.log(`New order #${order.id} from ${order.customer.firstName}`); console.log(`Amount: ${order.total}, Method: ${order.transaction.payment_method}`); // Your business logic here... break; case "order.status.changed": const updated = payload.data; const currentStatus = updated.status.status; console.log(`Order #${updated.id} status changed to ${currentStatus}`); break; } } app.listen(3000); ``` ```python Python (Flask) theme={null} import hmac import hashlib import json from flask import Flask, request, jsonify from threading import Thread app = Flask(__name__) WEBHOOK_SECRET = "your_28_char_secret_key" @app.route("/webhooks/rmz", methods=["POST"]) def handle_webhook(): # 1. Verify signature signature = request.headers.get("Signature", "") raw_body = request.data expected = hmac.new( WEBHOOK_SECRET.encode(), raw_body, hashlib.sha256 ).hexdigest() if not hmac.compare_digest(signature, expected): return jsonify({"error": "Invalid signature"}), 401 # 2. Parse payload payload = json.loads(raw_body) request_id = request.headers.get("X-RMZ-REQUEST-ID") # 3. Respond immediately # Process in background thread Thread(target=process_webhook, args=(payload, request_id)).start() return jsonify({"received": True}), 200 def process_webhook(payload, request_id): event = payload["event"] data = payload["data"] if event == "order.created": print(f"New order #{data['id']} - {data['total']} SAR") customer = data["customer"] print(f"Customer: {customer['firstName']} {customer['lastName']}") # Your business logic here... elif event == "order.status.changed": print(f"Order #{data['id']} status: {data['status']['status']}") if __name__ == "__main__": app.run(port=3000) ``` ```php PHP (Laravel) theme={null} header('Signature'); $rawBody = $request->getContent(); $secret = config('services.rmz.webhook_secret'); $expected = hash_hmac('sha256', $rawBody, $secret); if (!hash_equals($expected, $signature ?? '')) { Log::warning('Invalid webhook signature'); return response()->json(['error' => 'Invalid signature'], 401); } // 2. Parse payload $payload = json_decode($rawBody, true); $requestId = $request->header('X-RMZ-REQUEST-ID'); Log::info("Webhook received: {$payload['event']} (request: {$requestId})"); // 3. Dispatch to background job and respond immediately dispatch(function () use ($payload) { $this->processWebhook($payload); }); return response()->json(['received' => true], 200); } private function processWebhook(array $payload): void { $event = $payload['event']; $data = $payload['data']; match ($event) { 'order.created' => $this->handleOrderCreated($data), 'order.status.changed' => $this->handleOrderStatusChanged($data), default => Log::warning("Unknown webhook event: {$event}"), }; } private function handleOrderCreated(array $order): void { Log::info("New order #{$order['id']} - {$order['total']}"); // Your business logic... } private function handleOrderStatusChanged(array $order): void { $status = $order['status']['status']; Log::info("Order #{$order['id']} status changed to {$status}"); // Your business logic... } } ``` If you are using Laravel, make sure to exclude the webhook route from CSRF verification by adding it to the `$except` array in `App\Http\Middleware\VerifyCsrfToken`. *** ## Step 3: Handle Retries and Idempotency Webhooks may be delivered more than once. Use the `X-RMZ-REQUEST-ID` header to detect and ignore duplicates: ```javascript theme={null} // In-memory for demo — use Redis or a database in production const processedIds = new Set(); function isProcessed(requestId) { if (processedIds.has(requestId)) { return true; } processedIds.add(requestId); return false; } // In your webhook handler: if (isProcessed(requestId)) { console.log(`Duplicate webhook ${requestId}, skipping`); return res.status(200).json({ received: true }); } ``` Store processed request IDs in Redis with a 7-day TTL for automatic cleanup. In a database, use a unique constraint on the request ID column and catch the duplicate key error. *** ## Step 4: Test Your Webhook Before enabling the webhook for production: 1. **Use a tunneling tool** like ngrok to expose your local server: ```bash theme={null} ngrok http 3000 ``` Use the generated HTTPS URL as your webhook URL. 2. **Place a test order** in your store to trigger the `order.created` event. 3. **Check the webhook logs** in your dashboard to see the delivery status and response from your server. 4. **Verify the payload** matches the expected format documented in [Payload Format](/webhooks/payload-format). *** ## Common Patterns ### Sending Slack Notifications ```javascript theme={null} async function notifySlack(order) { await fetch(process.env.SLACK_WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: `New order #${order.id}`, blocks: [ { type: "section", text: { type: "mrkdwn", text: `*New Order #${order.id}*\n` + `Customer: ${order.customer.firstName} ${order.customer.lastName}\n` + `Amount: ${order.total} SAR\n` + `Payment: ${order.transaction.payment_method}` } } ] }) }); } ``` ### Syncing to a Database ```javascript theme={null} async function syncOrder(order) { await db.query( `INSERT INTO orders (rmz_order_id, customer_email, amount, status, created_at) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (rmz_order_id) DO UPDATE SET status = $4`, [order.id, order.customer.email, order.total, order.status.status, order.created_at] ); } ``` *** ## Troubleshooting * Verify the webhook is **enabled** in the dashboard * Check that your URL is publicly accessible (not `localhost`) * Ensure your endpoint accepts POST requests and returns a 2xx status * Check the webhook logs in the dashboard for error details * Use the **raw request body** for verification, not the parsed JSON * Ensure you are using the correct secret key for this specific webhook * Check for middleware that modifies the request body before your handler * This is expected behavior during retries. Implement idempotency using the `X-RMZ-REQUEST-ID` header. * Use the `created_at` timestamp in the `statuses` array to determine the correct chronological order. # RMZ Developer Docs Source: https://docs.rmz.gg/index Build on the leading digital products platform for the MENA region. # RMZ Developer Documentation RMZ is a multi-tenant e-commerce platform purpose-built for digital products in the MENA region. Merchants use RMZ to sell digital codes, software licenses, subscriptions, online courses, and services — with built-in payment processing, customer management, and storefront hosting. These docs cover every API and SDK you need to integrate with the platform, build custom storefronts, verify software licenses, and automate your store operations. ## APIs and SDKs Manage your store programmatically — orders, products, categories, subscriptions, and statistics. Authenticated with Bearer tokens. Build fully custom storefronts. Browse products, manage carts, handle checkout, and authenticate customers with OTP. Embed a quick-purchase checkout widget for any product on external websites via iframe. Verify software license keys from your applications. Supports HWID lock, IP lock, activation limits, and E2EE. TypeScript SDK for the Storefront API. Use it with React, Vue, Angular, or vanilla JS. Sell and verify FiveM scripts with built-in license enforcement and IP locking. ## Quick Links Make your first Merchant API call in under 5 minutes. Learn how auth works across all RMZ APIs. ## Open Source TypeScript SDK for building headless storefronts. Full example storefront built with Next.js and the Storefront SDK. FiveM server resource for license verification. License verification examples in Python, PHP, Node.js, and Lua. # Activations Source: https://docs.rmz.gg/licensing/activations How device activations work, max activation limits, and how to reset activations from the dashboard. The `max_activations` setting on your license product controls how many devices (or IPs) can use a single license key. ## Activation Limits | Setting | Behavior | | ------------ | --------------------------------------------- | | `1` | One device only. A second device is rejected. | | `3` | Up to three devices. The fourth is rejected. | | `0` or empty | Unlimited activations. | ## Auto-Activation Flow There is no separate "activate" API call. The `/verify` endpoint handles activation automatically: A call with `hwid: "machine-A"` activates the device and returns a valid response. Slot 1 is used. Another call with `hwid: "machine-A"` returns valid. The device is already activated -- the call is **idempotent** and does not consume an additional slot. A call with `hwid: "machine-B"` activates the second device. Slot 2 is used. If `max_activations` is reached and a new HWID or IP calls verify, the API returns the `ACTIVATION_LIMIT` error (HTTP 429). ## ACTIVATION\_LIMIT Error When all slots are used, the API responds with: ```json theme={null} { "success": false, "error": "ACTIVATION_LIMIT", "message": "Activation limit reached" } ``` The customer needs to either: * Use a device that is already activated * Ask the store owner to reset their activations * Purchase an additional license ## Resetting Activations Store owners can reset all device activations for a license key from the dashboard: 1. Go to your store dashboard 2. Navigate to **Licenses** 3. Find the license key (use search or filters) 4. Click the row actions menu 5. Select **Reset Activations** This frees all activation slots immediately. The next verification call from any device will re-activate it. Resetting activations is useful when a customer gets a new computer, reinstalls their OS, or changes their server IP. It does not affect the license validity or expiry date. ## Activation Data in Responses Every successful verification response includes activation counts: ```json theme={null} { "activations": { "current": 1, "max": 3, "remaining": 2 } } ``` | Field | Description | | ----------- | ------------------------------------- | | `current` | Number of devices currently activated | | `max` | Maximum allowed (`null` if unlimited) | | `remaining` | Slots left (`null` if unlimited) | Use this data to show your users how many devices they have left, or to warn them when they are approaching the limit. # End-to-End Encryption (E2EE) Source: https://docs.rmz.gg/licensing/e2ee Encrypt all license verification responses with AES-256-GCM to prevent man-in-the-middle attacks and proxy spoofing. When E2EE is enabled on a license product, **all** API responses (both success and error) are encrypted with AES-256-GCM. Your software decrypts the response locally using a shared encryption key. ## Why Use E2EE Without encryption, an attacker can intercept or modify the API response before it reaches your software. Common attacks include: * **Man-in-the-middle** -- intercepting and tampering with the response in transit * **Local proxy spoofing** -- setting up a fake local server that always returns `"success": true` * **Response sniffing** -- reading license details, plan info, or activation data E2EE prevents all three. Even if the response is intercepted, it cannot be read or modified without the encryption key. ## Setup Edit your license product in the dashboard and toggle **E2EE** on. After saving, copy the **encryption key** -- a 64-character hex string (representing 32 bytes). Store the key in your application. Your code will use it to decrypt every API response. ## Encrypted Response Format When E2EE is enabled, the API always responds with this structure (regardless of success or error): ```json theme={null} { "encrypted": true, "payload": "base64-encoded-ciphertext", "nonce": "base64-encoded-12-byte-nonce", "tag": "base64-encoded-16-byte-auth-tag" } ``` | Field | Description | | ----------- | -------------------------------------------- | | `encrypted` | Always `true` when E2EE is active | | `payload` | Base64-encoded AES-256-GCM ciphertext | | `nonce` | Base64-encoded 12-byte initialization vector | | `tag` | Base64-encoded 16-byte authentication tag | ## Decryption Steps 1. Base64-decode `payload`, `nonce`, and `tag` 2. Convert your 64-character hex key to 32 raw bytes 3. Decrypt using AES-256-GCM with the key, nonce, and tag 4. Parse the resulting plaintext as JSON -- this is the same response you would get without E2EE ## Decryption by Language Uses the built-in `crypto` module. No dependencies needed. ```javascript theme={null} const crypto = require("crypto"); function decryptResponse(body, hexKey) { const key = Buffer.from(hexKey, "hex"); const nonce = Buffer.from(body.nonce, "base64"); const tag = Buffer.from(body.tag, "base64"); const ciphertext = Buffer.from(body.payload, "base64"); const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce); decipher.setAuthTag(tag); let plaintext = decipher.update(ciphertext, null, "utf8"); plaintext += decipher.final("utf8"); return JSON.parse(plaintext); } ``` Requires the `cryptography` library: `pip install cryptography` ```python theme={null} import json, base64 from cryptography.hazmat.primitives.ciphers.aead import AESGCM def decrypt_response(body, hex_key): key = bytes.fromhex(hex_key) nonce = base64.b64decode(body["nonce"]) tag = base64.b64decode(body["tag"]) ciphertext = base64.b64decode(body["payload"]) aesgcm = AESGCM(key) plaintext = aesgcm.decrypt(nonce, ciphertext + tag, None) return json.loads(plaintext) ``` Uses the built-in OpenSSL extension (enabled by default in PHP 7.1+). ```php theme={null} function decrypt_response(array $body, string $hexKey): ?array { $key = hex2bin($hexKey); $nonce = base64_decode($body['nonce']); $tag = base64_decode($body['tag']); $ciphertext = base64_decode($body['payload']); $plaintext = openssl_decrypt( $ciphertext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $nonce, $tag ); if ($plaintext === false) { return null; } return json_decode($plaintext, true); } ``` FiveM's Lua runtime does not have built-in AES-256-GCM support. The recommended approach is to run a small Node.js helper that handles decryption, and call it from Lua via HTTP. ```lua theme={null} local DECRYPT_URL = "http://127.0.0.1:3999/decrypt" local function DecryptResponse(encryptedBody, callback) local payload = json.encode({ payload = encryptedBody.payload, nonce = encryptedBody.nonce, tag = encryptedBody.tag, key = ENCRYPTION_KEY, }) PerformHttpRequest(DECRYPT_URL, function(statusCode, responseText) if statusCode == 200 then callback(json.decode(responseText)) else callback(nil) end end, "POST", payload, { ["Content-Type"] = "application/json" }) end ``` See the [Lua / FiveM example](/licensing/examples/lua-fivem) for the full implementation. ## Key Rotation You can rotate the encryption key at any time from the product edit page in your dashboard. When you rotate: * A new 64-character hex key is generated * The old key stops working **immediately** * You must update the key in your software and push a new version to your users After rotating the key, any running instances of your software using the old key will fail to decrypt responses. Plan key rotations alongside software updates. ## Security Considerations ### What E2EE protects against * Intercepting and modifying API responses in transit * Setting up a fake local server that always returns `"success": true` * Sniffing license details, plan info, or activation data from the network ### What E2EE does not protect against * Reverse engineering your compiled binary to extract the encryption key or bypass the check entirely * Memory dumping at runtime to read decrypted responses * Patching your binary to skip the verification call ### Recommendations E2EE raises the bar significantly, but treat it as one layer in a defense-in-depth approach, not a silver bullet. * **Obfuscate your code** -- make it harder to read and reverse engineer * **Embed verification deep** in your application logic, not in a single easily-patchable function * **Use native compilation** (not interpreted scripts) when possible * **Combine license verification** with other integrity checks * **Don't store the encryption key as a plain string** -- derive it at runtime from multiple values # Error Codes Source: https://docs.rmz.gg/licensing/error-codes All license verification error codes, HTTP status codes, and rate limiting behavior. When a license verification fails, the API returns a JSON response with `"success": false`, an error code, and a human-readable message. ## Error Response Format ```json theme={null} { "success": false, "error": "LICENSE_NOT_FOUND", "message": "License not found" } ``` ## Error Codes | Error Code | HTTP Status | Message | Description | | ------------------- | ----------- | --------------------------------- | ------------------------------------------------------------------------------- | | `LICENSE_NOT_FOUND` | 404 | License not found | The license key does not exist for this product ID | | `LICENSE_EXPIRED` | 403 | License has expired | The license has passed its expiry date | | `LICENSE_REVOKED` | 403 | License has been revoked | The license has been permanently revoked (e.g. due to refund or manual action) | | `LICENSE_SUSPENDED` | 403 | License is suspended | The license has been temporarily suspended by the store owner | | `HWID_REQUIRED` | 403 | HWID is required for this license | The product lock type is `hwid` but no `hwid` field was included in the request | | `ACTIVATION_LIMIT` | 429 | Maximum activations reached | All activation slots are used. The device cannot be activated. | ## Handling Errors in Your Software ```javascript theme={null} const result = await verifyLicense(key, hwid); if (!result.valid) { switch (result.error) { case "LICENSE_NOT_FOUND": console.log("Invalid license key. Please check and try again."); break; case "LICENSE_EXPIRED": console.log("Your license has expired. Please renew."); break; case "LICENSE_REVOKED": console.log("This license has been revoked."); break; case "LICENSE_SUSPENDED": console.log("This license is temporarily suspended. Contact support."); break; case "HWID_REQUIRED": console.log("Hardware ID is required for this product."); break; case "ACTIVATION_LIMIT": console.log("Device limit reached. Deactivate another device first."); break; default: console.log("Verification failed:", result.message); } } ``` ```python theme={null} result = verify_license(key, hwid=hwid) if not result["valid"]: error = result["error"] if error == "LICENSE_NOT_FOUND": print("Invalid license key. Please check and try again.") elif error == "LICENSE_EXPIRED": print("Your license has expired. Please renew.") elif error == "LICENSE_REVOKED": print("This license has been revoked.") elif error == "LICENSE_SUSPENDED": print("This license is temporarily suspended. Contact support.") elif error == "HWID_REQUIRED": print("Hardware ID is required for this product.") elif error == "ACTIVATION_LIMIT": print("Device limit reached. Deactivate another device first.") else: print(f"Verification failed: {result['message']}") ``` ## Rate Limiting The verification endpoint is rate limited to **60 requests per minute per IP address**. This is more than sufficient for typical usage -- your software usually verifies once on startup or periodically (e.g. every few hours). When the rate limit is exceeded, the API returns: | HTTP Status | Description | | ----------- | ----------------- | | 429 | Too Many Requests | If you receive HTTP 429, implement retry with exponential backoff. Do not immediately retry in a tight loop. If your software runs on a server with many users behind the same IP, consider caching the verification result locally for a few minutes to reduce API calls. # Node.js Examples Source: https://docs.rmz.gg/licensing/examples/javascript Complete Node.js examples for license verification, with and without end-to-end encryption. Full working Node.js examples for the RMZ license verification API. Requires Node.js 18+ (uses built-in `fetch`). No external dependencies needed -- even E2EE uses the built-in `crypto` module. All examples are available on GitHub: [github.com/Rmz-App/rmz-license-examples](https://github.com/Rmz-App/rmz-license-examples) ## Plain Verification ```javascript verify.js theme={null} /** * RMZ License Verification — Node.js (without E2EE) * https://license.rmz.gg * * No dependencies required — uses built-in fetch (Node 18+) or install node-fetch. */ // ============================================= // Configuration — change these values // ============================================= const PRODUCT_ID = 0; // Replace with your product ID from the dashboard const API_URL = "https://license.rmz.gg/verify"; /** * Generate a unique hardware ID for this machine. */ function getHwid() { const crypto = require("crypto"); const os = require("os"); const raw = `${os.hostname()}-${os.arch()}-${os.platform()}`; return crypto.createHash("sha256").update(raw).digest("hex").slice(0, 32); } /** * Verify a license key with the RMZ API. * * @param {string} licenseKey - The license key to verify * @param {string|null} hwid - Hardware ID for device binding * @returns {Promise<{valid: boolean, data?: object, error?: string, message?: string}>} */ async function verifyLicense(licenseKey, hwid = null) { const payload = { product_id: PRODUCT_ID, license_key: licenseKey, }; if (hwid) payload.hwid = hwid; try { const resp = await fetch(API_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const body = await resp.json(); if (resp.ok && body.success) { return { valid: true, data: body.data }; } return { valid: false, error: body.error || "UNKNOWN", message: body.message || "Unknown error", }; } catch (e) { return { valid: false, error: "CONNECTION_ERROR", message: e.message }; } } // ============================================= // Example usage // ============================================= (async () => { const hwid = getHwid(); console.log(`Machine HWID: ${hwid}`); const readline = require("readline"); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question("Enter your license key: ", async (licenseKey) => { const result = await verifyLicense(licenseKey.trim(), hwid); if (result.valid) { const info = result.data; console.log(`\n Status: ${info.status}`); console.log(` Product: ${info.product.name}`); if (info.plan) { console.log(` Plan: ${info.plan.duration_label}`); console.log(` Start: ${info.plan.start_date}`); console.log(` End: ${info.plan.end_date}`); } if (info.expires_at) { console.log(` Expires: ${info.expires_at} (${info.expires_in_days} days left)`); } console.log(` Devices: ${info.activations.current}/${info.activations.max || "unlimited"}`); } else { console.log(`\n Error: ${result.error}`); console.log(` Message: ${result.message}`); } rl.close(); }); })(); ``` ## With E2EE When E2EE is enabled on your product, all responses are AES-256-GCM encrypted. This example decrypts them using Node.js built-in `crypto`. ```javascript verify_e2ee.js theme={null} /** * RMZ License Verification — Node.js (with E2EE) * https://license.rmz.gg * * All API responses are AES-256-GCM encrypted. This prevents * man-in-the-middle attacks and local proxy spoofing. * * No dependencies required — uses built-in crypto and fetch (Node 18+). */ const crypto = require("crypto"); // ============================================= // Configuration — change these values // ============================================= const PRODUCT_ID = 0; // Replace with your product ID from the dashboard const ENCRYPTION_KEY = ""; // Replace with your 64-character hex key from product settings const API_URL = "https://license.rmz.gg/verify"; /** * Generate a unique hardware ID for this machine. */ function getHwid() { const os = require("os"); const raw = `${os.hostname()}-${os.arch()}-${os.platform()}`; return crypto.createHash("sha256").update(raw).digest("hex").slice(0, 32); } /** * Decrypt an AES-256-GCM encrypted API response. * * @param {object} body - The response with 'payload', 'nonce', 'tag' fields * @returns {object|null} The decrypted data, or null if decryption fails */ function decryptResponse(body) { try { const key = Buffer.from(ENCRYPTION_KEY, "hex"); const nonce = Buffer.from(body.nonce, "base64"); const tag = Buffer.from(body.tag, "base64"); const ciphertext = Buffer.from(body.payload, "base64"); const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce); decipher.setAuthTag(tag); let plaintext = decipher.update(ciphertext, null, "utf8"); plaintext += decipher.final("utf8"); return JSON.parse(plaintext); } catch { return null; } } /** * Verify a license key with the RMZ API (E2EE mode). * Both success and error responses are encrypted. * * @param {string} licenseKey - The license key to verify * @param {string|null} hwid - Hardware ID for device binding * @returns {Promise<{valid: boolean, data?: object, error?: string, message?: string}>} */ async function verifyLicense(licenseKey, hwid = null) { const payload = { product_id: PRODUCT_ID, license_key: licenseKey, }; if (hwid) payload.hwid = hwid; try { const resp = await fetch(API_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); let body = await resp.json(); // All responses are encrypted when E2EE is enabled if (body.encrypted) { body = decryptResponse(body); if (!body) { return { valid: false, error: "DECRYPTION_FAILED", message: "Wrong encryption key" }; } } if (body.success) { return { valid: true, data: body.data }; } return { valid: false, error: body.error || "UNKNOWN", message: body.message || "Unknown error", }; } catch (e) { return { valid: false, error: "CONNECTION_ERROR", message: e.message }; } } // ============================================= // Example usage // ============================================= (async () => { const hwid = getHwid(); console.log(`Machine HWID: ${hwid}`); const readline = require("readline"); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question("Enter your license key: ", async (licenseKey) => { const result = await verifyLicense(licenseKey.trim(), hwid); if (result.valid) { const info = result.data; console.log(`\n Status: ${info.status}`); console.log(` Product: ${info.product.name}`); if (info.plan) { console.log(` Plan: ${info.plan.duration_label}`); console.log(` Start: ${info.plan.start_date}`); console.log(` End: ${info.plan.end_date}`); } if (info.expires_at) { console.log(` Expires: ${info.expires_at} (${info.expires_in_days} days left)`); } console.log(` Devices: ${info.activations.current}/${info.activations.max || "unlimited"}`); } else { console.log(`\n Error: ${result.error}`); console.log(` Message: ${result.message}`); } rl.close(); }); })(); ``` ## Key Functions | Function | Description | | -------------------------- | -------------------------------------------------------------------------------------------- | | `getHwid()` | Generates a SHA-256 hash from hostname, architecture, and platform | | `verifyLicense(key, hwid)` | Sends a POST request to the API and returns `{ valid, data }` or `{ valid, error, message }` | | `decryptResponse(body)` | (E2EE only) Decrypts AES-256-GCM response using the encryption key | ## Dependencies | Mode | Dependencies | | ------------ | ----------------------------------- | | Without E2EE | None (built-in `fetch` in Node 18+) | | With E2EE | None (built-in `crypto` module) | If you are using Node.js older than 18, install `node-fetch` as a dependency and import it: `const fetch = require("node-fetch")`. # Lua / FiveM Examples Source: https://docs.rmz.gg/licensing/examples/lua-fivem Complete FiveM Lua examples for license verification, with and without end-to-end encryption. Full working FiveM Lua examples for the RMZ license verification API. Uses FiveM's built-in `PerformHttpRequest` for HTTP calls. All examples are available on GitHub: [github.com/Rmz-App/rmz-license-examples](https://github.com/Rmz-App/rmz-license-examples) ## Dependencies | Mode | Dependencies | | ------------ | ------------------------------------------ | | Without E2EE | None (FiveM built-in HTTP) | | With E2EE | Node.js helper (Lua has no native AES-GCM) | ## Plain Verification ```lua verify.lua theme={null} --[[ RMZ License Verification — Lua / FiveM (without E2EE) https://license.rmz.gg Drop this file into your FiveM resource and call VerifyLicense(). ]] -- ============================================= -- Configuration — change these values -- ============================================= local PRODUCT_ID = 0 -- Replace with your product ID from the dashboard local API_URL = "https://license.rmz.gg/verify" --- Generate a simple hardware identifier from the server's hostname. --- For FiveM, you can also use the player's license identifier. --- @return string local function GetHwid() return GetConvar("sv_hostname", "unknown") .. "-" .. GetConvar("sv_maxclients", "0") end --- Verify a license key with the RMZ API. --- @param licenseKey string The license key to verify --- @param hwid string|nil Hardware ID for device binding --- @param callback function Called with (valid, data_or_error) local function VerifyLicense(licenseKey, hwid, callback) local payload = json.encode({ product_id = PRODUCT_ID, license_key = licenseKey, hwid = hwid, }) PerformHttpRequest(API_URL, function(statusCode, responseText, headers) if statusCode == 0 then callback(false, { error = "CONNECTION_ERROR", message = "Could not connect to license server" }) return end local body = json.decode(responseText) if statusCode == 200 and body and body.success then callback(true, body.data) else callback(false, { error = body and body.error or "UNKNOWN", message = body and body.message or "Unknown error", }) end end, "POST", payload, { ["Content-Type"] = "application/json" }) end -- ============================================= -- Example usage (FiveM server-side) -- ============================================= local LICENSE_KEY = "" -- Replace with your license key Citizen.CreateThread(function() local hwid = GetHwid() print("[RMZ License] Verifying license...") VerifyLicense(LICENSE_KEY, hwid, function(valid, data) if valid then print("[RMZ License] Valid!") print(" Product: " .. data.product.name) print(" Status: " .. data.status) if data.expires_at then print(" Expires: " .. data.expires_at) end else print("[RMZ License] INVALID: " .. data.error .. " — " .. data.message) -- Optionally stop the resource: -- StopResource(GetCurrentResourceName()) end end) end) ``` ## With E2EE FiveM's Lua runtime does not have built-in AES-256-GCM decryption. The recommended approach is to run a small Node.js helper alongside your resource that handles decryption via a local HTTP endpoint. Lua has no native AES-GCM support. The E2EE example below requires a local Node.js decryption service running on `http://127.0.0.1:3999/decrypt`. You can use the [Node.js E2EE example](/licensing/examples/javascript) as a starting point for the helper. ```lua verify_e2ee.lua theme={null} --[[ RMZ License Verification — Lua / FiveM (with E2EE) https://license.rmz.gg NOTE: FiveM's Lua does not have built-in AES-256-GCM decryption. For E2EE in FiveM, use the server-side Node.js bridge approach below. This file shows how to call a local Node.js decryption endpoint that your resource runs alongside the Lua script. Alternative: Use the Node.js example (verify_e2ee.js) as a standalone server-side script for FiveM resources. ]] -- ============================================= -- Configuration — change these values -- ============================================= local PRODUCT_ID = 0 -- Replace with your product ID from the dashboard local ENCRYPTION_KEY = "" -- Replace with your 64-character hex key from product settings local API_URL = "https://license.rmz.gg/verify" -- Local decryption endpoint (run verify_e2ee.js as a helper) -- Or handle decryption in your Node.js FiveM server script local DECRYPT_URL = "http://127.0.0.1:3999/decrypt" --- Generate a simple hardware identifier. --- @return string local function GetHwid() return GetConvar("sv_hostname", "unknown") .. "-" .. GetConvar("sv_maxclients", "0") end --- Decrypt an encrypted response via the local Node.js helper. --- @param encryptedBody table The encrypted response body --- @param callback function Called with decrypted table or nil local function DecryptResponse(encryptedBody, callback) local payload = json.encode({ payload = encryptedBody.payload, nonce = encryptedBody.nonce, tag = encryptedBody.tag, key = ENCRYPTION_KEY, }) PerformHttpRequest(DECRYPT_URL, function(statusCode, responseText) if statusCode == 200 then callback(json.decode(responseText)) else callback(nil) end end, "POST", payload, { ["Content-Type"] = "application/json" }) end --- Verify a license key with the RMZ API (E2EE mode). --- Both success and error responses are encrypted. --- @param licenseKey string --- @param hwid string|nil --- @param callback function Called with (valid, data_or_error) local function VerifyLicense(licenseKey, hwid, callback) local payload = json.encode({ product_id = PRODUCT_ID, license_key = licenseKey, hwid = hwid, }) PerformHttpRequest(API_URL, function(statusCode, responseText, headers) if statusCode == 0 then callback(false, { error = "CONNECTION_ERROR", message = "Could not connect to license server" }) return end local body = json.decode(responseText) -- All responses are encrypted when E2EE is enabled if body and body.encrypted then DecryptResponse(body, function(decrypted) if not decrypted then callback(false, { error = "DECRYPTION_FAILED", message = "Wrong encryption key" }) return end if decrypted.success then callback(true, decrypted.data) else callback(false, { error = decrypted.error or "UNKNOWN", message = decrypted.message or "Unknown error", }) end end) return end if statusCode == 200 and body and body.success then callback(true, body.data) else callback(false, { error = body and body.error or "UNKNOWN", message = body and body.message or "Unknown error", }) end end, "POST", payload, { ["Content-Type"] = "application/json" }) end -- ============================================= -- Example usage (FiveM server-side) -- ============================================= local LICENSE_KEY = "" -- Replace with your license key Citizen.CreateThread(function() local hwid = GetHwid() print("[RMZ License] Verifying license (E2EE)...") VerifyLicense(LICENSE_KEY, hwid, function(valid, data) if valid then print("[RMZ License] Valid!") print(" Product: " .. data.product.name) print(" Status: " .. data.status) if data.expires_at then print(" Expires: " .. data.expires_at) end else print("[RMZ License] INVALID: " .. data.error .. " — " .. data.message) end end) end) ``` ## Key Functions | Function | Description | | ------------------------------------ | -------------------------------------------------------------------------- | | `GetHwid()` | Generates an identifier from server hostname and max clients | | `VerifyLicense(key, hwid, callback)` | Sends a POST via `PerformHttpRequest` and calls back with `(valid, data)` | | `DecryptResponse(body, callback)` | (E2EE only) Sends encrypted payload to local Node.js helper for decryption | ## FiveM-Specific Notes All HTTP calls in FiveM are **asynchronous**. The `VerifyLicense` function uses a callback pattern rather than returning a value directly. Make sure your resource logic accounts for this. For a simpler setup, consider verifying the license once on resource start inside a `Citizen.CreateThread`. If the license is invalid, call `StopResource(GetCurrentResourceName())` to prevent the resource from running. # PHP Examples Source: https://docs.rmz.gg/licensing/examples/php Complete PHP examples for license verification, with and without end-to-end encryption. Full working PHP examples for the RMZ license verification API. No external dependencies needed -- uses built-in cURL and OpenSSL. All examples are available on GitHub: [github.com/Rmz-App/rmz-license-examples](https://github.com/Rmz-App/rmz-license-examples) ## Dependencies | Mode | Dependencies | | ------------ | -------------------------------------- | | Without E2EE | cURL extension (built-in) | | With E2EE | OpenSSL extension (built-in, PHP 7.1+) | ## Plain Verification ```php verify.php theme={null} bool, 'data' => [...]] or ['valid' => false, 'error' => '...', 'message' => '...'] */ function verify_license(string $licenseKey, ?string $hwid = null): array { $payload = [ 'product_id' => PRODUCT_ID, 'license_key' => $licenseKey, ]; if ($hwid) { $payload['hwid'] = $hwid; } $ch = curl_init(API_URL); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if ($error) { return ['valid' => false, 'error' => 'CONNECTION_ERROR', 'message' => $error]; } $body = json_decode($response, true); if ($httpCode === 200 && ($body['success'] ?? false)) { return ['valid' => true, 'data' => $body['data']]; } return [ 'valid' => false, 'error' => $body['error'] ?? 'UNKNOWN', 'message' => $body['message'] ?? 'Unknown error', ]; } // ============================================= // Example usage // ============================================= $hwid = get_hwid(); echo "Machine HWID: $hwid\n"; echo "Enter your license key: "; $licenseKey = trim(fgets(STDIN)); $result = verify_license($licenseKey, $hwid); if ($result['valid']) { $info = $result['data']; echo "\n Status: {$info['status']}\n"; echo " Product: {$info['product']['name']}\n"; if (!empty($info['plan'])) { echo " Plan: {$info['plan']['duration_label']}\n"; echo " Start: {$info['plan']['start_date']}\n"; echo " End: {$info['plan']['end_date']}\n"; } if (!empty($info['expires_at'])) { echo " Expires: {$info['expires_at']} ({$info['expires_in_days']} days left)\n"; } $max = $info['activations']['max'] ?? 'unlimited'; echo " Devices: {$info['activations']['current']}/{$max}\n"; } else { echo "\n Error: {$result['error']}\n"; echo " Message: {$result['message']}\n"; } ``` ## With E2EE When E2EE is enabled, all responses are AES-256-GCM encrypted. This example decrypts them using PHP's built-in `openssl_decrypt`. ```php verify_e2ee.php theme={null} bool, 'data' => [...]] or ['valid' => false, 'error' => '...', 'message' => '...'] */ function verify_license(string $licenseKey, ?string $hwid = null): array { $payload = [ 'product_id' => PRODUCT_ID, 'license_key' => $licenseKey, ]; if ($hwid) { $payload['hwid'] = $hwid; } $ch = curl_init(API_URL); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload), CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, ]); $response = curl_exec($ch); $error = curl_error($ch); curl_close($ch); if ($error) { return ['valid' => false, 'error' => 'CONNECTION_ERROR', 'message' => $error]; } $body = json_decode($response, true); // All responses are encrypted when E2EE is enabled if (!empty($body['encrypted'])) { $body = decrypt_response($body); if ($body === null) { return ['valid' => false, 'error' => 'DECRYPTION_FAILED', 'message' => 'Wrong encryption key']; } } if ($body['success'] ?? false) { return ['valid' => true, 'data' => $body['data']]; } return [ 'valid' => false, 'error' => $body['error'] ?? 'UNKNOWN', 'message' => $body['message'] ?? 'Unknown error', ]; } // ============================================= // Example usage // ============================================= $hwid = get_hwid(); echo "Machine HWID: $hwid\n"; echo "Enter your license key: "; $licenseKey = trim(fgets(STDIN)); $result = verify_license($licenseKey, $hwid); if ($result['valid']) { $info = $result['data']; echo "\n Status: {$info['status']}\n"; echo " Product: {$info['product']['name']}\n"; if (!empty($info['plan'])) { echo " Plan: {$info['plan']['duration_label']}\n"; echo " Start: {$info['plan']['start_date']}\n"; echo " End: {$info['plan']['end_date']}\n"; } if (!empty($info['expires_at'])) { echo " Expires: {$info['expires_at']} ({$info['expires_in_days']} days left)\n"; } $max = $info['activations']['max'] ?? 'unlimited'; echo " Devices: {$info['activations']['current']}/{$max}\n"; } else { echo "\n Error: {$result['error']}\n"; echo " Message: {$result['message']}\n"; } ``` ## Key Functions | Function | Description | | ----------------------------- | --------------------------------------------------------------------- | | `get_hwid()` | Generates a SHA-256 hash from hostname, machine type, and OS name | | `verify_license($key, $hwid)` | Sends a cURL POST request to the API and returns an associative array | | `decrypt_response($body)` | (E2EE only) Decrypts AES-256-GCM response using `openssl_decrypt` | PHP's `openssl_decrypt` with `aes-256-gcm` accepts the authentication tag as a separate parameter, unlike Python's `cryptography` library which expects it concatenated with the ciphertext. # Python Examples Source: https://docs.rmz.gg/licensing/examples/python Complete Python examples for license verification, with and without end-to-end encryption. Full working Python examples for the RMZ license verification API. All examples are available on GitHub: [github.com/Rmz-App/rmz-license-examples](https://github.com/Rmz-App/rmz-license-examples) ## Dependencies | Mode | Install | | ------------ | ----------------------------------- | | Without E2EE | `pip install requests` | | With E2EE | `pip install requests cryptography` | ## Plain Verification ```python verify.py theme={null} """ RMZ License Verification — Python (without E2EE) https://license.rmz.gg Requirements: pip install requests """ import requests import hashlib import platform import uuid # ============================================= # Configuration — change these values # ============================================= PRODUCT_ID = 61200 # Replace with your product ID from the dashboard API_URL = "https://license.rmz.gg/verify" def get_hwid(): """Generate a unique hardware ID for this machine.""" raw = f"{platform.node()}-{platform.machine()}-{uuid.getnode()}" return hashlib.sha256(raw.encode()).hexdigest()[:32] def verify_license(license_key, hwid=None): """ Verify a license key with the RMZ API. Returns: dict — { "valid": True, "data": {...} } on success — { "valid": False, "error": "...", "message": "..." } on failure """ payload = { "product_id": PRODUCT_ID, "license_key": license_key, } if hwid: payload["hwid"] = hwid try: resp = requests.post(API_URL, json=payload, timeout=10) body = resp.json() if resp.status_code == 200 and body.get("success"): return {"valid": True, "data": body["data"]} else: return { "valid": False, "error": body.get("error", "UNKNOWN"), "message": body.get("message", "Unknown error"), } except requests.RequestException as e: return {"valid": False, "error": "CONNECTION_ERROR", "message": str(e)} # ============================================= # Example usage # ============================================= if __name__ == "__main__": hwid = get_hwid() print(f"Machine HWID: {hwid}") license_key = input("Enter your license key: ").strip() result = verify_license(license_key, hwid=hwid) if result["valid"]: info = result["data"] print(f"\n Status: {info['status']}") print(f" Product: {info['product']['name']}") if info.get("plan"): print(f" Plan: {info['plan']['duration_label']}") print(f" Start: {info['plan']['start_date']}") print(f" End: {info['plan']['end_date']}") if info.get("expires_at"): print(f" Expires: {info['expires_at']} ({info['expires_in_days']} days left)") print(f" Devices: {info['activations']['current']}/{info['activations']['max'] or 'unlimited'}") else: print(f"\n Error: {result['error']}") print(f" Message: {result['message']}") ``` ## With E2EE When E2EE is enabled, all responses are AES-256-GCM encrypted. This example uses the `cryptography` library to decrypt them. ```python verify_e2ee.py theme={null} """ RMZ License Verification — Python (with E2EE) https://license.rmz.gg All API responses are AES-256-GCM encrypted. This prevents man-in-the-middle attacks and local proxy spoofing. Requirements: pip install requests cryptography """ import requests import hashlib import platform import uuid import json import base64 from cryptography.hazmat.primitives.ciphers.aead import AESGCM # ============================================= # Configuration — change these values # ============================================= PRODUCT_ID = 61200 # Replace with your product ID from the dashboard ENCRYPTION_KEY = "0f53ec151f1267d547b73978afe2cea559821b0d3b4c6bf809cc23a7b2003c18" # Replace with your 64-character hex key from product settings API_URL = "https://license.rmz.gg/verify" def get_hwid(): """Generate a unique hardware ID for this machine.""" raw = f"{platform.node()}-{platform.machine()}-{uuid.getnode()}" return hashlib.sha256(raw.encode()).hexdigest()[:32] def decrypt_response(body, hex_key): """ Decrypt an AES-256-GCM encrypted API response. Args: body: The JSON response with 'payload', 'nonce', 'tag' fields hex_key: The 64-character hex encryption key from product settings Returns: dict — the decrypted JSON data, or None if decryption fails """ try: key = bytes.fromhex(hex_key) nonce = base64.b64decode(body["nonce"]) tag = base64.b64decode(body["tag"]) ciphertext = base64.b64decode(body["payload"]) aesgcm = AESGCM(key) plaintext = aesgcm.decrypt(nonce, ciphertext + tag, None) return json.loads(plaintext) except Exception: return None def verify_license(license_key, hwid=None): """ Verify a license key with the RMZ API (E2EE mode). Both success and error responses are encrypted. Returns: dict — { "valid": True, "data": {...} } on success — { "valid": False, "error": "...", "message": "..." } on failure """ payload = { "product_id": PRODUCT_ID, "license_key": license_key, } if hwid: payload["hwid"] = hwid try: resp = requests.post(API_URL, json=payload, timeout=10) body = resp.json() # All responses are encrypted when E2EE is enabled if body.get("encrypted"): body = decrypt_response(body, ENCRYPTION_KEY) if body is None: return {"valid": False, "error": "DECRYPTION_FAILED", "message": "Wrong encryption key"} if body.get("success"): return {"valid": True, "data": body["data"]} else: return { "valid": False, "error": body.get("error", "UNKNOWN"), "message": body.get("message", "Unknown error"), } except requests.RequestException as e: return {"valid": False, "error": "CONNECTION_ERROR", "message": str(e)} # ============================================= # Example usage # ============================================= if __name__ == "__main__": hwid = get_hwid() print(f"Machine HWID: {hwid}") license_key = input("Enter your license key: ").strip() result = verify_license(license_key, hwid=hwid) if result["valid"]: info = result["data"] print(f"\n Status: {info['status']}") print(f" Product: {info['product']['name']}") if info.get("plan"): print(f" Plan: {info['plan']['duration_label']}") print(f" Start: {info['plan']['start_date']}") print(f" End: {info['plan']['end_date']}") if info.get("expires_at"): print(f" Expires: {info['expires_at']} ({info['expires_in_days']} days left)") print(f" Devices: {info['activations']['current']}/{info['activations']['max'] or 'unlimited'}") else: print(f"\n Error: {result['error']}") print(f" Message: {result['message']}") ``` ## Key Functions | Function | Description | | --------------------------------- | ------------------------------------------------------------------------------------------------------ | | `get_hwid()` | Generates a SHA-256 hash from hostname, machine architecture, and MAC address | | `verify_license(key, hwid)` | Sends a POST request to the API and returns `{ "valid", "data" }` or `{ "valid", "error", "message" }` | | `decrypt_response(body, hex_key)` | (E2EE only) Decrypts AES-256-GCM response using the `cryptography` library | The `AESGCM` class from `cryptography` expects the ciphertext and tag concatenated together. That is why the decryption call passes `ciphertext + tag` as a single value. # Lock Types Source: https://docs.rmz.gg/licensing/lock-types Control how license keys are bound to devices. Choose between no binding, hardware ID locking, or IP address locking. When creating a license product, you choose a **lock type** that determines how keys are bound to devices. There are three options. ## `none` -- No Device Binding Any device can verify the key. No `hwid` field is needed in the request. This is the simplest option for software that does not need device-level restrictions. ```json theme={null} { "product_id": 123, "license_key": "MYAPP-XXXX-XXXX-XXXX-XXXX" } ``` Use `none` when you only care about whether the key is valid, not which device is using it. Activations still apply -- each unique verification counts as an activation if `max_activations` is set. ## `hwid` -- Hardware ID Lock Each device sends a machine fingerprint (hardware ID). The key auto-binds to the HWID on the first verification call. Subsequent calls from the same HWID are idempotent. A new HWID consumes an activation slot. ```json theme={null} { "product_id": 123, "license_key": "MYAPP-XXXX-XXXX-XXXX-XXXX", "hwid": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4" } ``` If the lock type is `hwid` and the request does not include an `hwid` field, the API returns the `HWID_REQUIRED` error (HTTP 403). ### Generating a Hardware ID Combine machine-specific values and hash them to produce a consistent, unique identifier. The goal is a string that stays the same across reboots but differs between machines. ```javascript Node.js theme={null} const crypto = require("crypto"); const os = require("os"); const raw = `${os.hostname()}-${os.arch()}-${os.platform()}`; const hwid = crypto.createHash("sha256").update(raw).digest("hex").slice(0, 32); ``` ```python Python theme={null} import hashlib, platform, uuid raw = f"{platform.node()}-{platform.machine()}-{uuid.getnode()}" hwid = hashlib.sha256(raw.encode()).hexdigest()[:32] ``` ```php PHP theme={null} $raw = php_uname('n') . '-' . php_uname('m') . '-' . php_uname('s'); $hwid = substr(hash('sha256', $raw), 0, 32); ``` ```lua FiveM Lua theme={null} local hwid = GetConvar("sv_hostname", "unknown") .. "-" .. GetConvar("sv_maxclients", "0") ``` For stronger HWIDs, include additional hardware identifiers like MAC address, CPU ID, or disk serial number. The more inputs you combine, the harder it is to spoof. ## `ip` -- IP Address Lock The API auto-detects the caller's IP address and binds it to the key. No `hwid` field is needed in the request. Each unique IP uses an activation slot. ```json theme={null} { "product_id": 123, "license_key": "MYAPP-XXXX-XXXX-XXXX-XXXX" } ``` IP locking is useful for server-side software (e.g. FiveM resources, web apps, bots) where the IP is stable. It is not recommended for end-user desktop software because consumer IPs change frequently. ## Comparison | Feature | `none` | `hwid` | `ip` | | --------------------- | ---------------------- | ------------------------- | -------------------- | | Device binding | No | Yes (machine fingerprint) | Yes (IP address) | | `hwid` field required | No | Yes | No | | Best for | Simple key-only checks | Desktop software | Server-side software | | Spoofing difficulty | N/A | Medium-High | Low (VPN/proxy) | # Licensing Overview Source: https://docs.rmz.gg/licensing/overview Protect your software with license key verification. Customers purchase a key from your store and your software validates it with a single API call. RMZ Software Licensing lets you sell license keys for your software products. Your customers purchase a key from your store, and your software verifies it against the RMZ API on startup or periodically. ## How It Works In your store dashboard, create a product with type **Software License**. Choose a lock type (`none`, `hwid`, or `ip`), set max activations, and add pricing plans. When a customer completes an order, they receive a license key automatically (e.g. `MYAPP-XXXX-XXXX-XXXX-XXXX`). Your application sends a `POST` request to `https://license.rmz.gg/verify` with the product ID, license key, and optionally a hardware ID. The API returns whether the license is valid, along with full details including plan info, expiry, and activation counts. ## Quick Start ### 1. Create a License Product In your store dashboard, go to **Products** and create a new product: | Setting | Description | | ------------------- | -------------------------------------------------------------------------------- | | **Type** | Software License | | **Key prefix** | A short prefix for your keys (e.g. `MYAPP` produces `MYAPP-XXXX-XXXX-XXXX-XXXX`) | | **Lock type** | `none` (key-only), `hwid` (hardware ID), or `ip` (IP address) | | **Max activations** | How many devices can use one key (`0` = unlimited) | | **Plans** | Duration-based pricing tiers (monthly, yearly, lifetime, etc.) | | **E2EE** | Optionally encrypt all API responses with AES-256-GCM | ### 2. Verify in Your Software ```bash theme={null} curl -X POST https://license.rmz.gg/verify \ -H "Content-Type: application/json" \ -d '{ "product_id": 123, "license_key": "MYAPP-XXXX-XXXX-XXXX-XXXX", "hwid": "machine-hardware-id" }' ``` ### 3. Handle the Response ```json theme={null} { "success": true, "data": { "status": "active", "lock_type": "hwid", "product": { "id": 123, "name": "My Script Pro" }, "plan": { "duration": "annually", "duration_label": "سنه", "start_date": "23-03-2026", "end_date": "23-03-2027", "is_active": true }, "expires_at": "2027-03-23T12:00:00.000000Z", "expires_in_days": 365, "activations": { "current": 1, "max": 3, "remaining": 2 }, "metadata": null } } ``` ## Code Examples Full working examples are available for every supported language: Built-in fetch, no dependencies Uses the requests library Built-in cURL, no dependencies FiveM-native HTTP requests All examples are also available on GitHub: [github.com/Rmz-App/rmz-license-examples](https://github.com/Rmz-App/rmz-license-examples) ## Managing Licenses From your store dashboard under **Licenses**: * View all issued licenses with filters and search * Create manual licenses (for testing, gifts, etc.) * Revoke, suspend, or reactivate individual licenses * Reset device activations * View activation history and verification logs * Aggregate stats (active, expired, suspended, revoked) ## License Lifecycle | Event | Effect | | ------------------ | ------------------------------------------------ | | Customer purchases | Key auto-generated with the configured prefix | | Order completed | License activated, expiry set from plan duration | | Order cancelled | All order licenses revoked | | Order refunded | All order licenses revoked | | Expiry date passes | Marked expired automatically (hourly check) | # Plans & Duration Source: https://docs.rmz.gg/licensing/plans-and-duration Duration-based pricing plans for license products, from monthly to decennial, plus lifetime licenses. License products support subscription-style plans with different durations and prices. When a customer purchases a plan, the license key is issued with an expiry date based on the selected duration. ## Duration Keys | Duration Key | Display Label | Days | | -------------- | ------------- | ----- | | `monthly` | شهر | 30 | | `quarterly` | ٣ اشهر | 90 | | `semiAnnual` | ٦ أشهر | 180 | | `annually` | سنه | 365 | | `biennial` | سنتين | 730 | | `quinquennial` | ٥ سنوات | 1,825 | | `decennial` | ١٠ سنوات | 3,650 | You can set a different price for each duration when creating or editing your product. Customers choose their preferred plan at checkout. ## Plan Data in Responses When a license has a plan, the API response includes full plan details: ```json theme={null} { "plan": { "duration": "annually", "duration_label": "سنه", "start_date": "23-03-2026", "end_date": "23-03-2027", "is_active": true }, "expires_at": "2027-03-23T12:00:00.000000Z", "expires_in_days": 365 } ``` | Field | Description | | --------------------- | ------------------------------------------- | | `plan.duration` | The raw duration key (e.g. `annually`) | | `plan.duration_label` | Arabic display label (e.g. `سنه`) | | `plan.start_date` | When the plan started (DD-MM-YYYY) | | `plan.end_date` | When the plan ends (DD-MM-YYYY) | | `plan.is_active` | Whether the plan period is currently active | | `expires_at` | ISO 8601 expiry timestamp | | `expires_in_days` | Days remaining until expiry | ## Lifetime Licenses For products sold as one-time purchases without an expiry, the response shows: ```json theme={null} { "plan": null, "expires_at": null, "expires_in_days": null } ``` Check for `expires_at == null` in your code to determine if a license is lifetime. A lifetime license never expires and will always return `"status": "active"` (unless manually revoked or suspended). ## Expiry Behavior * An hourly background job checks all licenses and marks expired ones automatically. * Once expired, the API returns `LICENSE_EXPIRED` (HTTP 403) for that key. * The `expires_in_days` field lets you show a countdown or warning to your users before their license expires. Licenses are tied to the order. If an order is cancelled or refunded, all associated license keys are revoked regardless of their remaining duration. # Verification Endpoint Source: https://docs.rmz.gg/licensing/verification API reference for the license verification endpoint. Verify keys, auto-activate devices, and retrieve full license details in one call. ## `POST https://license.rmz.gg/verify` Verifies a license key and auto-activates the device in a single call. No separate activation step is needed. ## Request Send a JSON body with the following fields: | Field | Type | Required | Description | | ------------- | ------- | -------- | ---------------------------------------------- | | `product_id` | integer | Yes | Your product ID from the dashboard | | `license_key` | string | Yes | The customer's license key | | `hwid` | string | No | Hardware ID -- required if lock type is `hwid` | ```bash theme={null} curl -X POST https://license.rmz.gg/verify \ -H "Content-Type: application/json" \ -d '{ "product_id": 123, "license_key": "MYAPP-XXXX-XXXX-XXXX-XXXX", "hwid": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4" }' ``` ## Success Response (200) ```json theme={null} { "success": true, "data": { "status": "active", "lock_type": "hwid", "product": { "id": 123, "name": "My Script Pro" }, "plan": { "duration": "annually", "duration_label": "سنه", "start_date": "2026-03-23 12:00:00", "end_date": "2027-03-23 12:00:00", "is_active": true }, "expires_at": "2027-03-23T12:00:00+00:00", "expires_in_days": 365, "activations": { "current": 1, "max": 3, "remaining": 2 }, "metadata": null } } ``` ## Response Field Reference | Field | Type | Description | | ---------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------- | | `success` | boolean | `true` if the license is valid | | `data.status` | string | `active`, `expired`, `suspended`, or `revoked` | | `data.lock_type` | string | `none`, `hwid`, or `ip` | | `data.product.id` | integer | Product ID | | `data.product.name` | string | Product display name | | `data.plan` | object \| null | Subscription plan details (`null` if no plan) | | `data.plan.duration` | string | Raw duration key: `monthly`, `quarterly`, `semiAnnual`, `annually`, `biennial`, `quinquennial`, `decennial` | | `data.plan.duration_label` | string | Arabic display name for the duration | | `data.plan.start_date` | string | Plan start date (`YYYY-MM-DD HH:MM:SS` format) | | `data.plan.end_date` | string | Plan end date (`YYYY-MM-DD HH:MM:SS` format) | | `data.plan.is_active` | boolean | Whether the subscription is currently active | | `data.expires_at` | string \| null | ISO 8601 expiry date (e.g., `2027-03-23T12:00:00+00:00`). `null` means lifetime license | | `data.expires_in_days` | integer \| null | Days remaining until expiry. `null` means lifetime | | `data.activations.current` | integer | Number of devices currently activated | | `data.activations.max` | integer \| null | Maximum allowed activations. `null` means unlimited | | `data.activations.remaining` | integer \| null | Activation slots remaining. `null` means unlimited | | `data.metadata` | object \| null | Custom JSON data from product settings | ## Error Response ```json theme={null} { "success": false, "error": "LICENSE_NOT_FOUND", "message": "License not found" } ``` See [Error Codes](/licensing/error-codes) for the full list of error codes, messages, and their HTTP status codes. When [E2EE](/licensing/e2ee) is enabled on the product, both success and error responses are AES-256-GCM encrypted. The response structure changes to: ```json theme={null} { "encrypted": true, "payload": "base64-encoded-ciphertext", "nonce": "base64-encoded-nonce", "tag": "base64-encoded-tag" } ``` Decrypt the `payload` using AES-256-GCM with your product's encryption key, the `nonce`, and the `tag` to get the original JSON response. # Categories Source: https://docs.rmz.gg/merchant-api/categories Retrieve a paginated list of store categories. # List Categories
GET /categories
Returns a paginated list of categories for the authenticated store. ## Authentication ## Headers | Header | Value | Required | | --------------- | ----------------------- | ----------- | | `Authorization` | `Bearer YOUR_API_TOKEN` | Yes | | `Accept` | `application/json` | Recommended | ## Query Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ------------------------ | | `page` | integer | No | Page number (default: 1) | ## Example Request ```bash cURL theme={null} curl -X GET "https://merchant-api.rmz.gg/shawarma/categories?page=1" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ```javascript JavaScript theme={null} const response = await fetch("https://merchant-api.rmz.gg/shawarma/categories?page=1", { headers: { "Authorization": "Bearer YOUR_API_TOKEN" } }); const categories = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( "https://merchant-api.rmz.gg/shawarma/categories", headers={"Authorization": "Bearer YOUR_API_TOKEN"}, params={"page": 1} ) categories = response.json() ``` ```php PHP theme={null} $ch = curl_init("https://merchant-api.rmz.gg/shawarma/categories?page=1"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer YOUR_API_TOKEN" ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); ``` ## Success Response ```json theme={null} { "message": null, "data": { "current_page": 1, "data": [ { "id": 1, "slug": "software", "title": "Software", "description": "Digital software products", "metadata": null, "is_active": true, "parent_id": null, "store_id": 1, "deleted_at": null, "created_at": "2024-01-01T00:00:00.000000Z", "updated_at": "2024-01-01T00:00:00.000000Z", "sort_index": 1 }, { "id": 2, "slug": "game-keys", "title": "Game Keys", "description": "Video game activation keys", "metadata": null, "is_active": true, "parent_id": null, "store_id": 1, "deleted_at": null, "created_at": "2024-01-02T00:00:00.000000Z", "updated_at": "2024-01-02T00:00:00.000000Z", "sort_index": 2 } ], "first_page_url": "https://merchant-api.rmz.gg/shawarma/categories?page=1", "from": 1, "next_page_url": null, "path": "https://merchant-api.rmz.gg/shawarma/categories", "per_page": 15, "prev_page_url": null, "to": 2 }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` ## Error Responses | Code | Description | | ----- | --------------------------------------- | | `401` | Unauthorized — invalid or missing token | # Create Order Source: https://docs.rmz.gg/merchant-api/orders/create-order Create a new order programmatically via the Merchant API. # Create Order
POST /orders
Creates a new order for a customer. If the order total is greater than zero, a checkout link is returned for payment. If the order is free, it is completed immediately. ## Authentication ## Headers | Header | Value | Required | | --------------- | ----------------------- | -------- | | `Authorization` | `Bearer YOUR_API_TOKEN` | Yes | | `Content-Type` | `application/json` | Yes | ## Request Body | Parameter | Type | Required | Description | | ------------------------------ | -------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `customer` | object | No | Customer identification. Provide either `customer.id` to reference an existing customer, OR the full customer details below to create/find a customer. | | `customer.id` | integer | No | Existing customer ID (use this OR the fields below) | | `customer.country_code` | string | Conditional | Required if `customer.id` is not provided. One of: `966`, `973`, `971`, `974`, `968`, `965` | | `customer.phone` | string | Conditional | Required if `customer.id` is not provided | | `customer.firstName` | string | Conditional | Required if `customer.id` is not provided | | `customer.lastName` | string | Conditional | Required if `customer.id` is not provided | | `customer.email` | string | Conditional | Required if `customer.id` is not provided | | `created_from` | string | Yes | Domain name where the order originates (e.g., `myapp.com`) | | `products` | array | Yes | Array of products to order (min: 1) | | `products[].identifier_type` | string | Yes | Either `id` or `slug` | | `products[].identifier` | string/integer | Yes | Product ID or slug | | `products[].quantity` | integer | Yes | Quantity (min: 1) | | `products[].options` | object | No | Product field/option selections | | `products[].notice` | string | No | Customer note for this item (max: 800 chars) | | `products[].subscription_plan` | integer | No | Subscription variant ID (for subscription products) | | `coupon_code` | string | No | Coupon code to apply | ## Example Request — Existing Customer ```bash cURL theme={null} curl -X POST "https://merchant-api.rmz.gg/shawarma/orders" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "customer": { "id": 12345 }, "created_from": "myapp.com", "products": [ { "identifier_type": "id", "identifier": 101, "quantity": 2 } ], "coupon_code": "SAVE10" }' ``` ```javascript JavaScript theme={null} const response = await fetch("https://merchant-api.rmz.gg/shawarma/orders", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_TOKEN", "Content-Type": "application/json" }, body: JSON.stringify({ customer: { id: 12345 }, created_from: "myapp.com", products: [ { identifier_type: "id", identifier: 101, quantity: 2 } ], coupon_code: "SAVE10" }) }); const data = await response.json(); ``` ```python Python theme={null} import requests response = requests.post( "https://merchant-api.rmz.gg/shawarma/orders", headers={ "Authorization": "Bearer YOUR_API_TOKEN", "Content-Type": "application/json" }, json={ "customer": {"id": 12345}, "created_from": "myapp.com", "products": [ {"identifier_type": "id", "identifier": 101, "quantity": 2} ], "coupon_code": "SAVE10" } ) data = response.json() ``` ```php PHP theme={null} $ch = curl_init("https://merchant-api.rmz.gg/shawarma/orders"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer YOUR_API_TOKEN", "Content-Type: application/json" ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ "customer" => ["id" => 12345], "created_from" => "myapp.com", "products" => [ ["identifier_type" => "id", "identifier" => 101, "quantity" => 2] ], "coupon_code" => "SAVE10" ])); $response = json_decode(curl_exec($ch), true); ``` ## Example Request — New Customer ```bash cURL theme={null} curl -X POST "https://merchant-api.rmz.gg/shawarma/orders" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "customer": { "country_code": "966", "phone": "501234567", "firstName": "Ahmed", "lastName": "Ali", "email": "ahmed@example.com" }, "created_from": "myapp.com", "products": [ { "identifier_type": "slug", "identifier": "free-ebook", "quantity": 1 } ] }' ``` ```javascript JavaScript theme={null} const response = await fetch("https://merchant-api.rmz.gg/shawarma/orders", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_TOKEN", "Content-Type": "application/json" }, body: JSON.stringify({ customer: { country_code: "966", phone: "501234567", firstName: "Ahmed", lastName: "Ali", email: "ahmed@example.com" }, created_from: "myapp.com", products: [ { identifier_type: "slug", identifier: "free-ebook", quantity: 1 } ] }) }); const data = await response.json(); ``` ```python Python theme={null} import requests response = requests.post( "https://merchant-api.rmz.gg/shawarma/orders", headers={ "Authorization": "Bearer YOUR_API_TOKEN", "Content-Type": "application/json" }, json={ "customer": { "country_code": "966", "phone": "501234567", "firstName": "Ahmed", "lastName": "Ali", "email": "ahmed@example.com" }, "created_from": "myapp.com", "products": [ {"identifier_type": "slug", "identifier": "free-ebook", "quantity": 1} ] } ) data = response.json() ``` ```php PHP theme={null} $ch = curl_init("https://merchant-api.rmz.gg/shawarma/orders"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer YOUR_API_TOKEN", "Content-Type: application/json" ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ "customer" => [ "country_code" => "966", "phone" => "501234567", "firstName" => "Ahmed", "lastName" => "Ali", "email" => "ahmed@example.com" ], "created_from" => "myapp.com", "products" => [ ["identifier_type" => "slug", "identifier" => "free-ebook", "quantity" => 1] ] ])); $response = json_decode(curl_exec($ch), true); ``` ## Success Response — Paid Order **HTTP 201 Created** ```json theme={null} { "message": "Checkout link created successfully", "data": { "checkout_id": 123456, "checkout_url": "https://app.rmz.gg/checkout/abc123def456", "amount": 299.99, "status": "pending_payment" }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` Redirect the customer to `checkout_url` to complete payment. ## Success Response — Free Order **HTTP 201 Created** ```json theme={null} { "message": "Order created successfully", "data": { "order_id": 789012, "status": "completed", "amount": 0 }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` Free orders are completed immediately with no checkout step. ## Idempotency Duplicate requests within 5 minutes return the existing checkout or order instead of creating a new one. The response includes `"duplicate": true`: ```json theme={null} { "success": true, "message": "Checkout already exists", "data": { "checkout_id": 123456, "duplicate": true } } ``` ## Error Responses | Code | Description | | ----- | ---------------------------------------------------------------------------- | | `400` | Business logic error (product not found, out of stock, invalid coupon, etc.) | | `401` | Unauthorized — invalid or missing token | | `422` | Validation error — missing or invalid fields | ### Common Error Messages | Message | Cause | | ------------------------------------------------------ | ----------------------------------------------- | | `Product not found: {identifier}` | Product ID or slug does not exist in your store | | `Product unavailable: {name}` | Product is disabled (status 3) | | `Requested quantity not available for product: {name}` | Insufficient stock | | `Minimum quantity is {n} for product: {name}` | Below the minimum order quantity | | `Maximum quantity allowed for subscriptions is 1` | Subscription products cannot have quantity > 1 | | `Invalid coupon code` | Coupon does not exist or is disabled | | `Coupon is no longer valid` | Coupon has reached its maximum usage | | `Failed to create or find customer` | Customer ID not found in your store | # Get Order Source: https://docs.rmz.gg/merchant-api/orders/get-order Retrieve detailed information for a specific order. # Get Order
GET /orders/
Returns full details for a single order, including line items, transaction data, subscription info, and customer details. ## Authentication ## Headers | Header | Value | Required | | --------------- | ----------------------- | ----------- | | `Authorization` | `Bearer YOUR_API_TOKEN` | Yes | | `Accept` | `application/json` | Recommended | ## Path Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ------------ | | `id` | integer | Yes | The order ID | ## Example Request ```bash cURL theme={null} curl -X GET "https://merchant-api.rmz.gg/shawarma/orders/78901" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ```javascript JavaScript theme={null} const response = await fetch("https://merchant-api.rmz.gg/shawarma/orders/78901", { headers: { "Authorization": "Bearer YOUR_API_TOKEN" } }); const order = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( "https://merchant-api.rmz.gg/shawarma/orders/78901", headers={"Authorization": "Bearer YOUR_API_TOKEN"} ) order = response.json() ``` ```php PHP theme={null} $ch = curl_init("https://merchant-api.rmz.gg/shawarma/orders/78901"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer YOUR_API_TOKEN" ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); ``` ## Success Response ```json theme={null} { "message": null, "data": { "id": 78901, "store_id": 1, "customer_id": 123, "coupon_id": null, "total": 299.99, "checkout_id": 55001, "current_status": 4, "seen_at": null, "order_review_notification_sent_at": null, "tax_rate": null, "tax_amount": null, "prices_include_tax": false, "tax_country_code": null, "tax_registration_number": null, "created_at": "2024-01-15T14:30:00.000000Z", "updated_at": "2024-01-15T15:00:00.000000Z", "human_format": { "date_human": "منذ شهرين", "date_normal": "Mon, Jan 15, 2024 2:30 PM" }, "transaction": { "payment_method": "dokanpay", "deserved": 280.00, "id": 5001, "store_balance_credited_at": "2024-01-22T00:00:00.000000Z", "platform_fees": 19.99, "payment_data": null, "payment_id": "ch_abc123", "total": 299.99, "is_refunded": false, "is_hold": false, "is_by_platform": false, "order_id": 78901, "human_format": { "payment.method": "البطائق الإئتمانية", "created_at": "15 يناير 2024، 2:30 م", "created_at_text": "منذ شهرين", "store_balance_scheduled_at_text": "منذ شهر", "store_balance_scheduled_at": "22 يناير 2024، 12:00 ص" } }, "items": [ { "id": 1001, "order_id": 78901, "item_type": "App\\Models\\StoreProduct", "item_id": 101, "quantity": 2, "price": 149.99, "fields": [], "notice": null, "created_at": "2024-01-15T14:30:00.000000Z", "updated_at": "2024-01-15T14:30:00.000000Z", "item": { "id": 101, "name": "Premium Product", "price": 149.99, "type": "product" }, "subscription": null } ], "customer": { "id": 123, "firstName": "Ahmed", "lastName": "Ali", "country_code": "966", "phone": "501234567", "email": "ahmed@example.com" } }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` ## Response Fields | Field | Type | Description | | ---------------------------------------------------------- | ------------ | ----------------------------------------------------------------------------- | | `id` | integer | Order ID | | `store_id` | integer | Store ID | | `customer_id` | integer | Customer ID | | `coupon_id` | integer/null | Coupon ID if a coupon was applied | | `total` | float | Order total amount | | `checkout_id` | integer | Checkout session ID | | `current_status` | integer | Order status code (see [Overview](/merchant-api/overview#order-statuses)) | | `seen_at` | string/null | When the order was seen by the store owner | | `tax_rate` | string/null | Tax rate percentage applied | | `tax_amount` | string/null | Tax amount | | `prices_include_tax` | boolean | Whether prices include tax | | `human_format` | object | Human-readable date information (in Arabic) | | `human_format.date_human` | string | Relative date (e.g., "منذ شهرين") | | `human_format.date_normal` | string | Formatted date string | | `transaction.payment_method` | string | Payment method used (e.g., `dokanpay`, `bank`, `free`) | | `transaction.deserved` | float | Amount deserved by merchant after fees | | `transaction.is_refunded` | boolean | Whether the order was refunded | | `transaction.platform_fees` | float | Platform fees deducted | | `transaction.payment_data` | object/null | Bank transfer data (only for `bank` payment method, `null` for other methods) | | `transaction.payment_id` | string | Payment provider transaction ID | | `transaction.is_hold` | boolean | Whether the transaction is on hold | | `transaction.is_by_platform` | boolean | Whether the transaction was processed via the platform | | `transaction.store_balance_credited_at` | string/null | When the balance was credited to the store | | `transaction.order_id` | integer | The order ID this transaction belongs to | | `transaction.human_format` | object | Human-readable transaction info (in Arabic) | | `transaction.human_format.payment.method` | string | Arabic translation of the payment method | | `transaction.human_format.created_at` | string/null | Formatted creation date | | `transaction.human_format.created_at_text` | string/null | Relative creation date | | `transaction.human_format.store_balance_scheduled_at` | string/null | Formatted balance schedule date | | `transaction.human_format.store_balance_scheduled_at_text` | string/null | Relative balance schedule date | | `items` | array | Line items in the order | | `items[].id` | integer | Order item ID | | `items[].order_id` | integer | Parent order ID | | `items[].item_type` | string | Product model class | | `items[].item_id` | integer | Product ID | | `items[].quantity` | integer | Quantity ordered | | `items[].price` | float | Unit price at time of purchase | | `items[].fields` | array | Selected product options/fields | | `items[].notice` | string/null | Customer note for this item | | `items[].item` | object | Product details (id, name, price, type) | | `items[].subscription` | object/null | Subscription details if applicable | | `customer` | object | Customer information | ## Error Responses | Code | Description | | ----- | --------------------------------------- | | `401` | Unauthorized — invalid or missing token | | `404` | Order not found | # List Orders Source: https://docs.rmz.gg/merchant-api/orders/list-orders Retrieve a paginated list of orders with optional filters. # List Orders
GET /orders
Returns a paginated list of orders for the authenticated store. Includes customer and transaction data. ## Authentication ## Headers | Header | Value | Required | | --------------- | ----------------------- | ----------- | | `Authorization` | `Bearer YOUR_API_TOKEN` | Yes | | `Accept` | `application/json` | Recommended | ## Query Parameters | Parameter | Type | Required | Description | | ---------------- | ------- | -------- | --------------------------------------------------------------------- | | `page` | integer | No | Page number (default: 1) | | `created_from` | date | No | Filter orders created on or after this date (e.g., `2024-01-01`) | | `created_to` | date | No | Filter orders created on or before this date | | `orderBy` | string | No | Sort field: `id`, `created_at`, `updated_at`, `total` (default: `id`) | | `orderDirection` | string | No | Sort direction: `asc` or `desc` (default: `desc`) | ## Example Request ```bash cURL theme={null} curl -X GET "https://merchant-api.rmz.gg/shawarma/orders?page=1&orderBy=created_at&orderDirection=desc" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ```javascript JavaScript theme={null} const params = new URLSearchParams({ page: "1", orderBy: "created_at", orderDirection: "desc" }); const response = await fetch( `https://merchant-api.rmz.gg/shawarma/orders?${params}`, { headers: { "Authorization": "Bearer YOUR_API_TOKEN" } } ); const orders = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( "https://merchant-api.rmz.gg/shawarma/orders", headers={"Authorization": "Bearer YOUR_API_TOKEN"}, params={ "page": 1, "orderBy": "created_at", "orderDirection": "desc" } ) orders = response.json() ``` ```php PHP theme={null} $params = http_build_query([ "page" => 1, "orderBy" => "created_at", "orderDirection" => "desc" ]); $ch = curl_init("https://merchant-api.rmz.gg/shawarma/orders?{$params}"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer YOUR_API_TOKEN" ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); ``` ## Success Response ```json theme={null} { "message": null, "data": { "current_page": 1, "data": [ { "id": 78901, "store_id": 1, "customer_id": 123, "coupon_id": null, "total": 299.99, "checkout_id": 55001, "current_status": 4, "seen_at": null, "order_review_notification_sent_at": null, "tax_rate": null, "tax_amount": null, "prices_include_tax": false, "tax_country_code": null, "tax_registration_number": null, "created_at": "2024-01-15T14:30:00.000000Z", "updated_at": "2024-01-15T15:00:00.000000Z", "human_format": { "date_human": "منذ شهرين", "date_normal": "Mon, Jan 15, 2024 2:30 PM" }, "transaction": { "id": 5001, "total": 299.99, "deserved": 280.00, "payment_method": "dokanpay", "is_refunded": false, "order_id": 78901, "human_format": { "payment.method": "البطائق الإئتمانية", "created_at": null, "created_at_text": null, "store_balance_scheduled_at_text": null, "store_balance_scheduled_at": null } }, "customer": { "id": 123, "firstName": "Ahmed", "lastName": "Ali", "email": "ahmed@example.com", "country_code": "966", "phone": "501234567" } } ], "first_page_url": "https://merchant-api.rmz.gg/shawarma/orders?page=1", "from": 1, "next_page_url": "https://merchant-api.rmz.gg/shawarma/orders?page=2", "path": "https://merchant-api.rmz.gg/shawarma/orders", "per_page": 15, "prev_page_url": null, "to": 15 }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` The `transaction.human_format` values for `created_at` and `store_balance_scheduled_at` appear as `null` in the list endpoint because only specific transaction columns are selected. The `payment.method` field returns the Arabic translation of the payment method. ## Filtering by Date Range ```bash theme={null} curl -X GET "https://merchant-api.rmz.gg/shawarma/orders?created_from=2024-01-01&created_to=2024-01-31" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ## Error Responses | Code | Description | | ----- | -------------------------------------------- | | `401` | Unauthorized — invalid or missing token | | `422` | Validation error — invalid filter parameters | # Update Order Source: https://docs.rmz.gg/merchant-api/orders/update-order Update the status of an existing order. # Update Order
PUT /orders/
Updates an order's status. Use this to mark orders as shipped, delivered, completed, cancelled, or refunded. ## Authentication ## Headers | Header | Value | Required | | --------------- | ----------------------- | -------- | | `Authorization` | `Bearer YOUR_API_TOKEN` | Yes | | `Content-Type` | `application/json` | Yes | ## Path Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ------------ | | `id` | integer | Yes | The order ID | ## Request Body | Parameter | Type | Required | Description | | --------- | ------- | -------- | --------------------- | | `status` | integer | No | New status code (1-6) | ### Status Codes | Code | Status | | ---- | ---------------- | | `1` | Awaiting Payment | | `2` | Under Review | | `3` | In Progress | | `4` | Completed | | `5` | Cancelled | | `6` | Refunded | ## Example Request ```bash cURL theme={null} curl -X PUT "https://merchant-api.rmz.gg/shawarma/orders/78901" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"status": 3}' ``` ```javascript JavaScript theme={null} const response = await fetch("https://merchant-api.rmz.gg/shawarma/orders/78901", { method: "PUT", headers: { "Authorization": "Bearer YOUR_API_TOKEN", "Content-Type": "application/json" }, body: JSON.stringify({ status: 3 }) }); const data = await response.json(); ``` ```python Python theme={null} import requests response = requests.put( "https://merchant-api.rmz.gg/shawarma/orders/78901", headers={ "Authorization": "Bearer YOUR_API_TOKEN", "Content-Type": "application/json" }, json={"status": 3} ) data = response.json() ``` ```php PHP theme={null} $ch = curl_init("https://merchant-api.rmz.gg/shawarma/orders/78901"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer YOUR_API_TOKEN", "Content-Type: application/json" ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT"); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["status" => 3])); $response = json_decode(curl_exec($ch), true); ``` ## Success Response ```json theme={null} { "message": "Order Has Been Updates", "data": null, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` ## Error Responses | Code | Description | | ----- | --------------------------------------- | | `400` | Invalid status transition | | `401` | Unauthorized — invalid or missing token | | `404` | Order not found | | `422` | Validation error — invalid status value | # Merchant API Overview Source: https://docs.rmz.gg/merchant-api/overview Programmatically manage your RMZ store with the Merchant API. # Merchant API The Merchant API (codenamed **Shawarma**) lets you manage your RMZ store programmatically. Use it to fetch store data, manage orders, list products, and pull statistics into external systems. ## Base URL ``` https://merchant-api.rmz.gg/shawarma ``` ## Authentication ## Rate Limiting ## Response Format All successful responses follow this structure: ```json theme={null} { "message": "Success message or null", "data": { }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` Error responses include `error: true`: ```json theme={null} { "message": "Error description", "data": null, "api": "rmz.shawarma", "timestamp": 1699999999, "error": true } ``` ## Pagination ## Idempotency The `POST /orders` endpoint implements idempotency to prevent duplicate orders. Duplicate requests within 5 minutes return the existing checkout or order instead of creating a new one. Idempotency is based on a hash of the request content. ## Available Endpoints | Method | Endpoint | Description | | ------ | --------------------- | ------------------------------ | | `GET` | `/store` | Get store information | | `GET` | `/store/statics` | Get store statistics | | `GET` | `/orders` | List orders (paginated) | | `POST` | `/orders` | Create a new order | | `GET` | `/orders/{id}` | Get order details | | `PUT` | `/orders/{id}` | Update order status | | `GET` | `/products` | List products (paginated) | | `GET` | `/products/{id}` | Get product details | | `GET` | `/categories` | List categories (paginated) | | `GET` | `/subscriptions` | List subscriptions (paginated) | | `GET` | `/subscriptions/{id}` | Get subscription details | ## Order Statuses | Status Code | Description | Arabic | | ----------- | ---------------- | ------------- | | `1` | Awaiting Payment | بإنتظار الدفع | | `2` | Under Review | قيد المراجعة | | `3` | In Progress | قيد التنفيذ | | `4` | Completed | مكتمل | | `5` | Cancelled | ملغي | | `6` | Refunded | مسترجع | ## Product Types | Type | Description | | -------------- | ---------------------- | | `product` | Digital product | | `code` | Digital codes/keys | | `service` | Service-based product | | `subscription` | Recurring subscription | # Create Portal Session Source: https://docs.rmz.gg/merchant-api/portal-sessions/create-portal-session Generate a customer billing portal session URL for subscription self-service. # Create Portal Session
POST /portal-sessions
Creates a billing portal session for a customer. The portal allows customers to view their subscriptions, update payment methods, cancel subscriptions, and change plans — all without requiring you to build a subscription management UI. ## Authentication ## Headers | Header | Value | Required | | --------------- | ----------------------- | ----------- | | `Authorization` | `Bearer YOUR_API_TOKEN` | Yes | | `Content-Type` | `application/json` | Yes | | `Accept` | `application/json` | Recommended | ## Request Body | Parameter | Type | Required | Description | | ----------------------- | ------- | ----------- | ------------------------------------------------------------------------------------------------------------------------- | | `customer.id` | integer | Conditional | Existing customer ID. If provided, other customer fields are ignored. | | `customer.country_code` | string | Conditional | Customer's country dial code (e.g. `"966"` for Saudi Arabia, `"971"` for UAE). Required if `customer.id` is not provided. | | `customer.phone` | string | Conditional | Customer's phone number, 5-15 digits (e.g. `"512345678"`). Required if `customer.id` is not provided. | | `return_url` | string | No | URL to redirect to when the customer exits the portal | | `expires_in` | integer | No | Session lifetime in seconds (300-86400, default: 3600) | You must provide either `customer.id` (to reference an existing customer) **or** `customer.country_code` + `customer.phone` (to look up the customer by phone). ## Example Request (By Phone) ```bash cURL theme={null} curl -X POST "https://merchant-api.rmz.gg/shawarma/portal-sessions" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "customer": { "country_code": "966", "phone": "512345678" }, "return_url": "https://yourapp.com/account" }' ``` ```javascript JavaScript theme={null} const response = await fetch( "https://merchant-api.rmz.gg/shawarma/portal-sessions", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_TOKEN", "Content-Type": "application/json" }, body: JSON.stringify({ customer: { country_code: "966", phone: "512345678" }, return_url: "https://yourapp.com/account" }) } ); const session = await response.json(); // Redirect customer to session.data.url ``` ```python Python theme={null} import requests response = requests.post( "https://merchant-api.rmz.gg/shawarma/portal-sessions", headers={ "Authorization": "Bearer YOUR_API_TOKEN", "Content-Type": "application/json" }, json={ "customer": { "country_code": "966", "phone": "512345678" }, "return_url": "https://yourapp.com/account" } ) session = response.json() # Redirect customer to session["data"]["url"] ``` ```php PHP theme={null} $ch = curl_init("https://merchant-api.rmz.gg/shawarma/portal-sessions"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer YOUR_API_TOKEN", "Content-Type: application/json" ]); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ "customer" => [ "country_code" => "966", "phone" => "512345678" ], "return_url" => "https://yourapp.com/account" ])); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); // Redirect customer to $response["data"]["url"] ``` ## Example Request (By Customer ID) ```bash cURL theme={null} curl -X POST "https://merchant-api.rmz.gg/shawarma/portal-sessions" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "customer": { "id": 4501 }, "return_url": "https://yourapp.com/account" }' ``` ```javascript JavaScript theme={null} const response = await fetch( "https://merchant-api.rmz.gg/shawarma/portal-sessions", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_TOKEN", "Content-Type": "application/json" }, body: JSON.stringify({ customer: { id: 4501 }, return_url: "https://yourapp.com/account" }) } ); const session = await response.json(); // Redirect customer to session.data.url ``` ```python Python theme={null} import requests response = requests.post( "https://merchant-api.rmz.gg/shawarma/portal-sessions", headers={ "Authorization": "Bearer YOUR_API_TOKEN", "Content-Type": "application/json" }, json={ "customer": {"id": 4501}, "return_url": "https://yourapp.com/account" } ) session = response.json() # Redirect customer to session["data"]["url"] ``` ```php PHP theme={null} $ch = curl_init("https://merchant-api.rmz.gg/shawarma/portal-sessions"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer YOUR_API_TOKEN", "Content-Type: application/json" ]); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ "customer" => ["id" => 4501], "return_url" => "https://yourapp.com/account" ])); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); // Redirect customer to $response["data"]["url"] ``` ## Success Response ```json theme={null} { "message": "Portal session created successfully", "data": { "url": "https://billing.rmz.gg/s/aB3xK9mP...", "expires_at": "2025-06-01T01:00:00.000000Z" }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` ## Response Fields | Field | Type | Description | | ------------ | ------ | -------------------------------------------------------------------------------------------- | | `url` | string | URL to redirect the customer to (`https://billing.rmz.gg/s/{token}`) | | `expires_at` | string | ISO 8601 timestamp when the session expires (default: 1 hour, configurable via `expires_in`) | ## Portal Capabilities The customer billing portal allows customers to: | Capability | Description | | ------------------------- | -------------------------------------------------------- | | **View subscriptions** | See all active, past due, and canceled subscriptions | | **Cancel subscription** | Cancel at end of period or immediately | | **Change plan** | Upgrade or downgrade to a different subscription variant | | **Update payment method** | Add or change the saved card for auto-renewal | | **View invoices** | See payment history and invoice details | ## Authentication Flow The portal uses OTP (one-time password) verification to authenticate the customer: 1. Your server creates a portal session via this endpoint 2. Redirect the customer to the `url` 3. The customer verifies their identity via OTP sent to their phone 4. After verification, the customer can manage their subscriptions 5. When done, the customer is redirected to your `return_url` Portal session URLs are single-use and expire based on the `expires_in` parameter (default: 1 hour). Generate a new session each time the customer needs to access the portal. The portal is fully hosted by RMZ and styled to match your store theme. You do not need to build any subscription management UI on your end. ## Error Responses | Code | Description | | ----- | ------------------------------------------------ | | `401` | Unauthorized — invalid or missing token | | `404` | Customer not found | | `422` | Validation error — missing or invalid parameters | # Create Product Source: https://docs.rmz.gg/merchant-api/products/create-product Create a product of any type — digital codes, files, subscriptions, licenses, or courses. # Create Product
POST /products
Creates a product of any type (`product`, `code`, `service`, `subscription`, `license`, `course`). The API accepts the full product model — the same fields the dashboard uses — including media, custom fields, variants, course content, discounts, metadata, and SEO. ## Authentication ## Headers | Header | Value | Required | | --------------- | ----------------------- | ----------- | | `Authorization` | `Bearer YOUR_API_TOKEN` | Yes | | `Accept` | `application/json` | Recommended | | `Content-Type` | `application/json` | Yes | ## Body Parameters ### Core | Parameter | Type | Required | Description | | ----------------- | ------- | -------- | ---------------------------------------------------------------------------------- | | `name` | string | Yes | Product name | | `type` | string | Yes | One of `product`, `code`, `service`, `subscription`, `license`, `course` | | `price` | number | Yes | Selling price | | `cost_price` | number | Yes | Your cost price | | `slug` | string | No | URL slug — auto-generated from `name` if omitted. Must be unique within your store | | `description` | string | No | Product description. HTML allowed (sanitized server-side) | | `marketing_title` | string | No | Optional marketing headline | | `activation_info` | string | No | Post-purchase activation instructions. HTML allowed (sanitized) | | `status` | integer | No | `1` = active | | `show_reviews` | boolean | No | Whether to show reviews on the product page | | `is_noticeable` | boolean | No | Highlight/feature flag | ### Pricing & inventory | Parameter | Type | Required | Description | | -------------------- | ------- | -------- | --------------------------------- | | `discount_price` | number | No | Discounted price | | `discount_expiry` | date | No | Discount expiry, `YYYY-MM-DD` | | `stock` | integer | No | Stock quantity (non-`code` types) | | `min_qty` | number | No | Minimum order quantity | | `max_purchase_count` | integer | No | Max units one customer can buy | ### Media Upload files first via [Upload Product Media](/merchant-api/products/upload-product-media) to get a media `id`, then reference it here. | Parameter | Type | Required | Description | | ------------------------ | ------- | -------- | ----------------------------------------------------------------------------------------------- | | `image.file.response.id` | integer | Yes | Main product image media id | | `image_ulid` | string | No | Optional client-side reference | | `product_files` | array | No | Digital files (`product`/`license` types): `[{ "file": { "response": { "id": } } }]` | | `extra_images` | array | No | Additional gallery images: `[{ "file": { "response": { "id": } } }]` | Media you reference must have been uploaded by your store. Unknown or foreign media ids return `404`. ### Categories, benefits & custom fields | Parameter | Type | Required | Description | | ------------ | ----- | -------- | --------------------------------------------- | | `categories` | array | No | Array of category ids belonging to your store | | `benefits` | array | No | Array of benefit ids | | `fields` | array | No | Custom order fields — see shape below | ```json theme={null} "fields": [ { "type": "select", "name": "Region", "placeholder": "Choose a region", "required": true, "options": [ { "name": "EU", "price": 0 }, { "name": "US", "price": 5 } ] } ] ``` ### Digital codes (`type: code`) | Parameter | Type | Required | Description | | --------- | ----- | ---------- | ----------------------------------------------- | | `codes` | array | For `code` | Codes added to inventory: `[{ "code": "..." }]` | ### Subscription / License variants (`type: subscription` or `license`) | Parameter | Type | Required | Description | | ---------------------- | ----- | ---------------------------- | ------------------------------- | | `subscriptionVariants` | array | For `subscription`/`license` | Plan variants — see shape below | ```json theme={null} "subscriptionVariants": [ { "duration": 30, "price": 19.99, "badge": "Most popular", "features": [ { "name": "Priority support", "description": "24/7" } ] } ] ``` ### License configuration (`type: license`) | Parameter | Type | Description | | -------------------------------- | ------- | --------------------------------------------------------------------------- | | `license_config.lock_type` | string | `none`, `hwid`, or `ip` | | `license_config.prefix` | string | Up to 8 alphanumeric chars | | `license_config.expiry_type` | string | `lifetime` or `timed` | | `license_config.expiry_days` | integer | 1–3650 (for `timed`) | | `license_config.max_activations` | integer | 0–100 | | `license_config.e2ee` | boolean | Enables end-to-end encryption; an encryption key is generated automatically | ### Subscription configuration (`type: subscription`) | Parameter | Type | Description | | -------------------- | ------- | ------------------------------ | | `trial_days` | integer | 0–90 | | `cancel_behavior` | string | `end_of_period` or `immediate` | | `auto_renew_default` | boolean | Default auto-renew state | | `grace_period_days` | integer | 0–30 | ### Course content (`type: course`) ```json theme={null} "course": { "instructor": "Jane Doe", "level": "beginner", "sections": [ { "title": "Getting started", "description": "Intro", "sort_index": 0, "modules": [ { "title": "Welcome", "type": "media", "video_uri": "https://...", "sort_index": 0 }, { "title": "Notes", "type": "text", "content": "

...

", "sort_index": 1 } ] } ] } ``` | Field | Type | Description | | --------------------------------------- | ------ | ----------------------------------- | | `course.instructor` | string | Instructor name | | `course.level` | string | Course level | | `course.sections[].title` | string | Section title (required for course) | | `course.sections[].modules[].type` | string | `media` or `text` | | `course.sections[].modules[].video_uri` | string | Media URL (for `media` modules) | | `course.sections[].modules[].content` | string | HTML content (for `text` modules) | ### Display metadata & SEO | Parameter | Type | Description | | ------------------------------------------ | ------------ | ---------------------------- | | `metadata.display.show_discount_countdown` | boolean | Show a discount countdown | | `metadata.display.show_discount_savings` | boolean | Show savings amount | | `metadata.display.show_monthly_breakdown` | boolean | Show monthly price breakdown | | `seo.meta_title` | string | ≤ 70 chars | | `seo.meta_description` | string | ≤ 320 chars | | `seo.meta_keywords` | string | ≤ 255 chars | | `seo.og_title` | string | ≤ 70 chars | | `seo.og_description` | string | ≤ 320 chars | | `seo.canonical_url` | string (url) | ≤ 500 chars | | `seo.robots` | string | ≤ 50 chars | | `seo.extra_meta` | array | Additional meta entries | ## Example Requests ```bash Code product (cURL) theme={null} curl -X POST "https://merchant-api.rmz.gg/shawarma/products" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Premium Key", "type": "code", "price": 49.99, "cost_price": 20, "image": { "file": { "response": { "id": 9001 } } }, "categories": [12], "codes": [ { "code": "KEY-AAA-111" }, { "code": "KEY-BBB-222" } ] }' ``` ```json Subscription (body) theme={null} { "name": "Pro Plan", "type": "subscription", "price": 19.99, "cost_price": 0, "image": { "file": { "response": { "id": 9001 } } }, "trial_days": 7, "cancel_behavior": "end_of_period", "auto_renew_default": true, "subscriptionVariants": [ { "duration": 30, "price": 19.99, "badge": "Monthly", "features": [{ "name": "Priority support" }] }, { "duration": 365, "price": 199, "badge": "Yearly", "features": [] } ], "metadata": { "display": { "show_monthly_breakdown": true } } } ``` ```json License (body) theme={null} { "name": "App License", "type": "license", "price": 99, "cost_price": 10, "image": { "file": { "response": { "id": 9001 } } }, "license_config": { "lock_type": "hwid", "expiry_type": "timed", "expiry_days": 365, "max_activations": 3, "e2ee": true }, "subscriptionVariants": [ { "duration": 365, "price": 99, "features": [] } ], "product_files": [ { "file": { "response": { "id": 9002 } } } ] } ``` ```json Course (body) theme={null} { "name": "Mastering X", "type": "course", "price": 49, "cost_price": 0, "image": { "file": { "response": { "id": 9001 } } }, "course": { "instructor": "Jane Doe", "level": "beginner", "sections": [ { "title": "Intro", "modules": [ { "title": "Welcome", "type": "media", "video_uri": "https://..." } ] } ] } } ``` ## Success Response `201 Created` ```json theme={null} { "message": "تم إنشاء المنتج بنجاح", "data": { "id": 103, "name": "Premium Key", "slug": "premium-key", "type": "code", "price": 49.99, "codes": [ { "id": 5001, "code": "KEY-AAA-111" }, { "id": 5002, "code": "KEY-BBB-222" } ], "image": { "id": 9001 }, "categories": [ { "id": 12 } ], "subscriptionVariants": [] }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` ## Error Responses | Code | Description | | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `401` | Unauthorized — invalid or missing token | | `403` | Plan does not include the API feature, or `license` type requires a plan with license integration | | `404` | A referenced media id (image / product\_files / extra\_images) does not belong to your store | | `422` | Validation error (missing `name`/`price`/`type`, invalid `type`, duplicate `slug`, category not in your store, etc.). The `data` object contains field errors. | # Get Product Source: https://docs.rmz.gg/merchant-api/products/get-product Retrieve detailed information for a specific product. # Get Product
GET /products/
Returns full details for a single product, including images, digital codes, subscription variants, and categories. ## Authentication ## Headers | Header | Value | Required | | --------------- | ----------------------- | ----------- | | `Authorization` | `Bearer YOUR_API_TOKEN` | Yes | | `Accept` | `application/json` | Recommended | ## Path Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | -------------- | | `id` | integer | Yes | The product ID | ## Example Request ```bash cURL theme={null} curl -X GET "https://merchant-api.rmz.gg/shawarma/products/101" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ```javascript JavaScript theme={null} const response = await fetch("https://merchant-api.rmz.gg/shawarma/products/101", { headers: { "Authorization": "Bearer YOUR_API_TOKEN" } }); const product = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( "https://merchant-api.rmz.gg/shawarma/products/101", headers={"Authorization": "Bearer YOUR_API_TOKEN"} ) product = response.json() ``` ```php PHP theme={null} $ch = curl_init("https://merchant-api.rmz.gg/shawarma/products/101"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer YOUR_API_TOKEN" ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); ``` ## Success Response ```json theme={null} { "message": null, "data": { "id": 101, "store_id": 1, "name": "Premium License", "slug": "premium-license", "type": "code", "price": 49.99, "discount_price": null, "discount_expiry": null, "description": "Premium software license key...", "status": 1, "min_qty": 1, "max_purchase_count": null, "purchase_count": 15, "fields": [], "is_noticeable": false, "license_config": null, "metadata": null, "deleted_at": null, "created_at": "2024-01-01T00:00:00.000000Z", "updated_at": "2024-01-15T00:00:00.000000Z", "actual_price": 49.99, "current_stock": 25, "is_new": false, "is_discounted": false, "is_discount_expired": false, "show_discount_countdown": false, "show_discount_savings": false, "discount_savings_amount": 0, "codes": [ { "id": 301, "code": "XXXX-XXXX-XXXX", "order_product_id": null, "product_id": 101, "store_id": 1, "deleted_at": null, "created_at": "2024-01-01T00:00:00.000000Z", "updated_at": "2024-01-01T00:00:00.000000Z" } ], "image": { "id": 201, "model_type": "App\\Models\\StoreProduct", "model_id": 101, "type": "main_product_image", "file_name": "product-image.png", "path": "products/images", "metadata": null, "deleted_at": null, "created_at": "2024-01-01T00:00:00.000000Z", "updated_at": "2024-01-01T00:00:00.000000Z", "full_link": "https://cdn.rmz.gg/products/images/product-image.png" }, "subscription_variants": [], "categories": [ { "id": 1, "slug": "software", "title": "Software", "description": null, "metadata": null, "is_active": true, "parent_id": null, "store_id": 1, "deleted_at": null, "created_at": "2024-01-01T00:00:00.000000Z", "updated_at": "2024-01-01T00:00:00.000000Z", "sort_index": 1, "pivot": { "product_id": 101, "category_id": 1 } } ] }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` ## Response Fields | Field | Type | Description | | -------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------- | | `id` | integer | Product ID | | `store_id` | integer | Store ID | | `name` | string | Product name | | `slug` | string | URL-friendly identifier | | `type` | string | Product type: `product`, `code`, `service`, `subscription`, `course`, `license` | | `price` | float | Display price | | `discount_price` | float/null | Discounted price if a discount is active | | `discount_expiry` | string/null | Discount expiry date | | `description` | string | Product description | | `status` | integer | Product status (1 = active, 3 = disabled) | | `min_qty` | integer | Minimum order quantity | | `max_purchase_count` | integer/null | Maximum total purchases allowed (`null` = unlimited) | | `purchase_count` | integer | Current total purchase count | | `fields` | array | Custom fields/options configured for the product | | `is_noticeable` | boolean | Whether the product accepts customer notes | | `license_config` | object/null | License configuration (for `license` type products) | | `metadata` | object/null | Product metadata including display settings | | `actual_price` | float | Computed selling price (discount price if active, otherwise regular price) | | `current_stock` | integer | Current available stock | | `is_new` | boolean | Whether the product was created in the last 12 hours | | `is_discounted` | boolean | Whether an active discount is currently applied | | `is_discount_expired` | boolean | Whether the discount has expired | | `show_discount_countdown` | boolean | Whether to show a discount countdown timer | | `show_discount_savings` | boolean | Whether to show discount savings amount | | `discount_savings_amount` | float | Amount saved with discount (0 if no active discount) | | `image` | object/null | Product image with `full_link` containing the full CDN URL | | `image.full_link` | string | Full CDN URL for the image | | `codes` | array | Available (unsold) digital codes (for `code` type products). Only codes where `order_product_id` is `null` are returned. | | `subscription_variants` | array | Subscription plans (for `subscription` type products). Each variant includes a `durationText` field with the Arabic duration label. | | `subscription_variants[].durationText` | string | Arabic label for the duration (e.g., "شهر", "سنه") | | `categories` | array | Assigned categories with pivot data | The `codes` array only includes unsold codes (where `order_product_id` is null). For security, ensure you do not expose this data to end users in client-side applications. ## Error Responses | Code | Description | | ----- | --------------------------------------- | | `401` | Unauthorized — invalid or missing token | | `404` | Product not found | # List Products Source: https://docs.rmz.gg/merchant-api/products/list-products Retrieve a paginated list of products in your store. # List Products
GET /products
Returns a paginated list of products with basic information (ID, name, slug, type, and price). ## Authentication ## Headers | Header | Value | Required | | --------------- | ----------------------- | ----------- | | `Authorization` | `Bearer YOUR_API_TOKEN` | Yes | | `Accept` | `application/json` | Recommended | ## Query Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ------------------------ | | `page` | integer | No | Page number (default: 1) | ## Example Request ```bash cURL theme={null} curl -X GET "https://merchant-api.rmz.gg/shawarma/products?page=1" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ```javascript JavaScript theme={null} const response = await fetch("https://merchant-api.rmz.gg/shawarma/products?page=1", { headers: { "Authorization": "Bearer YOUR_API_TOKEN" } }); const products = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( "https://merchant-api.rmz.gg/shawarma/products", headers={"Authorization": "Bearer YOUR_API_TOKEN"}, params={"page": 1} ) products = response.json() ``` ```php PHP theme={null} $ch = curl_init("https://merchant-api.rmz.gg/shawarma/products?page=1"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer YOUR_API_TOKEN" ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); ``` ## Success Response ```json theme={null} { "message": null, "data": { "current_page": 1, "data": [ { "id": 101, "name": "Premium License", "slug": "premium-license", "type": "code", "price": 49.99, "actual_price": 49.99, "current_stock": 25, "is_new": false, "is_discounted": false, "is_discount_expired": false, "show_discount_countdown": false, "show_discount_savings": false, "discount_savings_amount": 0 }, { "id": 102, "name": "Monthly Subscription", "slug": "monthly-subscription", "type": "subscription", "price": 19.99, "actual_price": 19.99, "current_stock": 10000, "is_new": false, "is_discounted": false, "is_discount_expired": false, "show_discount_countdown": false, "show_discount_savings": false, "discount_savings_amount": 0 } ], "first_page_url": "https://merchant-api.rmz.gg/shawarma/products?page=1", "from": 1, "next_page_url": "https://merchant-api.rmz.gg/shawarma/products?page=2", "path": "https://merchant-api.rmz.gg/shawarma/products", "per_page": 15, "prev_page_url": null, "to": 15 }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` Only the selected fields (`id`, `name`, `slug`, `type`, `price`) are returned from the database, but the model's computed attributes (`actual_price`, `current_stock`, `is_new`, `is_discounted`, etc.) are always appended. Use the [Get Product](/merchant-api/products/get-product) endpoint for the full product object. ## Response Fields | Field | Type | Description | | ------------------------- | ------- | ------------------------------------------------------------------------------- | | `id` | integer | Product ID | | `name` | string | Product name | | `slug` | string | URL-friendly product identifier | | `type` | string | Product type: `product`, `code`, `service`, `subscription`, `course`, `license` | | `price` | float | Product price | | `actual_price` | float | Computed selling price (discount price if active, otherwise regular price) | | `current_stock` | integer | Current available stock | | `is_new` | boolean | Whether the product was created in the last 12 hours | | `is_discounted` | boolean | Whether an active discount is currently applied | | `is_discount_expired` | boolean | Whether the discount has expired | | `show_discount_countdown` | boolean | Whether to show a discount countdown timer | | `show_discount_savings` | boolean | Whether to show discount savings amount | | `discount_savings_amount` | float | Amount saved with discount (0 if no active discount) | ## Error Responses | Code | Description | | ----- | --------------------------------------- | | `401` | Unauthorized — invalid or missing token | # Update Product Source: https://docs.rmz.gg/merchant-api/products/update-product Update any field of a product — codes, files, variants, course content, pricing, SEO. # Update Product
PUT /products/
Updates a product you own. Accepts the **same full field set** as [Create Product](/merchant-api/products/create-product); all fields are optional except `slug`. Only the fields you send are changed — except where a field is a managed collection (see [Sync & replace semantics](#sync--replace-semantics)). ## Authentication ## Headers | Header | Value | Required | | --------------- | ----------------------- | ----------- | | `Authorization` | `Bearer YOUR_API_TOKEN` | Yes | | `Accept` | `application/json` | Recommended | | `Content-Type` | `application/json` | Yes | ## Path Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | -------------------------------------- | | `id` | integer | Yes | Product id (must belong to your store) | ## Body Parameters All fields from [Create Product](/merchant-api/products/create-product) are accepted and **optional except `slug`**: * **Core:** `name`, `slug`\* (must remain unique in your store), `type`, `description`, `marketing_title`, `activation_info`, `status`, `show_reviews`, `is_noticeable` * **Pricing/inventory:** `price`, `cost_price`, `discount_price`, `discount_expiry`, `stock`, `min_qty`, `max_purchase_count` * **Media:** `image.file.response.id` (omit to keep current), `product_files[]`, `extra_images[]` * **Relations:** `categories[]`, `benefits[]`, `fields[]` * **Per type:** `codes[]` (code), `subscriptionVariants[]` (+ `license_config` / subscription config), `course` (sections/modules) * **Display/SEO:** `metadata.display.*`, `seo.*` Refer to the [create reference](/merchant-api/products/create-product#body-parameters) for the exact shape of each object. ## Sync & replace semantics | Field | Behavior on update | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `codes` | **Replaces** the live (unsold) code set. To add, send the existing codes plus new ones. Sold codes are preserved. | | `subscriptionVariants` | **Synced by `id`**: include `id` to update a variant, omit `id` to create, omit a variant entirely to delete it. A `subscription`/`license` must keep **at least one** variant (an empty array is rejected). | | `subscriptionVariants[].features` | Synced by `id` within each variant. | | `course.sections` / `modules` | Synced by `id`; omitted sections/modules are deleted. | | `product_files` / `extra_images` | Synced; ids omitted from the list are detached/removed from the product. | | `image` | Omit to keep the current image; provide a new media id to replace it. | | `benefits` / `categories` | Replace the full set you send. | ## Example Requests ```bash Replace codes (cURL) theme={null} curl -X PUT "https://merchant-api.rmz.gg/shawarma/products/103" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Premium Key (v2)", "slug": "premium-key", "type": "code", "price": 59.99, "codes": [ { "code": "KEY-AAA-111" }, { "code": "KEY-CCC-333" } ] }' ``` ```json Update subscription variants (body) theme={null} { "name": "Pro Plan", "slug": "pro-plan", "type": "subscription", "price": 19.99, "subscriptionVariants": [ { "id": 5850, "duration": 30, "price": 24.99, "features": [] }, { "duration": 365, "price": 199, "badge": "Yearly", "features": [{ "name": "2 months free" }] } ] } ``` ```json Update discount + SEO + gallery (body) theme={null} { "name": "App License", "slug": "app-license", "type": "license", "discount_price": 79, "extra_images": [ { "file": { "response": { "id": 9101 } } } ], "seo": { "meta_title": "App License", "meta_description": "Buy a license" } } ``` ## Success Response `200 OK` ```json theme={null} { "message": "تم تحديث المنتج بنجاح", "data": { "id": 103, "name": "Premium Key (v2)", "slug": "premium-key", "type": "code", "price": 59.99, "codes": [ { "id": 5001, "code": "KEY-AAA-111" }, { "id": 5003, "code": "KEY-CCC-333" } ] }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` ## Error Responses | Code | Description | | ----- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `401` | Unauthorized — invalid or missing token | | `403` | Plan does not include the API feature, or `license` type requires a plan with license integration | | `404` | Product not found in your store, or a referenced media id is not yours | | `422` | Validation error (e.g. duplicate `slug`, empty `subscriptionVariants` for a subscription/license). The `data` object contains field errors. | # Upload Product Media Source: https://docs.rmz.gg/merchant-api/products/upload-product-media Upload a product image or digital file and get a media id to attach to a product. # Upload Product Media
POST /products/upload
Uploads a file (product image, extra image, or digital file) and returns a media `id`. Pass that `id` to [Create Product](/merchant-api/products/create-product) / [Update Product](/merchant-api/products/update-product) as `image.file.response.id` (or inside `product_files[]` / `extra_images[]`). ## Authentication ## Headers | Header | Value | Required | | --------------- | ----------------------- | -------- | | `Authorization` | `Bearer YOUR_API_TOKEN` | Yes | | `Content-Type` | `multipart/form-data` | Yes | ## Query Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ---------------------------------------------------------------- | | `type` | string | No | `image` (main, default), `extra_image`, or `file` (digital file) | ## Body (multipart/form-data) | Field | Type | Required | Description | | ------ | ---- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `file` | file | Yes | For `image`/`extra_image`: jpg, jpeg, png, gif (max 10 MB). For `file`: jpg, mp4, jpeg, png, gif, pdf, zip, docx, txt (max \~117 MB) | ## Example Request ```bash cURL theme={null} curl -X POST "https://merchant-api.rmz.gg/shawarma/products/upload?type=image" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -F "file=@/path/to/product.jpg" ``` ```javascript JavaScript theme={null} const form = new FormData(); form.append("file", fileInput.files[0]); const res = await fetch("https://merchant-api.rmz.gg/shawarma/products/upload?type=image", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_TOKEN" }, body: form }); const { data } = await res.json(); const mediaId = data.id; // pass as image.file.response.id ``` ## Success Response `200 OK` ```json theme={null} { "message": null, "data": { "id": 9001, "type": "main_product_image", "name": "product.jpg", "file_name": "a1b2c3....jpg", "path": "products/images/", "mime_type": "image/jpeg", "size": 48213 }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` ## Where to use the returned id Map each upload `type` to the create/update field that references the returned `data.id`: | Upload `type` | Use in create/update as | | ------------- | ----------------------------------------------------------------------------- | | `image` | `image.file.response.id` (main product image) | | `extra_image` | `extra_images[].file.response.id` (gallery) | | `file` | `product_files[].file.response.id` (digital file — `product`/`license` types) | ```json theme={null} { "image": { "file": { "response": { "id": 9001 } } }, "extra_images": [ { "file": { "response": { "id": 9101 } } } ], "product_files": [ { "file": { "response": { "id": 9002 } } } ] } ``` ## Error Responses | Code | Description | | ----- | --------------------------------------- | | `401` | Unauthorized — invalid or missing token | | `403` | Plan does not include the API feature | | `422` | Invalid file type or file too large | # Store Source: https://docs.rmz.gg/merchant-api/store Retrieve store information and statistics. # Store Retrieve your store's profile information and aggregate statistics. *** ## Get Store Information
GET /store
Returns the authenticated store's profile data. ### Authentication ### Headers | Header | Value | Required | | --------------- | ----------------------- | ----------- | | `Authorization` | `Bearer YOUR_API_TOKEN` | Yes | | `Accept` | `application/json` | Recommended | ### Example Request ```bash cURL theme={null} curl -X GET "https://merchant-api.rmz.gg/shawarma/store" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Accept: application/json" ``` ```javascript JavaScript theme={null} const response = await fetch("https://merchant-api.rmz.gg/shawarma/store", { headers: { "Authorization": "Bearer YOUR_API_TOKEN", "Accept": "application/json" } }); const data = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( "https://merchant-api.rmz.gg/shawarma/store", headers={"Authorization": "Bearer YOUR_API_TOKEN"} ) data = response.json() ``` ```php PHP theme={null} $ch = curl_init("https://merchant-api.rmz.gg/shawarma/store"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer YOUR_API_TOKEN", "Accept: application/json" ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); ``` ### Success Response Returns the full store object with all fields except `is_risky` and `is_restricted` (which are hidden). Below is an abbreviated example — the actual response includes all store fields. ```json theme={null} { "message": null, "data": { "id": 1, "subdomain": "my-store", "name": "My Store", "balance": "0.00", "description": "Store description", "logo": "stores/logo.png", "favicon": null, "theme": 1, "color": "#5400db", "activities": [], "social": {}, "settings": {}, "is_maintenance": false, "maintenance_message": null, "plan_id": 2, "plan_expires_at": "2025-12-31T00:00:00.000000Z", "is_kyc_verified": true, "is_beta_tester": false, "customization": {}, "ratio": "4.500", "ratio_synced_at": null, "ratio_based_on": 50, "user_id": 1, "deleted_at": null, "created_at": "2024-01-01T00:00:00.000000Z", "updated_at": "2024-01-15T00:00:00.000000Z", "human_format": { "expiry_date_human": "منذ 6 أشهر" } }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` The response includes the complete store model. Additional fields may be present depending on store configuration. The fields `is_risky` and `is_restricted` are always hidden from the response. ### Error Responses | Code | Description | | ----- | --------------------------------------- | | `401` | Unauthorized — invalid or missing token | *** ## Get Store Statistics
GET /store/statics
Returns aggregate statistics for the authenticated store. Results are cached for 3 minutes. ### Authentication ### Headers | Header | Value | Required | | --------------- | ----------------------- | ----------- | | `Authorization` | `Bearer YOUR_API_TOKEN` | Yes | | `Accept` | `application/json` | Recommended | ### Example Request ```bash cURL theme={null} curl -X GET "https://merchant-api.rmz.gg/shawarma/store/statics" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ```javascript JavaScript theme={null} const response = await fetch("https://merchant-api.rmz.gg/shawarma/store/statics", { headers: { "Authorization": "Bearer YOUR_API_TOKEN" } }); const stats = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( "https://merchant-api.rmz.gg/shawarma/store/statics", headers={"Authorization": "Bearer YOUR_API_TOKEN"} ) stats = response.json() ``` ```php PHP theme={null} $ch = curl_init("https://merchant-api.rmz.gg/shawarma/store/statics"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer YOUR_API_TOKEN" ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); ``` ### Success Response ```json theme={null} { "message": null, "data": { "sales_total": 125750.50, "sales_count": 342, "customers_count": 156, "products_count": 23, "categories_count": 5, "subscribers_count": 45, "pages_count": 3, "coupons_count": 8, "duration": "lifetime" }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` ### Response Fields | Field | Type | Description | | ------------------- | ------- | ------------------------------------------------------------- | | `sales_total` | float | Total revenue (excluding pending, cancelled, refunded orders) | | `sales_count` | integer | Total number of completed orders | | `customers_count` | integer | Total registered customers | | `products_count` | integer | Total products in store | | `categories_count` | integer | Total categories | | `subscribers_count` | integer | Total active subscriptions | | `pages_count` | integer | Total custom pages | | `coupons_count` | integer | Total coupon codes | | `duration` | string | Always `"lifetime"` — indicates statistics cover all time | Statistics are cached for 3 minutes. Frequent polling will return the same data until the cache expires. ### Error Responses | Code | Description | | ----- | --------------------------------------- | | `401` | Unauthorized — invalid or missing token | # Cancel Subscription Source: https://docs.rmz.gg/merchant-api/subscriptions/cancel-subscription Cancel a customer subscription immediately or at the end of the current period. # Cancel Subscription
POST /subscriptions//cancel
Cancels a customer subscription. You can cancel immediately (access revoked right away) or at the end of the current billing period (customer retains access until the period ends). ## Authentication ## Headers | Header | Value | Required | | --------------- | ----------------------- | ----------- | | `Authorization` | `Bearer YOUR_API_TOKEN` | Yes | | `Content-Type` | `application/json` | Yes | | `Accept` | `application/json` | Recommended | ## Path Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ------------------- | | `id` | integer | Yes | The subscription ID | ## Request Body | Parameter | Type | Required | Description | | ----------- | ------ | -------- | --------------------------------------------------------- | | `effective` | string | No | `immediate` or `end_of_period` (default: `end_of_period`) | ## Example Request ```bash cURL theme={null} curl -X POST "https://merchant-api.rmz.gg/shawarma/subscriptions/501/cancel" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"effective": "end_of_period"}' ``` ```javascript JavaScript theme={null} const response = await fetch( "https://merchant-api.rmz.gg/shawarma/subscriptions/501/cancel", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_TOKEN", "Content-Type": "application/json" }, body: JSON.stringify({ effective: "end_of_period" }) } ); const result = await response.json(); ``` ```python Python theme={null} import requests response = requests.post( "https://merchant-api.rmz.gg/shawarma/subscriptions/501/cancel", headers={ "Authorization": "Bearer YOUR_API_TOKEN", "Content-Type": "application/json" }, json={"effective": "end_of_period"} ) result = response.json() ``` ```php PHP theme={null} $ch = curl_init("https://merchant-api.rmz.gg/shawarma/subscriptions/501/cancel"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer YOUR_API_TOKEN", "Content-Type: application/json" ]); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["effective" => "end_of_period"])); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); ``` ## Success Response Returns the full subscription object in the standard `SubscriptionResource` shape — identical to [Get Subscription](./get-subscription), [List Subscriptions](./list-subscriptions), and [Lookup Subscriptions](./lookup-subscriptions). The fields most relevant to cancellation are shown below — see [Get Subscription](./get-subscription) for the complete field list. ```json theme={null} { "message": "Subscription canceled successfully", "data": { "id": 501, "status": "active", "external_customer_id": "usr_abc123", "cancel_at_period_end": true, "canceled_at": "2025-06-15T12:00:00.000000Z", "current_period_end": "2025-07-01T00:00:00.000000Z" }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` ## Behavior Options | Behavior | Description | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `end_of_period` | The subscription remains active until `current_period_end`. No renewal will be attempted. This is the default and recommended option. | | `immediate` | The subscription is canceled and access is revoked immediately. The status changes to `canceled`. | When using `end_of_period`, the subscription status stays `active` and `cancel_at_period_end` is set to `true`. The subscription will transition to `expired` when the period ends. Immediate cancellation cannot be undone. The customer would need to create a new subscription. Consider using `end_of_period` to give the customer the full value of their current billing period. ## Error Responses | Code | Description | | ----- | ------------------------------------------------------------------------------------ | | `401` | Unauthorized — invalid or missing token | | `404` | Subscription not found | | `409` | Subscription is already canceled or expired | | `422` | Validation error — invalid effective value, or subscription already canceled/expired | # Create Checkout Session Source: https://docs.rmz.gg/merchant-api/subscriptions/create-checkout-session Create a checkout session for a customer to subscribe to a product. # Create Checkout Session
POST /subscriptions/checkout-sessions
Creates a hosted checkout session that redirects the customer to complete their subscription purchase. This is the primary way for SaaS integrations to create subscriptions programmatically. ## Authentication ## Headers | Header | Value | Required | | --------------- | ----------------------- | ----------- | | `Authorization` | `Bearer YOUR_API_TOKEN` | Yes | | `Content-Type` | `application/json` | Yes | | `Accept` | `application/json` | Recommended | ## Request Body | Parameter | Type | Required | Description | | ----------------------- | ------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `product_id` | integer | Yes | The subscription product ID | | `variant_id` | integer | Yes | The subscription variant ID (determines duration and price) | | `customer.id` | integer | Conditional | Existing customer ID. If provided, other customer fields are ignored. | | `customer.country_code` | string | Conditional | Customer's country dial code (e.g. `"966"` for Saudi Arabia, `"971"` for UAE). Required if `customer.id` is not provided. | | `customer.phone` | string | Conditional | Customer's phone number, 5-15 digits (e.g. `"512345678"`). Required if `customer.id` is not provided. | | `customer.firstName` | string | Conditional | Customer's first name. Required if `customer.id` is not provided. | | `customer.lastName` | string | Conditional | Customer's last name. Required if `customer.id` is not provided. | | `customer.email` | string | Conditional | Customer's email address. Required if `customer.id` is not provided. | | `success_url` | string | Yes | URL to redirect to after successful payment | | `cancel_url` | string | Yes | URL to redirect to if the customer cancels | | `metadata` | object | No | Key-value pairs to attach to the subscription. Max 20 keys, values max 500 characters. Included in all webhook payloads and API responses. Use this for any additional per-subscription custom data you want echoed back. | | `external_customer_id` | string | No | Your system's stable user identifier (max 191 characters). Use this as the correlation key between RMZ subscriptions and your own user records. It is included in every subscription webhook payload at `data.subscription.external_customer_id` and in every subscription API response at `data.external_customer_id` (get, list, lookup, cancel, pause, unpause, extend). Can also be used as a query parameter on the `/subscriptions/lookup` endpoint. | You must provide either `customer.id` (to reference an existing customer) **or** the full set of `customer.country_code`, `customer.phone`, `customer.firstName`, `customer.lastName`, and `customer.email` (to create or match a customer). ## Example Request (New Customer) ```bash cURL theme={null} curl -X POST "https://merchant-api.rmz.gg/shawarma/subscriptions/checkout-sessions" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "product_id": 102, "variant_id": 15, "customer": { "country_code": "966", "phone": "512345678", "firstName": "Ahmed", "lastName": "Ali", "email": "ahmed@example.com" }, "metadata": { "external_user_id": "usr_abc123", "plan": "pro" }, "success_url": "https://yourapp.com/subscription/success", "cancel_url": "https://yourapp.com/subscription/cancel" }' ``` ```javascript JavaScript theme={null} const response = await fetch( "https://merchant-api.rmz.gg/shawarma/subscriptions/checkout-sessions", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_TOKEN", "Content-Type": "application/json" }, body: JSON.stringify({ product_id: 102, variant_id: 15, customer: { country_code: "966", phone: "512345678", firstName: "Ahmed", lastName: "Ali", email: "ahmed@example.com" }, success_url: "https://yourapp.com/subscription/success", cancel_url: "https://yourapp.com/subscription/cancel" }) } ); const session = await response.json(); ``` ```python Python theme={null} import requests response = requests.post( "https://merchant-api.rmz.gg/shawarma/subscriptions/checkout-sessions", headers={ "Authorization": "Bearer YOUR_API_TOKEN", "Content-Type": "application/json" }, json={ "product_id": 102, "variant_id": 15, "customer": { "country_code": "966", "phone": "512345678", "firstName": "Ahmed", "lastName": "Ali", "email": "ahmed@example.com" }, "success_url": "https://yourapp.com/subscription/success", "cancel_url": "https://yourapp.com/subscription/cancel" } ) session = response.json() ``` ```php PHP theme={null} $ch = curl_init("https://merchant-api.rmz.gg/shawarma/subscriptions/checkout-sessions"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer YOUR_API_TOKEN", "Content-Type: application/json" ]); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ "product_id" => 102, "variant_id" => 15, "customer" => [ "country_code" => "966", "phone" => "512345678", "firstName" => "Ahmed", "email" => "ahmed@example.com" ], "success_url" => "https://yourapp.com/subscription/success", "cancel_url" => "https://yourapp.com/subscription/cancel" ])); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); ``` ## Example Request (Existing Customer) ```bash cURL theme={null} curl -X POST "https://merchant-api.rmz.gg/shawarma/subscriptions/checkout-sessions" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "product_id": 102, "variant_id": 15, "customer": { "id": 4501 }, "success_url": "https://yourapp.com/subscription/success", "cancel_url": "https://yourapp.com/subscription/cancel" }' ``` ```javascript JavaScript theme={null} const response = await fetch( "https://merchant-api.rmz.gg/shawarma/subscriptions/checkout-sessions", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_TOKEN", "Content-Type": "application/json" }, body: JSON.stringify({ product_id: 102, variant_id: 15, customer: { id: 4501 }, success_url: "https://yourapp.com/subscription/success", cancel_url: "https://yourapp.com/subscription/cancel" }) } ); const session = await response.json(); ``` ```python Python theme={null} import requests response = requests.post( "https://merchant-api.rmz.gg/shawarma/subscriptions/checkout-sessions", headers={ "Authorization": "Bearer YOUR_API_TOKEN", "Content-Type": "application/json" }, json={ "product_id": 102, "variant_id": 15, "customer": {"id": 4501}, "success_url": "https://yourapp.com/subscription/success", "cancel_url": "https://yourapp.com/subscription/cancel" } ) session = response.json() ``` ```php PHP theme={null} $ch = curl_init("https://merchant-api.rmz.gg/shawarma/subscriptions/checkout-sessions"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer YOUR_API_TOKEN", "Content-Type: application/json" ]); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ "product_id" => 102, "variant_id" => 15, "customer" => ["id" => 4501], "success_url" => "https://yourapp.com/subscription/success", "cancel_url" => "https://yourapp.com/subscription/cancel" ])); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); ``` ## Success Response ```json theme={null} { "message": null, "data": { "session_id": "cs_abc123def456", "checkout_url": "https://billing.rmz.gg/checkout/01KNAE1283PNWZDNE5W2TNRY5J", "expires_at": "2025-06-01T01:00:00.000000Z" }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` ## Response Fields | Field | Type | Description | | -------------- | ------ | ------------------------------------------------------- | | `session_id` | string | Unique identifier for the checkout session | | `checkout_url` | string | URL to redirect the customer to for payment | | `expires_at` | string | ISO 8601 timestamp when the session expires (7 minutes) | ## Flow 1. Your server creates a checkout session via this endpoint 2. Redirect the customer to the `checkout_url` 3. The customer completes payment on the hosted checkout page 4. On success, the customer is redirected to your `success_url` with `?rmz-subscription-id={id}` appended as a query parameter 5. A `subscription.created` webhook is fired to your webhook endpoint **Example redirect:** ``` https://yourapp.com/subscription/success?rmz-subscription-id=501 ``` Use the `rmz-subscription-id` query parameter from the redirect to immediately look up the subscription. Always also listen for the `subscription.created` webhook to confirm the subscription was successfully created, as the redirect alone is not a guarantee of payment success. ## Error Responses | Code | Description | | ----- | ------------------------------------------------ | | `401` | Unauthorized — invalid or missing token | | `404` | Product or variant not found | | `422` | Validation error — missing or invalid parameters | # Extend Subscription Source: https://docs.rmz.gg/merchant-api/subscriptions/extend-subscription Extend a subscription's current period by a specified number of days or months. # Extend Subscription
POST /subscriptions//extend
Extends a subscription's current billing period by a specified number of days or months. This is useful for courtesy extensions, compensation for service outages, or promotional offers. You must provide either `days` or `months` (but not both). This endpoint has an idempotency guard. If a duplicate request is made within 10 seconds, it will return `429 Too Many Requests` instead of double-extending the subscription. ## Authentication ## Headers | Header | Value | Required | | --------------- | ----------------------- | ----------- | | `Authorization` | `Bearer YOUR_API_TOKEN` | Yes | | `Content-Type` | `application/json` | Yes | | `Accept` | `application/json` | Recommended | ## Path Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ------------------- | | `id` | integer | Yes | The subscription ID | ## Request Body | Parameter | Type | Required | Description | | --------- | ------- | ------------------------- | ------------------------------------------------------------------------------------- | | `days` | integer | Required without `months` | Number of days to extend the subscription (1-365) | | `months` | integer | Required without `days` | Number of months to extend the subscription (1-24). Each month is treated as 30 days. | ## Example Request ```bash cURL theme={null} curl -X POST "https://merchant-api.rmz.gg/shawarma/subscriptions/501/extend" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{"days": 7}' ``` ```javascript JavaScript theme={null} const response = await fetch( "https://merchant-api.rmz.gg/shawarma/subscriptions/501/extend", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_TOKEN", "Content-Type": "application/json" }, body: JSON.stringify({ days: 7 }) } ); const result = await response.json(); ``` ```python Python theme={null} import requests response = requests.post( "https://merchant-api.rmz.gg/shawarma/subscriptions/501/extend", headers={ "Authorization": "Bearer YOUR_API_TOKEN", "Content-Type": "application/json" }, json={"days": 7} ) result = response.json() ``` ```php PHP theme={null} $ch = curl_init("https://merchant-api.rmz.gg/shawarma/subscriptions/501/extend"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer YOUR_API_TOKEN", "Content-Type: application/json" ]); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["days" => 7])); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); ``` ## Success Response Returns the full subscription object in the standard `SubscriptionResource` shape — identical to [Get Subscription](./get-subscription), [List Subscriptions](./list-subscriptions), and [Lookup Subscriptions](./lookup-subscriptions). The fields most relevant to extension are shown below — see [Get Subscription](./get-subscription) for the complete field list. ```json theme={null} { "message": "Subscription extended successfully", "data": { "id": 501, "status": "active", "external_customer_id": "usr_abc123", "current_period_start": "2025-06-01T00:00:00.000000Z", "current_period_end": "2025-07-08T00:00:00.000000Z", "end_date": "2025-07-08T00:00:00.000000Z" }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` ## Response Fields The response is the full `SubscriptionResource`. See [Get Subscription](./get-subscription) for the complete list. The fields most relevant to extension: | Field | Type | Description | | ---------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | integer | Subscription ID | | `status` | string | Current subscription status | | `external_customer_id` | string/null | Stable identifier the merchant supplied at checkout creation. Use this to correlate RMZ subscriptions with your own user records. `null` if not supplied. | | `current_period_start` | string | Start of the current billing period | | `current_period_end` | string | New end of the current billing period (extended) | | `end_date` | string | Updated subscription end date | A `subscription.updated` webhook event is fired when a subscription is extended, including the number of days added and the new end date. ## Error Responses | Code | Description | | ----- | ----------------------------------------------------------------- | | `401` | Unauthorized — invalid or missing token | | `404` | Subscription not found | | `409` | Subscription is canceled or expired and cannot be extended | | `422` | Validation error — must provide `days` (1-365) or `months` (1-24) | # Get Subscription Source: https://docs.rmz.gg/merchant-api/subscriptions/get-subscription Retrieve detailed information for a specific subscription. # Get Subscription
GET /subscriptions/
Returns full details for a single customer subscription, including product, customer, and order information. ## Authentication ## Headers | Header | Value | Required | | --------------- | ----------------------- | ----------- | | `Authorization` | `Bearer YOUR_API_TOKEN` | Yes | | `Accept` | `application/json` | Recommended | ## Path Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ------------------- | | `id` | integer | Yes | The subscription ID | ## Example Request ```bash cURL theme={null} curl -X GET "https://merchant-api.rmz.gg/shawarma/subscriptions/501" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ```javascript JavaScript theme={null} const response = await fetch("https://merchant-api.rmz.gg/shawarma/subscriptions/501", { headers: { "Authorization": "Bearer YOUR_API_TOKEN" } }); const subscription = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( "https://merchant-api.rmz.gg/shawarma/subscriptions/501", headers={"Authorization": "Bearer YOUR_API_TOKEN"} ) subscription = response.json() ``` ```php PHP theme={null} $ch = curl_init("https://merchant-api.rmz.gg/shawarma/subscriptions/501"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer YOUR_API_TOKEN" ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); ``` ## Success Response ```json theme={null} { "message": null, "data": { "id": 501, "status": "active", "external_customer_id": "usr_abc123", "starts_at": "2024-01-01", "ends_at": "2024-12-31", "start_date": "2024-01-01", "end_date": "2024-12-31", "current_period_start": "2024-01-01T00:00:00.000000Z", "current_period_end": "2024-12-31T00:00:00.000000Z", "trial_ends_at": null, "cancel_at_period_end": false, "canceled_at": null, "duration": "annually", "order_id": 78901, "auto_renew": true, "price": { "amount": 199.99, "formatted": "١٩٩٫٩٩ ر.س", "currency": "SAR" }, "metadata": { "external_user_id": "usr_abc123", "plan": "pro" }, "product": { "id": 102, "name": "Pro Plan", "slug": "pro-plan", "type": "subscription" }, "variant": { "id": 15, "duration": "annually", "duration_text": "سنه", "price": 199.99 }, "payment_method": { "last_four": "4242", "scheme": "visa" }, "scheduled_variant": null, "features": null, "is_active": true, "is_expired": false, "days_remaining": 276, "created_at": "2024-01-01T00:00:00.000000Z", "updated_at": "2024-01-01T00:00:00.000000Z" }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` ## Response Fields | Field | Type | Description | | ----------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | integer | Subscription ID | | `status` | string | Current status: `trialing`, `active`, `past_due`, `paused`, `canceled`, `expired` | | `external_customer_id` | string/null | Stable identifier the merchant supplied at checkout creation. Use this to correlate RMZ subscriptions with your own user records. `null` if not supplied. | | `current_period_start` | string | Start of current billing period (ISO 8601) | | `current_period_end` | string | End of current billing period (ISO 8601) | | `trial_ends_at` | string/null | When the trial ends (null if no trial) | | `cancel_at_period_end` | boolean | Whether the subscription will cancel at end of current period | | `canceled_at` | string/null | When cancellation was requested | | `paused_at` | string | ISO 8601 timestamp recording when the subscription was paused. Only present when `status` is `paused`. | | `paused_remaining_days` | integer | Number of days that were remaining in the current period at the moment of pause. When the subscription is unpaused, this many days are added to the new period end. Only present when `status` is `paused`. | | `next_retry_at` | string | ISO 8601 timestamp for when the next automatic retry charge will be attempted. Only present when `status` is `past_due`. | | `duration` | string | Duration key: `monthly`, `quarterly`, `semiAnnual`, `annually`, `biennial`, `quinquennial`, `decennial` | | `order_id` | integer | Original order ID | | `auto_renew` | boolean | Whether auto-renewal is active (has saved card and not canceling) | | `price` | object | The locked subscription price (set at creation, immune to variant price changes) | | `metadata` | object/null | Custom key-value pairs attached at checkout session creation | | `product` | object | The subscription product (id, name, slug, type) | | `variant` | object | Current subscription variant (id, duration, duration\_text, price) | | `payment_method` | object/null | Saved card info (last\_four, scheme) — only if auto-renew is active | | `scheduled_variant` | object/null | Pending variant change (applied at next renewal) | | `is_active` | boolean | Whether the subscription currently grants access | | `is_expired` | boolean | Whether the subscription has expired | | `days_remaining` | integer | Number of days remaining in current period | ## Error Responses | Code | Description | | ----- | --------------------------------------- | | `401` | Unauthorized — invalid or missing token | | `404` | Subscription not found | # List Subscriptions Source: https://docs.rmz.gg/merchant-api/subscriptions/list-subscriptions Retrieve a paginated list of customer subscriptions with optional filters. # List Subscriptions
GET /subscriptions
Returns a paginated list of customer subscriptions for the authenticated store. You can filter by customer email, phone, or country code. ## Authentication ## Headers | Header | Value | Required | | --------------- | ----------------------- | ----------- | | `Authorization` | `Bearer YOUR_API_TOKEN` | Yes | | `Accept` | `application/json` | Recommended | ## Query Parameters | Parameter | Type | Required | Description | | --------------------- | ------- | -------- | --------------------------------------------- | | `page` | integer | No | Page number (default: 1) | | `customerEmail` | string | No | Filter by customer email | | `customerPhone` | string | No | Filter by customer phone number | | `customerCountryCode` | string | No | Filter by customer country code (e.g., `966`) | ## Example Request ```bash cURL theme={null} curl -X GET "https://merchant-api.rmz.gg/shawarma/subscriptions?customerEmail=ahmed@example.com" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ```javascript JavaScript theme={null} const params = new URLSearchParams({ customerEmail: "ahmed@example.com" }); const response = await fetch( `https://merchant-api.rmz.gg/shawarma/subscriptions?${params}`, { headers: { "Authorization": "Bearer YOUR_API_TOKEN" } } ); const subscriptions = await response.json(); ``` ```python Python theme={null} import requests response = requests.get( "https://merchant-api.rmz.gg/shawarma/subscriptions", headers={"Authorization": "Bearer YOUR_API_TOKEN"}, params={"customerEmail": "ahmed@example.com"} ) subscriptions = response.json() ``` ```php PHP theme={null} $params = http_build_query(["customerEmail" => "ahmed@example.com"]); $ch = curl_init("https://merchant-api.rmz.gg/shawarma/subscriptions?{$params}"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer YOUR_API_TOKEN" ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); ``` ## Success Response ```json theme={null} { "message": null, "data": { "current_page": 1, "data": [ { "id": 501, "status": "active", "external_customer_id": "usr_abc123", "starts_at": "2026-01-01 00:00:00", "ends_at": "2027-01-01 00:00:00", "start_date": "2026-01-01 00:00:00", "end_date": "2027-01-01 00:00:00", "current_period_start": "2026-04-01 00:00:00", "current_period_end": "2026-05-01 00:00:00", "trial_ends_at": null, "cancel_at_period_end": false, "canceled_at": null, "duration": "monthly", "order_id": 78901, "auto_renew": true, "price": { "amount": 99.00, "formatted": "99.00 ر.س", "currency": "SAR" }, "product": { "id": 102, "name": "Pro Plan", "slug": "pro-plan", "type": "subscription" }, "variant": { "id": 305, "duration": "monthly", "duration_text": "شهر", "price": 99.00 }, "payment_method": { "last_four": "4242", "scheme": "visa" }, "metadata": null, "features": null, "is_active": true, "is_expired": false, "days_remaining": 255, "created_at": "2026-01-01T00:00:00.000000Z", "updated_at": "2026-04-01T00:00:00.000000Z" } ], "first_page_url": "https://merchant-api.rmz.gg/shawarma/subscriptions?page=1", "from": 1, "next_page_url": null, "path": "https://merchant-api.rmz.gg/shawarma/subscriptions", "per_page": 15, "prev_page_url": null, "to": 1 }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` ## Duration Values The `duration` field on each subscription is an enum representing the billing period of the subscription variant. Valid values: | Value | Meaning | | -------------- | -------- | | `monthly` | 1 month | | `quarterly` | 3 months | | `semiAnnual` | 6 months | | `annually` | 1 year | | `biennial` | 2 years | | `quinquennial` | 5 years | | `decennial` | 10 years | ## Status Values The `status` field reflects the current lifecycle state of the subscription. Possible values: | Value | Meaning | | ---------- | ---------------------------------------------------- | | `trialing` | In a trial period; access is granted | | `active` | Fully paid and active; access is granted | | `past_due` | Renewal payment failed; may still be in grace period | | `paused` | Temporarily paused by the merchant or customer | | `canceled` | Canceled; may still have access until period end | | `expired` | Terminal state; access is no longer granted | ## Filter Examples ### By Customer Phone ```bash theme={null} curl -X GET "https://merchant-api.rmz.gg/shawarma/subscriptions?customerCountryCode=966&customerPhone=501234567" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ### All Subscriptions (No Filter) ```bash theme={null} curl -X GET "https://merchant-api.rmz.gg/shawarma/subscriptions?page=1" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ## Error Responses | Code | Description | | ----- | --------------------------------------- | | `401` | Unauthorized — invalid or missing token | # Lookup Subscriptions Source: https://docs.rmz.gg/merchant-api/subscriptions/lookup-subscriptions Check whether a user currently has an active subscription. Built for SaaS paywall integrations. # Lookup Subscriptions
GET /subscriptions/lookup
Returns the subscriptions a given end user has with your store, filtered to the ones that currently grant access (status `active` or `trialing`) by default. This is the hot-path endpoint for **SaaS paywalls** — the typical integration asks "does this user have an active subscription?" on every protected request. The response includes a convenience `has_active` boolean so you can gate access with a single field check. ## Authentication ## Headers | Header | Value | Required | | --------------- | ----------------------- | ----------- | | `Authorization` | `Bearer YOUR_API_TOKEN` | Yes | | `Accept` | `application/json` | Recommended | ## Rate Limit This endpoint is throttled to **120 requests per minute** per API token — higher than the rest of the subscription endpoints because it is intended to be called on every paywalled request. If you expect higher volume, cache results briefly on your side (e.g. 60 seconds per user). ## Query Parameters You must provide **at least one** of the following user identifiers: * `external_customer_id`, **or** * `email`, **or** * `phone` (together with `country_code`). If none of these are supplied the request is rejected with a `422`. | Parameter | Type | Required | Description | | ---------------------- | ---------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `external_customer_id` | string (max 191) | Conditional | The merchant's own user identifier, stored on the subscription at checkout creation time. This is the fastest and most reliable lookup because it is indexed and isolated per store. | | `email` | email (max 191) | Conditional | Customer email. Matched exactly against the customer record. | | `country_code` | string (max 5) | Conditional | Country dial code without the `+` (e.g. `966`). A leading `+` is stripped automatically. Should be paired with `phone` to be useful. | | `phone` | string (max 15) | Conditional | Customer phone number (local part only, no country code). | | `product_id` | integer | No | Restrict results to a single product. Useful when you sell multiple subscription products and only want to check entitlement for one of them. | | `include_inactive` | boolean | No | Defaults to `false`. When `false`, only `active` and `trialing` subscriptions are returned (the "currently entitled" view). Set to `true` to also see `paused`, `past_due`, `canceled`, and `expired` rows, for example when building a billing history screen. | The lookup is always scoped to your store — you will never see subscriptions that belong to another merchant, even if the same customer email exists across stores. ## Example Requests ### Paywall check by external ID (most common) ```bash cURL theme={null} curl -X GET "https://merchant-api.rmz.gg/shawarma/subscriptions/lookup?external_customer_id=usr_abc123" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ```javascript JavaScript theme={null} const params = new URLSearchParams({ external_customer_id: "usr_abc123" }); const response = await fetch( `https://merchant-api.rmz.gg/shawarma/subscriptions/lookup?${params}`, { headers: { "Authorization": "Bearer YOUR_API_TOKEN" } } ); const { data } = await response.json(); if (data.has_active) { // grant access } ``` ```python Python theme={null} import requests response = requests.get( "https://merchant-api.rmz.gg/shawarma/subscriptions/lookup", headers={"Authorization": "Bearer YOUR_API_TOKEN"}, params={"external_customer_id": "usr_abc123"}, ) data = response.json()["data"] if data["has_active"]: grant_access() ``` ```php PHP theme={null} $params = http_build_query(["external_customer_id" => "usr_abc123"]); $ch = curl_init("https://merchant-api.rmz.gg/shawarma/subscriptions/lookup?{$params}"); curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer YOUR_API_TOKEN"]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); if ($response["data"]["has_active"]) { // grant access } ``` ### Lookup by email, restricted to a specific product ```bash theme={null} curl -X GET "https://merchant-api.rmz.gg/shawarma/subscriptions/lookup?email=ahmed@example.com&product_id=102" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ### Lookup by phone, including historical subscriptions ```bash theme={null} curl -X GET "https://merchant-api.rmz.gg/shawarma/subscriptions/lookup?country_code=966&phone=501234567&include_inactive=1" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ## Success Response ```json theme={null} { "message": null, "data": { "count": 1, "has_active": true, "subscriptions": [ { "id": 501, "status": "active", "external_customer_id": "usr_abc123", "starts_at": "2024-01-01", "ends_at": "2024-12-31", "start_date": "2024-01-01", "end_date": "2024-12-31", "current_period_start": "2024-01-01T00:00:00.000000Z", "current_period_end": "2024-12-31T00:00:00.000000Z", "trial_ends_at": null, "cancel_at_period_end": false, "canceled_at": null, "duration": "annually", "order_id": 78901, "auto_renew": true, "price": { "amount": 199.99, "formatted": "١٩٩٫٩٩ ر.س", "currency": "SAR" }, "product": { "id": 102, "name": "Pro Plan", "slug": "pro-plan", "type": "subscription" }, "variant": { "id": 15, "duration": "annually", "duration_text": "سنه", "price": 199.99 }, "metadata": { "external_user_id": "usr_abc123", "plan": "pro" }, "features": null, "is_active": true, "is_expired": false, "days_remaining": 276, "created_at": "2024-01-01T00:00:00.000000Z", "updated_at": "2024-01-01T00:00:00.000000Z" } ] }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` ## Response Fields | Field | Type | Description | | --------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `count` | integer | Number of subscriptions returned. The endpoint caps results at **50** per call. | | `has_active` | boolean | `true` if at least one of the returned rows is `active` or `trialing`. Use this as your paywall gate. | | `subscriptions` | array | Array of subscription objects. Each object has the same shape as [Get Subscription](./get-subscription), **without** the `payment_method`, `scheduled_variant`, or `customer` fields (only `product` and `variant` relations are preloaded here). | ### Empty result When no subscription matches, the endpoint still returns `200 OK` with an empty array — there is no `404`: ```json theme={null} { "message": null, "data": { "count": 0, "has_active": false, "subscriptions": [] }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` ## Common Integration Pattern A minimal SaaS paywall middleware: ```javascript theme={null} async function hasActiveSubscription(userId) { const res = await fetch( `https://merchant-api.rmz.gg/shawarma/subscriptions/lookup?external_customer_id=${encodeURIComponent(userId)}&product_id=102`, { headers: { Authorization: `Bearer ${process.env.RMZ_API_TOKEN}` } } ); if (!res.ok) { // Fail closed or fail open depending on your risk tolerance. // Most paywalls fail closed on 5xx and fail open on network errors. return false; } const { data } = await res.json(); return data.has_active; } ``` Pass `external_customer_id` when you create the checkout session (see [Create Checkout Session](./create-checkout-session)). That ID is stored on the subscription itself, so the lookup is a direct indexed read instead of a `customers` table join. ## Error Responses | Code | Description | | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `401` | Unauthorized — invalid or missing API token. | | `403` | Subscription billing is not enabled for the store (RMZ Plus required). | | `422` | Validation error. Returned when no identifier is provided, or when a parameter exceeds its length limit / fails the type check (e.g. `email` is not a valid email). | | `429` | Rate limit exceeded (120 requests per minute per token). | ### Example `422` — no identifier supplied ```json theme={null} { "message": "At least one of external_customer_id, email, or phone is required", "data": null, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` ### Example `422` — invalid parameter ```json theme={null} { "message": "The given data was invalid.", "errors": { "email": ["The email must be a valid email address."] } } ``` # Pause Subscription Source: https://docs.rmz.gg/merchant-api/subscriptions/pause-subscription Pause a customer subscription to temporarily suspend auto-renewal. # Pause Subscription
POST /subscriptions//pause
Pauses an active subscription. Auto-renewal is suspended and `is_active` becomes `false`. The remaining days in the current period are stored internally and will be restored when the subscription is unpaused. ## Authentication ## Headers | Header | Value | Required | | --------------- | ----------------------- | ----------- | | `Authorization` | `Bearer YOUR_API_TOKEN` | Yes | | `Accept` | `application/json` | Recommended | ## Path Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ------------------- | | `id` | integer | Yes | The subscription ID | ## Example Request ```bash cURL theme={null} curl -X POST "https://merchant-api.rmz.gg/shawarma/subscriptions/501/pause" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ```javascript JavaScript theme={null} const response = await fetch( "https://merchant-api.rmz.gg/shawarma/subscriptions/501/pause", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_TOKEN" } } ); const result = await response.json(); ``` ```python Python theme={null} import requests response = requests.post( "https://merchant-api.rmz.gg/shawarma/subscriptions/501/pause", headers={"Authorization": "Bearer YOUR_API_TOKEN"} ) result = response.json() ``` ```php PHP theme={null} $ch = curl_init("https://merchant-api.rmz.gg/shawarma/subscriptions/501/pause"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer YOUR_API_TOKEN" ]); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); ``` ## Success Response Returns the full subscription object in the standard `SubscriptionResource` shape — identical to [Get Subscription](./get-subscription), [List Subscriptions](./list-subscriptions), and [Lookup Subscriptions](./lookup-subscriptions). The fields most relevant to pausing are shown below — see [Get Subscription](./get-subscription) for the complete field list. ```json theme={null} { "message": "Subscription paused successfully", "data": { "id": 501, "status": "paused", "external_customer_id": "usr_abc123", "is_active": false, "current_period_end": "2025-07-01T00:00:00.000000Z" }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` ## Behavior * Only `active` subscriptions can be paused * Auto-renewal is suspended — no payment attempts will be made * `is_active` is set to `false` * The remaining days in the current period are stored in metadata * When unpaused, the period is extended by the stored remaining days * A `subscription.paused` webhook event is dispatched Pausing a subscription does not refund any charges. It simply stops auto-renewal and marks the subscription as inactive. Use this for temporary holds, not cancellations. ## Error Responses | Code | Description | | ----- | --------------------------------------- | | `401` | Unauthorized — invalid or missing token | | `404` | Subscription not found | | `422` | Only active subscriptions can be paused | # Unpause Subscription Source: https://docs.rmz.gg/merchant-api/subscriptions/unpause-subscription Resume a paused subscription, restoring access and extending the billing period. # Unpause Subscription
POST /subscriptions//unpause
Resumes a paused subscription. The subscription returns to `active` status and the billing period is extended by the number of days that were remaining when the subscription was paused. ## Authentication ## Headers | Header | Value | Required | | --------------- | ----------------------- | ----------- | | `Authorization` | `Bearer YOUR_API_TOKEN` | Yes | | `Accept` | `application/json` | Recommended | ## Path Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ------------------- | | `id` | integer | Yes | The subscription ID | ## Example Request ```bash cURL theme={null} curl -X POST "https://merchant-api.rmz.gg/shawarma/subscriptions/501/unpause" \ -H "Authorization: Bearer YOUR_API_TOKEN" ``` ```javascript JavaScript theme={null} const response = await fetch( "https://merchant-api.rmz.gg/shawarma/subscriptions/501/unpause", { method: "POST", headers: { "Authorization": "Bearer YOUR_API_TOKEN" } } ); const result = await response.json(); ``` ```python Python theme={null} import requests response = requests.post( "https://merchant-api.rmz.gg/shawarma/subscriptions/501/unpause", headers={"Authorization": "Bearer YOUR_API_TOKEN"} ) result = response.json() ``` ```php PHP theme={null} $ch = curl_init("https://merchant-api.rmz.gg/shawarma/subscriptions/501/unpause"); curl_setopt($ch, CURLOPT_HTTPHEADER, [ "Authorization: Bearer YOUR_API_TOKEN" ]); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = json_decode(curl_exec($ch), true); ``` ## Success Response Returns the full subscription object in the standard `SubscriptionResource` shape — identical to [Get Subscription](./get-subscription), [List Subscriptions](./list-subscriptions), and [Lookup Subscriptions](./lookup-subscriptions). The fields most relevant to unpausing are shown below — see [Get Subscription](./get-subscription) for the complete field list. ```json theme={null} { "message": "Subscription unpaused successfully", "data": { "id": 501, "status": "active", "external_customer_id": "usr_abc123", "is_active": true, "current_period_start": "2025-06-20T00:00:00.000000Z", "current_period_end": "2025-07-10T00:00:00.000000Z" }, "api": "rmz.shawarma", "timestamp": 1699999999 } ``` ## Behavior * Only `paused` subscriptions can be unpaused * Status returns to `active` and `is_active` is set to `true` * `current_period_start` is set to now * `current_period_end` is extended from now by the remaining days stored at pause time (minimum 1 day) * Auto-renewal resumes normally at the new period end * A `subscription.unpaused` webhook event is dispatched ## Example Timeline 1. Subscription active: Jan 1 - Feb 1 (31 days) 2. Paused on Jan 20 (11 days remaining stored) 3. Unpaused on Jan 25 4. New period: Jan 25 - Feb 5 (11 days from unpause) ## Error Responses | Code | Description | | ----- | ----------------------------------------- | | `401` | Unauthorized — invalid or missing token | | `404` | Subscription not found | | `422` | Only paused subscriptions can be unpaused | # Analytics Collection Source: https://docs.rmz.gg/storefront-api/analytics Collect pageview and custom analytics events from your storefront. The Analytics Collection endpoints allow your custom storefront to send pageview, custom event, and interaction data to RMZ for store analytics. These endpoints run on a separate rate limit from the main Storefront API to ensure analytics traffic never blocks store requests. These endpoints are used internally by the RMZ analytics script (`rmz-analytics.js`) and the Storefront SDK. You can also call them directly from a custom storefront implementation. ## Authentication No Bearer token or secret key is required. The store is identified from the request domain/origin (same as other Storefront API endpoints). ## Rate Limits Analytics endpoints have their own per-IP rate limiting (separate from the main API): | Limit | Window | | ----------------- | -------- | | 120 events per IP | 1 minute | Batch requests count each event in the batch against the rate limit. *** ## POST /storefront/analytics/collect Submit a single analytics event. ### Headers | Header | Value | Required | | ------------ | ---------------- | -------- | | Content-Type | application/json | Yes | ### Body Parameters | Parameter | Type | Required | Description | | ------------- | ------- | -------- | ------------------------------------------------------------------- | | type | string | No | Event type: `pageview`, `event`, `outbound_click`. Default: `event` | | session\_id | string | No | Session identifier (max 64 chars) | | visitor\_id | string | No | Persistent visitor identifier (max 64 chars) | | url | string | No | Full page URL (max 2000 chars) | | path | string | No | Page path (max 500 chars) | | event\_name | string | No | Custom event name (for `type: event`, max 100 chars) | | properties | object | No | Custom event properties (for `type: event`) | | title | string | No | Page title (for `type: pageview`, max 500 chars) | | referrer | string | No | Referrer URL (for `type: pageview`, max 2000 chars) | | screen\_width | integer | No | Screen width in pixels (for `type: pageview`) | | destination | string | No | Outbound link URL (for `type: outbound_click`, max 2000 chars) | | text | string | No | Link text (for `type: outbound_click`, max 200 chars) | ### Example Request ```bash cURL theme={null} curl -X POST "https://front.rmz.gg/api/storefront/analytics/collect" \ -H "Content-Type: application/json" \ -d '{ "type": "pageview", "session_id": "sess_abc123", "visitor_id": "vis_xyz789", "url": "https://mystore.com/products/game-key", "path": "/products/game-key", "title": "Premium Game Key - My Store", "referrer": "https://google.com", "screen_width": 1920 }' ``` ```javascript JavaScript theme={null} await fetch("https://front.rmz.gg/api/storefront/analytics/collect", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ type: "pageview", session_id: "sess_abc123", visitor_id: "vis_xyz789", url: window.location.href, path: window.location.pathname, title: document.title, referrer: document.referrer, screen_width: window.innerWidth }) }); ``` ```python Python theme={null} requests.post("https://front.rmz.gg/api/storefront/analytics/collect", json={ "type": "event", "session_id": "sess_abc123", "visitor_id": "vis_xyz789", "event_name": "add_to_cart", "properties": {"product_id": 101, "quantity": 1}, "url": "https://mystore.com/products/game-key", "path": "/products/game-key" }) ``` ### Response #### Success (202) ```json theme={null} { "status": "ok" } ``` #### Error Responses | Status | Description | | ------ | ----------------------------------------------------- | | 400 | `store_id` could not be resolved from request context | | 429 | Rate limit exceeded (120 events/min per IP) | The response uses a `202 Accepted` status code rather than `200 OK`, indicating the event has been accepted for processing. The response format uses `status` instead of the standard `success`/`data` wrapper used by other Storefront API endpoints. *** ## POST /storefront/analytics/collect/batch Submit multiple analytics events in a single request. Reduces HTTP overhead for high-traffic storefronts. ### Headers | Header | Value | Required | | ------------ | ---------------- | -------- | | Content-Type | application/json | Yes | ### Body Parameters | Parameter | Type | Required | Description | | --------- | ----- | -------- | ----------------------------------------------------------------------------------------------------------------- | | events | array | Yes | Array of event objects (max 30 per batch). Each event has the same structure as the single collect endpoint body. | ### Example Request ```bash cURL theme={null} curl -X POST "https://front.rmz.gg/api/storefront/analytics/collect/batch" \ -H "Content-Type: application/json" \ -d '{ "events": [ { "type": "pageview", "session_id": "sess_abc123", "visitor_id": "vis_xyz789", "url": "https://mystore.com/", "path": "/", "title": "Home - My Store" }, { "type": "event", "session_id": "sess_abc123", "visitor_id": "vis_xyz789", "event_name": "view_product", "properties": {"product_id": 101} } ] }' ``` ```javascript JavaScript theme={null} const events = [ { type: "pageview", session_id: sid, visitor_id: vid, url: "https://mystore.com/", path: "/" }, { type: "event", session_id: sid, visitor_id: vid, event_name: "view_product", properties: { product_id: 101 } } ]; await fetch("https://front.rmz.gg/api/storefront/analytics/collect/batch", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ events }) }); ``` ```python Python theme={null} requests.post("https://front.rmz.gg/api/storefront/analytics/collect/batch", json={ "events": [ {"type": "pageview", "session_id": "sess_abc123", "url": "https://mystore.com/", "path": "/"}, {"type": "event", "session_id": "sess_abc123", "event_name": "view_product", "properties": {"product_id": 101}} ] }) ``` ### Response #### Success (202) ```json theme={null} { "status": "ok", "processed": 2 } ``` The `processed` field indicates how many events from the batch were successfully processed. #### Error Responses | Status | Description | | ------ | ------------------------------------------------------------------------------ | | 400 | `events` array is missing or empty, or `store_id` could not be resolved | | 429 | Rate limit exceeded (each event in the batch counts against the 120/min limit) | Batches are capped at 30 events. Any events beyond the 30th are silently dropped. If your storefront generates more than 30 events between flushes, send multiple batch requests. ## Supported Event Types | Type | Description | | ---------------- | ------------------------------------------------------- | | `pageview` | Page view with title, referrer, and screen width | | `event` | Custom named event with arbitrary properties | | `outbound_click` | Click on an external link with destination URL and text | The event types `web_vital`, `js_error`, `time_on_page`, and `scroll_depth` are silently skipped by the server and will not be recorded. # Customer Profile Source: https://docs.rmz.gg/storefront-api/authentication/customer-profile Get and update authenticated customer profile information, and log out. ## GET /customer/profile Retrieve the authenticated customer's profile. ### Authentication Requires Bearer token (`auth:customer_api`). ### Headers | Header | Value | Required | | ------------- | ------- | -------- | | Authorization | Bearer | Yes | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/customer/profile" \ -H "Authorization: Bearer 1|abc123xyz..." ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/customer/profile", { headers: { "Authorization": "Bearer 1|abc123xyz..." } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/customer/profile", headers={ "Authorization": "Bearer 1|abc123xyz..." }) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->get("https://front.rmz.gg/api/customer/profile"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "id": 123, "first_name": "Ahmed", "last_name": "Ali", "full_name": "Ahmed Ali", "email": "ahmed@example.com", "phone": "501234567", "country_code": "966", "full_phone": "+966501234567", "avatar": null, "is_banned": false, "created_at": "2024-06-01T00:00:00.000000Z", "updated_at": "2024-06-15T10:30:00.000000Z" } } ``` #### Error Responses | Status | Description | | ------ | -------------------------- | | 401 | Customer not authenticated | *** ## PATCH /customer/profile Update the authenticated customer's profile. ### Authentication Requires Bearer token (`auth:customer_api`). ### Headers | Header | Value | Required | | ------------- | ---------------- | -------- | | Authorization | Bearer | Yes | | Content-Type | application/json | Yes | ### Body Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------------------------------------- | | firstName | string | Yes | Customer first name | | lastName | string | Yes | Customer last name | | email | string | Yes | Customer email (must be unique per store) | ### Example Request ```bash cURL theme={null} curl -X PATCH "https://front.rmz.gg/api/customer/profile" \ -H "Authorization: Bearer 1|abc123xyz..." \ -H "Content-Type: application/json" \ -d '{ "firstName": "Ahmed", "lastName": "Ali", "email": "ahmed.updated@example.com" }' ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/customer/profile", { method: "PATCH", headers: { "Authorization": "Bearer 1|abc123xyz...", "Content-Type": "application/json" }, body: JSON.stringify({ firstName: "Ahmed", lastName: "Ali", email: "ahmed.updated@example.com" }) }); const data = await response.json(); ``` ```python Python theme={null} response = requests.patch("https://front.rmz.gg/api/customer/profile", headers={"Authorization": "Bearer 1|abc123xyz..."}, json={ "firstName": "Ahmed", "lastName": "Ali", "email": "ahmed.updated@example.com" } ) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->patch("https://front.rmz.gg/api/customer/profile", [ "firstName" => "Ahmed", "lastName" => "Ali", "email" => "ahmed.updated@example.com" ]); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "id": 123, "first_name": "Ahmed", "last_name": "Ali", "full_name": "Ahmed Ali", "email": "ahmed.updated@example.com", "phone": "501234567", "country_code": "966", "full_phone": "+966501234567", "avatar": null, "is_banned": false, "created_at": "2024-06-01T00:00:00.000000Z", "updated_at": "2024-06-15T12:00:00.000000Z" }, "message": "Profile updated successfully" } ``` #### Error Responses | Status | Description | | ------ | ---------------------------------------------------------------- | | 401 | Customer not authenticated | | 422 | Validation error (email already in use, missing required fields) | *** ## POST /customer/logout Revoke the current access token and log out. ### Authentication Requires Bearer token (`auth:customer_api`). ### Headers | Header | Value | Required | | ------------- | ------- | -------- | | Authorization | Bearer | Yes | ### Example Request ```bash cURL theme={null} curl -X POST "https://front.rmz.gg/api/customer/logout" \ -H "Authorization: Bearer 1|abc123xyz..." ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/customer/logout", { method: "POST", headers: { "Authorization": "Bearer 1|abc123xyz..." } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.post("https://front.rmz.gg/api/customer/logout", headers={ "Authorization": "Bearer 1|abc123xyz..." }) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->post("https://front.rmz.gg/api/customer/logout"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": null, "message": "Logged out successfully" } ``` After logging out, discard the Bearer token on the client side. The token is permanently revoked and cannot be reused. # Guest Cart & Cart Tokens Source: https://docs.rmz.gg/storefront-api/authentication/guest-cart How unauthenticated visitors can manage shopping carts using the X-Cart-Token header. Guest visitors can browse products and manage a shopping cart without authenticating. Cart state is tracked via a cart token passed in the `X-Cart-Token` header. ## How It Works 1. When a guest adds their first item to the cart, the API creates a cart and returns a `cart_token` in the response 2. Store this token on the client (e.g., in `localStorage` or a cookie) 3. Include it in all subsequent cart and checkout requests via the `X-Cart-Token` header 4. When the customer authenticates, the cart is associated with their account and a `cart_token` is returned in the auth response ## X-Cart-Token Header ``` X-Cart-Token: cart_abc123 ``` Include this header on all cart-related requests: * `GET /cart` * `POST /cart/add` * `PATCH /cart/items/{id}` * `DELETE /cart/items/{id}` * `DELETE /cart/clear` * `GET /cart/count` * `GET /cart/validate` * `GET /cart/summary` * `POST /cart/coupon` * `DELETE /cart/coupon` * `POST /checkout` ## Example: Guest Cart Flow ```javascript JavaScript theme={null} // 1. Add item to cart (first time, no token yet) const addResponse = await fetch("https://front.rmz.gg/api/cart/add", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ product_id: 101, qty: 1 }) }); const addData = await addResponse.json(); // 2. Store the cart token from the response const cartToken = addData.data.cart_token; localStorage.setItem("cart_token", cartToken); // 3. Use the token for subsequent requests const cartResponse = await fetch("https://front.rmz.gg/api/cart", { headers: { "X-Cart-Token": cartToken } }); const cart = await cartResponse.json(); ``` ```python Python theme={null} import requests # 1. Add item to cart add_response = requests.post("https://front.rmz.gg/api/cart/add", json={ "product_id": 101, "qty": 1 }) cart_token = add_response.json()["data"]["cart_token"] # 2. Use the token for subsequent requests cart_response = requests.get("https://front.rmz.gg/api/cart", headers={ "X-Cart-Token": cart_token }) cart = cart_response.json() ``` ```php PHP theme={null} // 1. Add item to cart $addResponse = Http::post("https://front.rmz.gg/api/cart/add", [ "product_id" => 101, "qty" => 1 ]); $cartToken = $addResponse->json()["data"]["cart_token"]; // 2. Use the token for subsequent requests $cartResponse = Http::withHeaders([ "X-Cart-Token" => $cartToken ])->get("https://front.rmz.gg/api/cart"); $cart = $cartResponse->json(); ``` ## Cart Token After Authentication When a customer authenticates via the OTP flow, the verify response includes a `cart_token`. Use this token for all subsequent cart operations alongside the `Authorization` header. ```json theme={null} { "success": true, "data": { "type": "authenticated", "token": "1|abc123xyz...", "cart_token": "cart_xyz789", "customer": { ... } } } ``` For authenticated checkout requests, include both headers: ```bash theme={null} curl -X POST "https://front.rmz.gg/api/checkout" \ -H "Authorization: Bearer 1|abc123xyz..." \ -H "X-Cart-Token: cart_xyz789" \ -H "Content-Type: application/json" ``` Always persist the cart token on the client side. If the token is lost, the guest cart cannot be recovered. ## Public Endpoints (No Auth Required) The following endpoints work without any authentication and do not require a cart token: * All `GET /store/*` endpoints * All `GET /products/*` endpoints * All `GET /categories/*` endpoints * All `GET /pages/*` endpoints * All `GET /components/*` endpoints * All `GET /reviews/*` endpoints * `GET /featured-products` # OTP Authentication Flow Source: https://docs.rmz.gg/storefront-api/authentication/otp-flow Authenticate customers using OTP verification via phone number or email. The Storefront API uses OTP-based authentication. Customers receive a 4-digit verification code via SMS or email, verify it, and receive a Bearer token for authenticated requests. ## Flow Overview 1. **Start** -- Send phone number or email to receive an OTP code 2. **Verify** -- Submit the OTP code to verify identity 3. **Complete** -- (New customers only) Provide name and email to finish registration *** ## POST /auth/start Start an authentication session. Auto-detects the type based on provided fields. ### Authentication None required (guest endpoint). ### Request #### Headers | Header | Value | Required | | ------------ | ---------------- | -------- | | Content-Type | application/json | Yes | #### Body Parameters | Parameter | Type | Required | Description | | ------------- | ------- | ---------------- | ----------------------------------------------- | | country\_code | numeric | Yes (phone auth) | Country dial code (1-999, e.g., `966`) | | phone | string | Yes (phone auth) | Phone number without country code (digits only) | | email | string | Yes (email auth) | Customer email address | Provide either `country_code` + `phone` for phone authentication, or `email` for email authentication. The API auto-detects which flow to use. #### Example Request ```bash cURL theme={null} curl -X POST "https://front.rmz.gg/api/auth/start" \ -H "Content-Type: application/json" \ -d '{ "country_code": "966", "phone": "501234567" }' ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/auth/start", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ country_code: "966", phone: "501234567" }) }); const data = await response.json(); ``` ```python Python theme={null} import requests response = requests.post("https://front.rmz.gg/api/auth/start", json={ "country_code": "966", "phone": "501234567" }) data = response.json() ``` ```php PHP theme={null} $response = Http::post("https://front.rmz.gg/api/auth/start", [ "country_code" => "966", "phone" => "501234567" ]); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "session_token": "auth_abc123xyz" }, "message": "Verification code sent successfully" } ``` #### Error Responses | Status | Message | Description | | ------ | ----------------------------------------------------------------- | ----------------------------------------- | | 422 | *(validation errors)* | Missing or invalid phone/email fields | | 429 | Too many authentication attempts today. Please try again tomorrow | 50 sessions/day per IP exceeded | | 429 | Too many authentication attempts for this phone number today | 10 sessions/day per phone number exceeded | | 500 | Store not found in context | Store could not be resolved | *** ## POST /auth/phone/start Dedicated phone authentication endpoint. Behaves identically to `POST /auth/start` with phone parameters but enforces phone-specific validation. ### Body Parameters | Parameter | Type | Required | Description | | ------------- | ------- | -------- | -------------------------- | | country\_code | numeric | Yes | Country dial code (1-999) | | phone | string | Yes | Phone number (digits only) | *** ## POST /auth/initiate Legacy authentication endpoint. Accepts a `type` field and nested `data` object instead of top-level fields. ### Authentication None required (guest endpoint). ### Body Parameters | Parameter | Type | Required | Description | | ------------ | ------ | ----------- | --------------------------------------------------------- | | type | string | Yes | Authentication type. Currently only `phone` is supported | | data.country | string | Yes (phone) | Country dial code (must be a supported Arab country code) | | data.phone | string | Yes (phone) | Phone number (6-12 digits) | | data.email | string | Yes (email) | Customer email address | ### Response Same as `POST /auth/start`. This is a legacy endpoint maintained for backward compatibility. New integrations should use `POST /auth/start` instead. *** ## POST /auth/verify Verify the OTP code sent to the customer's phone or email. ### Authentication None required (guest endpoint). ### Request #### Headers | Header | Value | Required | | ------------ | ---------------- | -------- | | Content-Type | application/json | Yes | #### Body Parameters | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ----------------------------------------- | | session\_token | string | No | Session token from the start step | | code | integer | Yes | The 4-digit OTP code | | otp | integer | No | Alias for `code` (backward compatibility) | #### Example Request ```bash cURL theme={null} curl -X POST "https://front.rmz.gg/api/auth/verify" \ -H "Content-Type: application/json" \ -d '{ "session_token": "auth_abc123xyz", "code": 1234 }' ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/auth/verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_token: "auth_abc123xyz", code: 1234 }) }); const data = await response.json(); ``` ```python Python theme={null} response = requests.post("https://front.rmz.gg/api/auth/verify", json={ "session_token": "auth_abc123xyz", "code": 1234 }) data = response.json() ``` ```php PHP theme={null} $response = Http::post("https://front.rmz.gg/api/auth/verify", [ "session_token" => "auth_abc123xyz", "code" => 1234 ]); $data = $response->json(); ``` ### Response #### Existing Customer (200) When the customer already exists in this store: ```json theme={null} { "success": true, "data": { "type": "authenticated", "token": "1|abc123xyz...", "cart_token": "cart_xyz789", "customer": { "id": 123, "first_name": "Ahmed", "last_name": "Ali", "email": "ahmed@example.com", "phone": "501234567", "country_code": "966" } }, "message": "Authentication successful" } ``` #### Customer From Another Store (200) When the customer exists in another store but not in this one, a new customer record is automatically created for this store: ```json theme={null} { "success": true, "data": { "type": "new_customer", "token": "1|abc123xyz...", "cart_token": "cart_xyz789", "customer": { "id": 456, "first_name": "Ahmed", "last_name": "Ali", "email": "ahmed@example.com", "phone": "501234567", "country_code": "966" } }, "message": "Authentication successful" } ``` The `type` field can be either `"authenticated"` (existing customer in this store) or `"new_customer"` (auto-created from another store). Both cases return a token and the customer can proceed immediately. #### New Customer (200) When the phone/email is not registered in any store, the response indicates registration is required: ```json theme={null} { "success": true, "data": { "type": "new", "requires_registration": true, "session_token": "auth_abc123xyz" }, "message": "Please complete your registration" } ``` When `requires_registration` is `true`, you must call `POST /auth/complete` to finish creating the customer account. #### Error Responses | Status | Message | Description | | ------ | --------------------------------------------------------------- | ------------------------------------------- | | 400 | Please restart the authentication process | Invalid or missing session | | 400 | Verification code expired. Please restart the process | Session has expired | | 400 | Too many failed attempts. Please restart the process | Max verification attempts reached | | 400 | Invalid verification code | Wrong OTP code | | 429 | Too many verification attempts. Please wait before trying again | Rate limit exceeded (5 attempts/min per IP) | | 429 | Please wait seconds before trying again | Progressive cooldown active | *** ## POST /auth/phone/verify Dedicated phone verification endpoint. Behaves identically to `POST /auth/verify`. *** ## POST /auth/resend Resend the OTP code for an active session. ### Authentication None required (guest endpoint). ### Request #### Body Parameters | Parameter | Type | Required | Description | | -------------- | ------ | -------- | --------------------------------- | | session\_token | string | Yes | Session token from the start step | #### Example Request ```bash cURL theme={null} curl -X POST "https://front.rmz.gg/api/auth/resend" \ -H "Content-Type: application/json" \ -d '{ "session_token": "auth_abc123xyz" }' ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/auth/resend", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_token: "auth_abc123xyz" }) }); ``` ```python Python theme={null} response = requests.post("https://front.rmz.gg/api/auth/resend", json={ "session_token": "auth_abc123xyz" }) ``` ```php PHP theme={null} $response = Http::post("https://front.rmz.gg/api/auth/resend", [ "session_token" => "auth_abc123xyz" ]); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "session_token": "auth_abc123xyz" }, "message": "Verification code sent successfully" } ``` #### Error Responses | Status | Message | Description | | ------ | ---------------------------------------------------------- | ------------------------------------ | | 400 | Invalid session. Please restart the authentication process | Session token not found | | 400 | Session expired. Please restart the authentication process | Session has expired | | 429 | Please wait 30 seconds before requesting a new code | Must wait 30 seconds between resends | | 429 | Too many resend attempts. Please wait before trying again | 3 resends per 10 minutes exceeded | *** ## POST /auth/phone/resend Dedicated phone resend endpoint. Behaves identically to `POST /auth/resend`. *** ## POST /auth/complete Complete registration for new customers after OTP verification. ### Authentication None required (guest endpoint). Requires a verified session token. ### Request #### Body Parameters | Parameter | Type | Required | Description | | -------------- | ------ | -------- | ----------------------------------------- | | session\_token | string | Yes | Verified session token | | email | string | Yes | Customer email (must be unique per store) | | firstName | string | Yes | Customer first name | | lastName | string | Yes | Customer last name | #### Example Request ```bash cURL theme={null} curl -X POST "https://front.rmz.gg/api/auth/complete" \ -H "Content-Type: application/json" \ -d '{ "session_token": "auth_abc123xyz", "email": "ahmed@example.com", "firstName": "Ahmed", "lastName": "Ali" }' ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/auth/complete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ session_token: "auth_abc123xyz", email: "ahmed@example.com", firstName: "Ahmed", lastName: "Ali" }) }); const data = await response.json(); ``` ```python Python theme={null} response = requests.post("https://front.rmz.gg/api/auth/complete", json={ "session_token": "auth_abc123xyz", "email": "ahmed@example.com", "firstName": "Ahmed", "lastName": "Ali" }) data = response.json() ``` ```php PHP theme={null} $response = Http::post("https://front.rmz.gg/api/auth/complete", [ "session_token" => "auth_abc123xyz", "email" => "ahmed@example.com", "firstName" => "Ahmed", "lastName" => "Ali" ]); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "type": "registered", "token": "1|abc123xyz...", "customer": { "id": 456, "first_name": "Ahmed", "last_name": "Ali", "email": "ahmed@example.com", "phone": "501234567", "country_code": "966" } }, "message": "Account created and authenticated successfully" } ``` #### Error Responses | Status | Message | Description | | ------ | --------------------------------------------------------------- | ----------------------------------------------------------------- | | 400 | Please restart the authentication process | Session not found, not verified, or not eligible for registration | | 400 | Session expired. Please restart the process | Session has expired | | 400 | Customer already exists. Please login with existing credentials | Customer record already exists for this store | | 422 | *(validation errors)* | Email already in use in this store, or missing required fields | *** ## Rate Limits Summary | Action | Limit | Window | | -------------------- | ------------------- | ---------- | | Start authentication | 50 per IP | 24 hours | | Phone-specific start | 10 per phone number | 24 hours | | Verify OTP | 5 per IP | 1 minute | | Resend OTP | 3 per identifier | 10 minutes | | Resend cooldown | 1 per session | 30 seconds | ## Session Expiry OTP sessions expire after **5 minutes**. Expired sessions are automatically cleaned up. If a session expires, the customer must restart the authentication flow from `POST /auth/start`. # Coupons Source: https://docs.rmz.gg/storefront-api/cart/coupons Apply and remove coupon codes from the shopping cart. ## POST /cart/coupon Apply a coupon code to the cart. ### Authentication Optional. Use `X-Cart-Token` for guest carts. ### Headers | Header | Value | Required | | ------------ | ---------------- | -------- | | X-Cart-Token | Yes (guest) | | | Content-Type | application/json | Yes | ### Body Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | -------------------- | | coupon | string | Yes | Coupon code to apply | ### Example Request ```bash cURL theme={null} curl -X POST "https://front.rmz.gg/api/cart/coupon" \ -H "Content-Type: application/json" \ -H "X-Cart-Token: cart_abc123" \ -d '{"coupon": "SAVE10"}' ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/cart/coupon", { method: "POST", headers: { "Content-Type": "application/json", "X-Cart-Token": "cart_abc123" }, body: JSON.stringify({ coupon: "SAVE10" }) }); const data = await response.json(); ``` ```python Python theme={null} response = requests.post("https://front.rmz.gg/api/cart/coupon", headers={"X-Cart-Token": "cart_abc123"}, json={"coupon": "SAVE10"} ) data = response.json() ``` ```php PHP theme={null} $response = Http::withHeaders(["X-Cart-Token" => "cart_abc123"]) ->post("https://front.rmz.gg/api/cart/coupon", ["coupon" => "SAVE10"]); $data = $response->json(); ``` ### Response #### Success (200) Returns the full cart contents with the discount applied. ```json theme={null} { "success": true, "data": { "cart_token": "cart_abc123", "items": [...], "count": 2, "subtotal": 399.98, "discount_amount": 40.00, "total": 359.98, "total_before_tax": 359.98, "coupon": { ... }, "currency": "SAR", "tax": { "enabled": false, "rate": 0, "rate_formatted": "0%", "amount": 0, "amount_formatted": "0.00 ر.س", "country_code": null, "country_name": null } }, "message": "Coupon applied successfully" } ``` #### Error Responses | Status | Description | | ------ | ----------------------------------------------------------------------------------------------------------- | | 400 | Invalid coupon, expired, usage limit reached, minimum order not met, or coupon not applicable to cart items | | 422 | Validation error (missing coupon code) | *** ## DELETE /cart/coupon Remove the applied coupon from the cart. ### Authentication Optional. Use `X-Cart-Token` for guest carts. ### Headers | Header | Value | Required | | ------------ | ----------- | -------- | | X-Cart-Token | Yes (guest) | | ### Example Request ```bash cURL theme={null} curl -X DELETE "https://front.rmz.gg/api/cart/coupon" \ -H "X-Cart-Token: cart_abc123" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/cart/coupon", { method: "DELETE", headers: { "X-Cart-Token": "cart_abc123" } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.delete("https://front.rmz.gg/api/cart/coupon", headers={ "X-Cart-Token": "cart_abc123" }) data = response.json() ``` ```php PHP theme={null} $response = Http::withHeaders(["X-Cart-Token" => "cart_abc123"]) ->delete("https://front.rmz.gg/api/cart/coupon"); $data = $response->json(); ``` ### Response #### Success (200) Returns the full cart contents with the discount removed. ```json theme={null} { "success": true, "data": { "cart_token": "cart_abc123", "items": [...], "count": 2, "subtotal": 399.98, "discount_amount": 0, "total": 399.98, "total_before_tax": 399.98, "coupon": null, "currency": "SAR", "tax": { "enabled": false, "rate": 0, "rate_formatted": "0%", "amount": 0, "amount_formatted": "0.00 ر.س", "country_code": null, "country_name": null } }, "message": "Coupon removed successfully" } ``` Coupons are re-validated at checkout time. If a coupon becomes invalid between cart and checkout (e.g., it expires or reaches its usage limit), the checkout will fail with a message indicating the coupon was removed. # Manage Cart Source: https://docs.rmz.gg/storefront-api/cart/manage-cart Add, update, remove, and validate cart items for guest and authenticated customers. All cart endpoints accept the `X-Cart-Token` header for guest carts. Authenticated customers should also include their `Authorization: Bearer` token. *** ## GET /cart Get the current cart contents. ### Authentication Optional. Use `X-Cart-Token` for guest carts or `Authorization: Bearer` for authenticated customers. ### Headers | Header | Value | Required | | ------------- | ----------- | -------- | | X-Cart-Token | Yes (guest) | | | Authorization | Bearer | No | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/cart" \ -H "X-Cart-Token: cart_abc123" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/cart", { headers: { "X-Cart-Token": "cart_abc123" } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/cart", headers={ "X-Cart-Token": "cart_abc123" }) data = response.json() ``` ```php PHP theme={null} $response = Http::withHeaders(["X-Cart-Token" => "cart_abc123"])->get("https://front.rmz.gg/api/cart"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "cart_token": "cart_abc123", "items": [ { "id": 1, "product_id": 101, "name": "Premium Game Key", "slug": "premium-game-key", "image": { "url": "https://...", "full_link": "https://...", "alt_text": "Premium Game Key" }, "quantity": 2, "unit_price": 199.99, "total_price": 399.98, "pricing": { "base_price": 199.99, "addons_price": 0, "subscription_price": 0, "unit_total": 199.99, "formatted": { "base_price": "199.99 ر.س", "addons_price": null, "subscription_price": null, "unit_total": "199.99 ر.س", "total_price": "399.98 ر.س" } }, "custom_fields": [], "subscription_plan": null, "notice": null, "product": { "id": 101, "name": "Premium Game Key", "slug": "premium-game-key", "type": "code", "fields": null, "price": { "formatted": "199.99 ر.س" }, "image": { "url": "https://...", "full_link": "https://...", "alt_text": "Premium Game Key" } } } ], "count": 2, "subtotal": 399.98, "discount_amount": 0, "total": 399.98, "total_before_tax": 399.98, "coupon": null, "currency": "SAR", "tax": { "enabled": false, "rate": 0, "rate_formatted": "0%", "amount": 0, "amount_formatted": "0.00 ر.س", "country_code": null, "country_name": null } } } ``` *** ## POST /cart/add Add a product to the cart. ### Authentication Optional. Use `X-Cart-Token` for guest carts. ### Headers | Header | Value | Required | | ------------ | ---------------------- | -------- | | X-Cart-Token | No (created if absent) | | | Content-Type | application/json | Yes | ### Body Parameters | Parameter | Type | Required | Description | | ------------------ | ------- | -------- | -------------------------------------------------------------------- | | product\_id | integer | Yes | Product ID (must belong to the current store) | | qty | numeric | Yes | Quantity to add (minimum 1) | | fields | array | No | Custom field values (key-value pairs matching product fields) | | subscription\_plan | integer | No | Subscription variant ID (required for subscription/license products) | | notice | string | No | Customer note for this item (max 800 characters) | ### Example Request ```bash cURL theme={null} curl -X POST "https://front.rmz.gg/api/cart/add" \ -H "Content-Type: application/json" \ -H "X-Cart-Token: cart_abc123" \ -d '{ "product_id": 101, "qty": 2, "fields": { "platform": "PC" }, "notice": "Please include receipt" }' ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/cart/add", { method: "POST", headers: { "Content-Type": "application/json", "X-Cart-Token": "cart_abc123" }, body: JSON.stringify({ product_id: 101, qty: 2, fields: { platform: "PC" }, notice: "Please include receipt" }) }); const data = await response.json(); ``` ```python Python theme={null} response = requests.post("https://front.rmz.gg/api/cart/add", headers={"X-Cart-Token": "cart_abc123"}, json={ "product_id": 101, "qty": 2, "fields": {"platform": "PC"}, "notice": "Please include receipt" } ) data = response.json() ``` ```php PHP theme={null} $response = Http::withHeaders(["X-Cart-Token" => "cart_abc123"]) ->post("https://front.rmz.gg/api/cart/add", [ "product_id" => 101, "qty" => 2, "fields" => ["platform" => "PC"], "notice" => "Please include receipt" ]); $data = $response->json(); ``` ### Response #### Success (200) Returns the full cart contents (same format as `GET /cart`). ```json theme={null} { "success": true, "data": { ... }, "message": "تم إضافة المنتج بنجاح إلى السلة" } ``` #### Error Responses | Status | Description | | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | | 400 | Out of stock, product unavailable, required fields missing, subscription already in cart, below minimum quantity, or subscription plan not selected | | 422 | Validation error (invalid product\_id, missing qty) | Subscription and license products can only have a quantity of 1 in the cart. Adding a subscription product that is already in the cart will return an error. *** ## PATCH /cart/items/ Update the quantity of a cart item. ### Authentication Optional. Use `X-Cart-Token` for guest carts. ### Path Parameters | Parameter | Type | Description | | --------- | ------- | --------------------------- | | id | integer | Product ID of the cart item | ### Headers | Header | Value | Required | | ------------ | ---------------- | -------- | | X-Cart-Token | Yes (guest) | | | Content-Type | application/json | Yes | ### Body Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | --------------------------------- | | quantity | integer | Yes | New quantity (set to 0 to remove) | ### Example Request ```bash cURL theme={null} curl -X PATCH "https://front.rmz.gg/api/cart/items/101" \ -H "Content-Type: application/json" \ -H "X-Cart-Token: cart_abc123" \ -d '{"quantity": 3}' ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/cart/items/101", { method: "PATCH", headers: { "Content-Type": "application/json", "X-Cart-Token": "cart_abc123" }, body: JSON.stringify({ quantity: 3 }) }); ``` ```python Python theme={null} response = requests.patch("https://front.rmz.gg/api/cart/items/101", headers={"X-Cart-Token": "cart_abc123"}, json={"quantity": 3} ) ``` ```php PHP theme={null} $response = Http::withHeaders(["X-Cart-Token" => "cart_abc123"]) ->patch("https://front.rmz.gg/api/cart/items/101", ["quantity" => 3]); ``` ### Response #### Success (200) Returns the full cart contents. ```json theme={null} { "success": true, "data": { ... }, "message": "Cart updated successfully" } ``` #### Error Responses | Status | Description | | ------ | ---------------------------------------------------------------------------- | | 400 | Below minimum quantity, exceeds stock, or subscription max quantity exceeded | | 404 | Product not found | *** ## DELETE /cart/items/ Remove a specific item from the cart. ### Path Parameters | Parameter | Type | Description | | --------- | ------- | ------------------------------------- | | id | integer | Product ID of the cart item to remove | ### Example Request ```bash cURL theme={null} curl -X DELETE "https://front.rmz.gg/api/cart/items/101" \ -H "X-Cart-Token: cart_abc123" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/cart/items/101", { method: "DELETE", headers: { "X-Cart-Token": "cart_abc123" } }); ``` ```python Python theme={null} response = requests.delete("https://front.rmz.gg/api/cart/items/101", headers={ "X-Cart-Token": "cart_abc123" }) ``` ```php PHP theme={null} $response = Http::withHeaders(["X-Cart-Token" => "cart_abc123"]) ->delete("https://front.rmz.gg/api/cart/items/101"); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { ... }, "message": "Product removed from cart successfully" } ``` *** ## DELETE /cart/clear Remove all items from the cart. ### Example Request ```bash cURL theme={null} curl -X DELETE "https://front.rmz.gg/api/cart/clear" \ -H "X-Cart-Token: cart_abc123" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/cart/clear", { method: "DELETE", headers: { "X-Cart-Token": "cart_abc123" } }); ``` ```python Python theme={null} response = requests.delete("https://front.rmz.gg/api/cart/clear", headers={ "X-Cart-Token": "cart_abc123" }) ``` ```php PHP theme={null} $response = Http::withHeaders(["X-Cart-Token" => "cart_abc123"]) ->delete("https://front.rmz.gg/api/cart/clear"); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { ... }, "message": "Cart cleared successfully" } ``` *** ## GET /cart/count Get the number of items in the cart. ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/cart/count" \ -H "X-Cart-Token: cart_abc123" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/cart/count", { headers: { "X-Cart-Token": "cart_abc123" } }); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/cart/count", headers={ "X-Cart-Token": "cart_abc123" }) ``` ```php PHP theme={null} $response = Http::withHeaders(["X-Cart-Token" => "cart_abc123"]) ->get("https://front.rmz.gg/api/cart/count"); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "count": 3, "cart_token": "cart_abc123" } } ``` *** ## GET /cart/validate Validate the cart before proceeding to checkout. Checks stock availability, product status, and other constraints. ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/cart/validate" \ -H "X-Cart-Token: cart_abc123" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/cart/validate", { headers: { "X-Cart-Token": "cart_abc123" } }); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/cart/validate", headers={ "X-Cart-Token": "cart_abc123" }) ``` ```php PHP theme={null} $response = Http::withHeaders(["X-Cart-Token" => "cart_abc123"]) ->get("https://front.rmz.gg/api/cart/validate"); ``` ### Response #### Valid Cart (200) ```json theme={null} { "success": true, "data": { ... }, "message": "Cart is valid" } ``` #### Invalid Cart (400) ```json theme={null} { "success": false, "message": "Cart validation failed", "data": { "errors": [ "Product 'Game Key' is out of stock", "Product 'Software License' has been removed" ] } } ``` *** ## GET /cart/summary Get a cart summary including available payment methods and shipping information. Use this before displaying the checkout page. ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/cart/summary" \ -H "X-Cart-Token: cart_abc123" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/cart/summary", { headers: { "X-Cart-Token": "cart_abc123" } }); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/cart/summary", headers={ "X-Cart-Token": "cart_abc123" }) ``` ```php PHP theme={null} $response = Http::withHeaders(["X-Cart-Token" => "cart_abc123"]) ->get("https://front.rmz.gg/api/cart/summary"); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "cart_token": "cart_abc123", "items": [...], "count": 2, "subtotal": 399.98, "discount_amount": 0, "total": 399.98, "total_before_tax": 399.98, "coupon": null, "currency": "SAR", "tax": { "enabled": false, "rate": 0, "rate_formatted": "0%", "amount": 0, "amount_formatted": "0.00 ر.س", "country_code": null, "country_name": null }, "payment_methods": { ... }, "shipping": { "required": false, "cost": 0 } } } ``` RMZ is a digital products platform. Shipping is always `required: false` with `cost: 0`. # Categories Source: https://docs.rmz.gg/storefront-api/categories Browse store categories and their products. ## GET /categories List all active categories for the store, ordered by sort index. ### Authentication None required. ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/categories" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/categories"); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/categories") data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/categories"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": [ { "id": 1, "name": "Games", "slug": "games", "description": null, "image": null, "icon": null, "is_active": true, "sort_order": 0, "products_count": 24, "created_at": "2024-01-01T00:00:00.000000Z", "updated_at": "2024-06-15T10:30:00.000000Z" }, { "id": 2, "name": "Software", "slug": "software", "description": null, "image": null, "icon": null, "is_active": true, "sort_order": 1, "products_count": 12, "created_at": "2024-01-01T00:00:00.000000Z", "updated_at": "2024-06-15T10:30:00.000000Z" } ] } ``` *** ## GET /categories/ Get a single category by slug, including product count from child categories. ### Authentication None required. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ----------------- | | slug | string | Category URL slug | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/categories/games" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/categories/games"); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/categories/games") data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/categories/games"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "id": 1, "name": "Games", "slug": "games", "description": null, "image": null, "icon": null, "is_active": true, "sort_order": 0, "products_count": 36, "created_at": "2024-01-01T00:00:00.000000Z", "updated_at": "2024-06-15T10:30:00.000000Z" } } ``` The `products_count` includes products from both the parent category and all its active child categories. #### Error Responses | Status | Description | | ------ | ------------------------------ | | 404 | Category not found or inactive | *** ## GET /categories//products Get products within a category. Includes products from child categories of the specified parent. ### Authentication None required. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ----------------- | | slug | string | Category URL slug | ### Query Parameters | Parameter | Type | Required | Description | | ---------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------- | | search | string | No | Search within category products (max 255 chars) | | sort | string | No | Sort order: `price_asc`, `price_desc`, `name_asc`, `name_desc`, `created_asc`, `created_desc`. Default: newest first | | per\_page | integer | No | Items per page (1-50). Default: `19` | | type | string | No | Filter by type: `digital`, `subscription`, `course` | | price\_min | number | No | Minimum price filter | | price\_max | number | No | Maximum price filter | | in\_stock | boolean | No | Filter to only in-stock products | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/categories/games/products?sort=price_asc&per_page=12" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/categories/games/products?sort=price_asc&per_page=12"); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/categories/games/products", params={ "sort": "price_asc", "per_page": 12 }) data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/categories/games/products", [ "sort" => "price_asc", "per_page" => 12 ]); $data = $response->json(); ``` ### Response #### Success (200) Products use the same `ProductResource` structure as `GET /products`. See [List Products](/storefront-api/products/list-products) for the full response schema. ```json theme={null} { "success": true, "data": [ { "id": 101, "name": "Premium Game Key", "marketing_title": null, "slug": "premium-game-key", "description": "Premium game activation key for PC", "short_description": "Premium game activation key for PC...", "type": "code", "status": 1, "is_featured": false, "is_noticeable": false, "is_new": false, "is_discounted": false, "is_discount_expired": false, "show_reviews": true, "price": { "original": 199.99, "actual": 199.99, "discount": null, "discount_expiry": null, "show_discount_countdown": false, "show_discount_savings": false, "savings_amount": 0, "formatted": "199.99 ر.س", "formatted_original": "199.99 ر.س", "discount_percentage": 0, "currency": "SAR" }, "stock": { "available": 50, "unlimited": false, "min_qty": 1, "codes_count": 50, "is_in_stock": true }, "sales": { "badge": null }, "image": { "id": 1, "url": "https://...", "full_link": "https://...", "path": "...", "filename": "image.webp", "alt_text": "Premium Game Key" }, "categories": [], "fields": null, "meta": null, "metadata": null, "tags": null, "seo": { "meta_title": null, "meta_description": null, "meta_keywords": null }, "created_at": "2024-06-15T10:30:00.000000Z", "updated_at": "2024-06-15T10:30:00.000000Z" } ], "pagination": { "current_page": 1, "last_page": 3, "per_page": 19, "total": 36, "from": 1, "to": 19, "has_more_pages": true, "next_page_url": "...", "prev_page_url": null } } ``` #### Error Responses | Status | Description | | ------ | ------------------------------------ | | 404 | Category not found or inactive | | 422 | Validation error on query parameters | # Checkout Source: https://docs.rmz.gg/storefront-api/checkout Create checkout sessions and retrieve payment results. ## POST /checkout Create a checkout session from the current cart. The cart is validated and cleared upon successful checkout creation. ### Authentication Requires Bearer token (`auth:customer_api`) and `X-Cart-Token`. ### Headers | Header | Value | Required | | ------------- | ---------------- | -------- | | Authorization | Bearer | Yes | | X-Cart-Token | Yes | | | Content-Type | application/json | Yes | ### Body Parameters | Parameter | Type | Required | Description | | -------------- | ------ | -------- | ------------------------------- | | customer\_note | string | No | Optional note from the customer | ### Example Request ```bash cURL theme={null} curl -X POST "https://front.rmz.gg/api/checkout" \ -H "Authorization: Bearer 1|abc123xyz..." \ -H "X-Cart-Token: cart_abc123" \ -H "Content-Type: application/json" \ -d '{"customer_note": "Please deliver after 5 PM"}' ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/checkout", { method: "POST", headers: { "Authorization": "Bearer 1|abc123xyz...", "X-Cart-Token": "cart_abc123", "Content-Type": "application/json" }, body: JSON.stringify({ customer_note: "Please deliver after 5 PM" }) }); const data = await response.json(); ``` ```python Python theme={null} response = requests.post("https://front.rmz.gg/api/checkout", headers={ "Authorization": "Bearer 1|abc123xyz...", "X-Cart-Token": "cart_abc123" }, json={"customer_note": "Please deliver after 5 PM"} ) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...") ->withHeaders(["X-Cart-Token" => "cart_abc123"]) ->post("https://front.rmz.gg/api/checkout", [ "customer_note" => "Please deliver after 5 PM" ]); $data = $response->json(); ``` ### Response #### Payment Required (200) When the order total is greater than zero, a payment URL is returned: ```json theme={null} { "success": true, "data": { "type": "payment_required", "checkout_id": 12345, "checkout_url": "chk_abc123", "amount": 399.98, "redirect_url": "https://store.rmz.gg/checkout/chk_abc123" }, "message": "Checkout session created" } ``` Redirect the customer to `redirect_url` to complete the payment. #### Free Order (200) When the order total is zero (e.g., 100% discount coupon), the order is created immediately: ```json theme={null} { "success": true, "data": { "type": "free_order", "order_id": 78901, "redirect_url": null }, "message": "Order completed successfully" } ``` #### Error Responses | Status | Description | | ------ | -------------------------------------------------------------------------- | | 400 | Cart is empty, validation failed, coupon invalid, or total amount mismatch | | 401 | Not authenticated | | 500 | Checkout processing error | The cart is cleared after a successful checkout. If the customer needs to modify their order, they must add items to the cart again. The checkout includes a server-side total recalculation as a security measure. If the cart total doesn't match the recalculated amount (e.g., due to cart manipulation), the checkout will fail. *** ## GET /checkout//result Get the result of a checkout after payment processing. Use the `checkout_url` value (not the numeric ID) as the path parameter. ### Authentication Requires Bearer token (`auth:customer_api`). ### Path Parameters | Parameter | Type | Description | | --------- | ------ | -------------------------------------------- | | id | string | Checkout URL identifier (e.g., `chk_abc123`) | ### Headers | Header | Value | Required | | ------------- | ------- | -------- | | Authorization | Bearer | Yes | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/checkout/chk_abc123/result" \ -H "Authorization: Bearer 1|abc123xyz..." ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/checkout/chk_abc123/result", { headers: { "Authorization": "Bearer 1|abc123xyz..." } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/checkout/chk_abc123/result", headers={ "Authorization": "Bearer 1|abc123xyz..." }) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...") ->get("https://front.rmz.gg/api/checkout/chk_abc123/result"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "checkout": { "id": 12345, "amount": 399.98, "status": "Completed", "payment_method": "card", "payment_id": "ch_abc123", "created_at": "2024-06-15T14:30:00.000000Z" }, "order": { "id": 78901, "total": 399.98, "status": "Completed", "created_at": "2024-06-15T14:30:00.000000Z" } }, "message": "Checkout result retrieved" } ``` #### Error Responses | Status | Description | | ------ | -------------------------------------------------------------------------------------- | | 401 | Not authenticated | | 404 | Checkout not found, order not yet created, or checkout belongs to a different customer | Poll this endpoint after redirecting the customer back from the payment page to check whether the payment was successful and the order was created. # Homepage Components Source: https://docs.rmz.gg/storefront-api/components Retrieve configurable homepage components including banners, product lists, features, and reviews. Components are the building blocks of a store's homepage. Store owners configure them in the dashboard. Component types include banners, product lists, features, reviews, and custom content. *** ## GET /components Get all homepage components for the store, sorted by display order. ### Authentication None required. ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/components" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/components"); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/components") data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/components"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": [ { "id": 1, "type": "banner", "name": "Main Banner", "data": { "type": "carousel", "images": [ { "id": 1, "url": "https://...", "full_link": "https://...", "alt_text": "Summer Sale", "link_url": "/categories/summer-deals", "sort_order": 0 } ], "settings": {} }, "sort_order": 1, "settings": {} }, { "id": 2, "type": "product-list", "name": "Best Sellers", "data": { "title": "Best Sellers", "products": [ { "id": 101, "name": "Premium Game Key", "slug": "premium-game-key", "price": 199.99, "image": { "url": "https://..." } } ], "view_all_link": "/categories/games", "settings": {}, "options": {} }, "sort_order": 2, "settings": {} }, { "id": 3, "type": "feature", "name": "Store Features", "data": { "features": [ { "title": "Instant Delivery", "description": "Digital products delivered instantly", "icon": "bolt" } ], "settings": {} }, "sort_order": 3, "settings": {} }, { "id": 4, "type": "reviews", "name": "Customer Reviews", "data": { "reviews": [ { "id": 1, "rating": 5, "comment": "Great store!", "reviewer": { "name": "Ahmed Ali" }, "created_at": "2024-06-15" } ], "settings": {} }, "sort_order": 4, "settings": {} } ] } ``` ### Component Types | Type | Description | | -------------- | ---------------------------------------------------------------------------------------------------- | | `banner` | Image banner (single or carousel). Contains `images` array with URLs and links. | | `product-list` | Curated product list. Supports best sellers, latest, category-based, and manually selected products. | | `feature` | Store feature highlights with title, description, and optional icon. | | `reviews` | Customer reviews section showing published store reviews. | | `custom` | Custom content block with arbitrary HTML content. | *** ## GET /components/ Get a specific component by ID. ### Authentication None required. ### Path Parameters | Parameter | Type | Description | | --------- | ------- | ------------ | | id | integer | Component ID | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/components/2" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/components/2"); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/components/2") data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/components/2"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "id": 2, "type": "best_sellers", "name": "Best Sellers", "data": { "title": "Best Sellers", "products": [...], "view_all_link": "/categories/games", "settings": {}, "options": {} }, "sort_order": 2, "settings": {} } } ``` The `type` field in the single-component response returns the raw component type (e.g., `best_sellers`, `moving_group`, `latest`) rather than the mapped type used in the list endpoint (`product-list`, `banner`, etc.). #### Error Responses | Status | Description | | ------ | ------------------- | | 404 | Component not found | *** ## GET /components//products Get paginated products for a product-list component. ### Authentication None required. ### Path Parameters | Parameter | Type | Description | | --------- | ------- | -------------------------------------------- | | id | integer | Component ID (must be a `product-list` type) | ### Query Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | --------------------------------------- | | per\_page | integer | No | Products per page (1-50). Default: `12` | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/components/2/products?per_page=12" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/components/2/products?per_page=12"); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/components/2/products", params={"per_page": 12}) data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/components/2/products", ["per_page" => 12]); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": [ { "id": 101, "name": "Premium Game Key", "slug": "premium-game-key", "type": "code", "price": 199.99, "image": { "url": "https://..." }, "categories": [] } ], "pagination": { "current_page": 1, "last_page": 2, "per_page": 12, "total": 20, "from": 1, "to": 12, "has_more_pages": true, "next_page_url": "...", "prev_page_url": null } } ``` #### Error Responses | Status | Description | | ------ | ------------------------------------ | | 400 | Component is not a product-list type | | 404 | Component not found | # Courses Source: https://docs.rmz.gg/storefront-api/courses Access enrolled courses, track progress, view module content, and mark modules as completed. All course endpoints require customer authentication. Customers can only access courses they have purchased. *** ## GET /courses List the authenticated customer's enrolled courses. ### Authentication Requires Bearer token (`auth:customer_api`). ### Headers | Header | Value | Required | | ------------- | ------- | -------- | | Authorization | Bearer | Yes | ### Query Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | -------------------------------------------------- | | status | string | No | Filter by status: `active`, `completed`, `expired` | | per\_page | integer | No | Items per page (1-50). Default: `10` | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/courses?status=active&per_page=10" \ -H "Authorization: Bearer 1|abc123xyz..." ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/courses?status=active", { headers: { "Authorization": "Bearer 1|abc123xyz..." } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/courses", headers={"Authorization": "Bearer 1|abc123xyz..."}, params={"status": "active"} ) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->get("https://front.rmz.gg/api/courses", [ "status" => "active" ]); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": [ { "id": 1, "progress": 45.5, "status": "active", "enrolled_at": "2024-06-01 00:00:00", "completed_at": null, "expires_at": null, "last_accessed_at": "2024-06-10 14:30:00", "certificate_url": null, "is_expired": false, "course": { "id": 5, "name": "Web Development Fundamentals", "description": "Learn the basics of web development...", "short_description": "Learn the basics of web development...", "slug": "web-development-fundamentals", "instructor": "Ahmed Ali", "level": "beginner", "image": null, "total_modules": 10, "estimated_duration": 0, "difficulty_level": "beginner", "sequential_modules": false, "sections": [ { "id": 1, "title": "Getting Started", "description": "Introduction to web development", "sort_index": 0, "modules": [ { "id": 1, "title": "Introduction to HTML", "description": "Learn the basics of HTML", "content": "

HTML Basics

...", "video_url": null, "duration_minutes": 30, "sort_index": 0, "type": "text" } ] } ] }, "completed_modules": [1, 2, 3], "completed_modules_count": 3, "is_completed": false, "is_active": true, "can_access": true, "created_at": "2024-06-01 00:00:00", "updated_at": "2024-06-10 14:30:00" } ], "pagination": { "current_page": 1, "last_page": 1, "per_page": 10, "total": 2, "from": 1, "to": 2, "has_more_pages": false, "next_page_url": null, "prev_page_url": null } } ``` *** ## GET /courses/ Get details of a specific course enrollment, including module listing. ### Authentication Requires Bearer token (`auth:customer_api`). ### Path Parameters | Parameter | Type | Description | | --------- | ------- | -------------------- | | id | integer | Course enrollment ID | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/courses/1" \ -H "Authorization: Bearer 1|abc123xyz..." ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/courses/1", { headers: { "Authorization": "Bearer 1|abc123xyz..." } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/courses/1", headers={ "Authorization": "Bearer 1|abc123xyz..." }) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->get("https://front.rmz.gg/api/courses/1"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "id": 1, "progress": 45.5, "status": "active", "enrolled_at": "2024-06-01 00:00:00", "completed_at": null, "expires_at": null, "last_accessed_at": "2024-06-10 14:30:00", "certificate_url": null, "is_expired": false, "course": { "id": 5, "name": "Web Development Fundamentals", "description": "Learn the basics of web development...", "short_description": "Learn the basics of web development...", "slug": "web-development-fundamentals", "instructor": "Ahmed Ali", "level": "beginner", "image": null, "total_modules": 10, "estimated_duration": 0, "difficulty_level": "beginner", "sequential_modules": false, "sections": [ { "id": 1, "title": "Getting Started", "description": "Introduction to web development", "sort_index": 0, "modules": [ { "id": 1, "title": "Introduction to HTML", "description": "Learn the basics of HTML", "content": "

HTML Basics

...", "video_url": null, "duration_minutes": 30, "sort_index": 0, "type": "text" }, { "id": 2, "title": "CSS Basics", "description": "Learn the fundamentals of CSS styling", "content": "

CSS Basics

...", "video_url": null, "duration_minutes": 45, "sort_index": 1, "type": "text" } ] } ] }, "completed_modules": [1, 2, 3], "completed_modules_count": 3, "is_completed": false, "is_active": true, "can_access": true, "created_at": "2024-06-01 00:00:00", "updated_at": "2024-06-10 14:30:00" } } ``` #### Error Responses | Status | Description | | ------ | -------------------- | | 401 | Not authenticated | | 404 | Enrollment not found | *** ## GET /courses//progress Get detailed progress for a course enrollment, including per-module completion status and estimated remaining time. ### Authentication Requires Bearer token (`auth:customer_api`). ### Path Parameters | Parameter | Type | Description | | --------- | ------- | -------------------- | | id | integer | Course enrollment ID | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/courses/1/progress" \ -H "Authorization: Bearer 1|abc123xyz..." ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/courses/1/progress", { headers: { "Authorization": "Bearer 1|abc123xyz..." } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/courses/1/progress", headers={ "Authorization": "Bearer 1|abc123xyz..." }) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->get("https://front.rmz.gg/api/courses/1/progress"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "overall_progress": 50.0, "total_modules": 10, "completed_modules": 5, "estimated_remaining_time": 150, "modules": [ { "id": 1, "title": "Introduction to HTML", "sort_order": 1, "is_completed": true, "duration_minutes": 30 }, { "id": 2, "title": "CSS Basics", "sort_order": 2, "is_completed": false, "duration_minutes": 45 } ] } } ``` The `estimated_remaining_time` is in minutes and is calculated from the `duration_minutes` of incomplete modules. #### Error Responses | Status | Description | | ------ | -------------------- | | 401 | Not authenticated | | 404 | Enrollment not found | *** ## GET /courses//modules/ Get the full content of a specific course module. ### Authentication Requires Bearer token (`auth:customer_api`). ### Path Parameters | Parameter | Type | Description | | --------- | ------- | -------------------- | | courseId | integer | Course enrollment ID | | moduleId | integer | Module ID | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/courses/1/modules/2" \ -H "Authorization: Bearer 1|abc123xyz..." ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/courses/1/modules/2", { headers: { "Authorization": "Bearer 1|abc123xyz..." } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/courses/1/modules/2", headers={ "Authorization": "Bearer 1|abc123xyz..." }) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->get("https://front.rmz.gg/api/courses/1/modules/2"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "id": 2, "title": "CSS Basics", "description": "Learn the fundamentals of CSS styling", "content": "

CSS Basics

In this module...

", "content_type": "html", "duration_minutes": 45, "sort_order": 2, "is_completed": false, "resources": [ { "name": "CSS Reference Guide", "url": "https://..." } ] } } ``` #### Error Responses | Status | Description | | ------ | ----------------------------------------------------------------------------------------------------- | | 401 | Not authenticated | | 403 | Enrollment not active, enrollment expired, or previous modules not completed (for sequential courses) | | 404 | Enrollment or module not found | If the course has `sequential_modules` enabled in its settings, you must complete all previous modules before accessing later ones. Attempting to skip ahead will return a 403 error. *** ## POST /courses//modules//complete Mark a course module as completed and update progress. ### Authentication Requires Bearer token (`auth:customer_api`). ### Path Parameters | Parameter | Type | Description | | --------- | ------- | -------------------- | | courseId | integer | Course enrollment ID | | moduleId | integer | Module ID | ### Example Request ```bash cURL theme={null} curl -X POST "https://front.rmz.gg/api/courses/1/modules/2/complete" \ -H "Authorization: Bearer 1|abc123xyz..." ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/courses/1/modules/2/complete", { method: "POST", headers: { "Authorization": "Bearer 1|abc123xyz..." } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.post("https://front.rmz.gg/api/courses/1/modules/2/complete", headers={ "Authorization": "Bearer 1|abc123xyz..." }) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->post("https://front.rmz.gg/api/courses/1/modules/2/complete"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "progress": 60.0, "is_completed": false }, "message": "Module marked as completed" } ``` When the last module is completed: ```json theme={null} { "success": true, "data": { "progress": 100.0, "is_completed": true }, "message": "Module marked as completed" } ``` Marking a module as complete is idempotent. Completing an already-completed module will not change the progress or return an error. #### Error Responses | Status | Description | | ------ | ------------------------------ | | 401 | Not authenticated | | 403 | Enrollment not active | | 404 | Enrollment or module not found | *** ## Legacy Course Endpoints The following endpoints are maintained for backward compatibility. They use the `customer/courses` prefix and are handled by the OrderController. ### GET /customer/courses List the customer's course enrollments. Same functionality as `GET /courses`. ### GET /customer/courses/ Get a specific course enrollment. Same functionality as `GET /courses/{id}`. ### GET /customer/courses//modules/ Get a course module with pagination-style navigation (previous/next module links). Returns the module content alongside a `navigation` object. #### Response ```json theme={null} { "success": true, "data": { "course": { ... }, "module": { ... }, "navigation": { "next": 3, "previous": 1 } } } ``` ### POST /customer/courses//modules//complete Mark a course module as completed. Same functionality as `POST /courses/{courseId}/modules/{moduleId}/complete`. New integrations should use the `/courses/*` endpoints instead of `/customer/courses/*`. The legacy endpoints remain available for backward compatibility. # Custom Storefront Tokens Source: https://docs.rmz.gg/storefront-api/custom-tokens Generate, manage, and validate API tokens for custom storefronts connecting to your store. Custom Storefront Tokens allow store owners to generate API keys that external developers can use to build custom storefronts. Tokens are scoped by domain, environment, and permissions. *** ## POST /custom/tokens Generate a new API token for a custom storefront domain. ### Authentication Requires Bearer token (`auth:customer_api`). ### Headers | Header | Value | Required | | ------------- | ---------------- | -------- | | Authorization | Bearer | Yes | | Content-Type | application/json | Yes | ### Body Parameters | Parameter | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------- | | domain | string | Yes | The domain where the token will be used (max 255 chars). Production domains must use HTTPS and cannot be localhost. | | permissions | array | Yes | Array of permission strings (minimum 1) | | environment | string | Yes | Either `development` or `production` | ### Available Permissions | Permission | Description | | ----------------- | -------------------------- | | `read_products` | Read product data | | `read_categories` | Read category data | | `read_orders` | Read order data | | `read_store` | Read store information | | `read_analytics` | Read analytics data | | `write_cart` | Manage cart operations | | `write_wishlist` | Manage wishlist operations | Development tokens are limited to `read_products`, `read_categories`, and `read_store` permissions only. Requesting other permissions for a development token will return an error. ### Example Request ```bash cURL theme={null} curl -X POST "https://front.rmz.gg/api/custom/tokens" \ -H "Authorization: Bearer 1|abc123xyz..." \ -H "Content-Type: application/json" \ -d '{ "domain": "https://mystore.example.com", "permissions": ["read_products", "read_categories", "read_store", "write_cart"], "environment": "production" }' ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/custom/tokens", { method: "POST", headers: { "Authorization": "Bearer 1|abc123xyz...", "Content-Type": "application/json" }, body: JSON.stringify({ domain: "https://mystore.example.com", permissions: ["read_products", "read_categories", "read_store", "write_cart"], environment: "production" }) }); const data = await response.json(); ``` ```python Python theme={null} response = requests.post("https://front.rmz.gg/api/custom/tokens", headers={"Authorization": "Bearer 1|abc123xyz..."}, json={ "domain": "https://mystore.example.com", "permissions": ["read_products", "read_categories", "read_store", "write_cart"], "environment": "production" } ) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->post("https://front.rmz.gg/api/custom/tokens", [ "domain" => "https://mystore.example.com", "permissions" => ["read_products", "read_categories", "read_store", "write_cart"], "environment" => "production" ]); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "id": 1, "token": "cst_42_a1b2c3d4e5f6...", "domain": "https://mystore.example.com", "permissions": ["read_products", "read_categories", "read_store", "write_cart"], "environment": "production", "expires_at": "2025-06-15T00:00:00.000000Z", "created_at": "2024-06-15T10:30:00.000000Z", "usage_note": "Store this token securely. It will not be shown again." }, "message": "API token generated successfully" } ``` The raw token value is only returned once at creation time. Store it securely -- it cannot be retrieved again. The token is stored as a SHA-256 hash on the server. #### Error Responses | Status | Description | | ------ | --------------------------------------------------------------------------------------------------------------- | | 400 | Production domain uses localhost, production domain not HTTPS, or development token with restricted permissions | | 401 | Not authenticated | | 409 | Token already exists for this domain and environment | | 422 | Validation error | ### Token Expiry | Environment | Expires After | | ------------- | ------------- | | `development` | 30 days | | `production` | 1 year | *** ## GET /custom/tokens List all tokens for the authenticated customer's store. ### Authentication Requires Bearer token (`auth:customer_api`). ### Headers | Header | Value | Required | | ------------- | ------- | -------- | | Authorization | Bearer | Yes | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/custom/tokens" \ -H "Authorization: Bearer 1|abc123xyz..." ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/custom/tokens", { headers: { "Authorization": "Bearer 1|abc123xyz..." } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/custom/tokens", headers={ "Authorization": "Bearer 1|abc123xyz..." }) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->get("https://front.rmz.gg/api/custom/tokens"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": [ { "id": 1, "domain": "https://mystore.example.com", "permissions": ["read_products", "read_categories", "read_store", "write_cart"], "environment": "production", "is_active": true, "last_used_at": "2024-06-15T12:00:00.000000Z", "usage_count": 1500, "expires_at": "2025-06-15T00:00:00.000000Z", "created_at": "2024-06-15T10:30:00.000000Z" } ] } ``` The raw token value is never returned in list responses. Only metadata is shown. *** ## DELETE /custom/tokens/ Revoke (deactivate) a token. ### Authentication Requires Bearer token (`auth:customer_api`). ### Path Parameters | Parameter | Type | Description | | --------- | ------- | ------------------ | | tokenId | integer | Token ID to revoke | ### Example Request ```bash cURL theme={null} curl -X DELETE "https://front.rmz.gg/api/custom/tokens/1" \ -H "Authorization: Bearer 1|abc123xyz..." ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/custom/tokens/1", { method: "DELETE", headers: { "Authorization": "Bearer 1|abc123xyz..." } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.delete("https://front.rmz.gg/api/custom/tokens/1", headers={ "Authorization": "Bearer 1|abc123xyz..." }) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->delete("https://front.rmz.gg/api/custom/tokens/1"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": null, "message": "Token revoked successfully" } ``` #### Error Responses | Status | Description | | ------ | ----------------- | | 401 | Not authenticated | | 404 | Token not found | *** ## GET /custom/tokens//stats Get usage statistics for a specific token. ### Authentication Requires Bearer token (`auth:customer_api`). ### Path Parameters | Parameter | Type | Description | | --------- | ------- | ----------- | | tokenId | integer | Token ID | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/custom/tokens/1/stats" \ -H "Authorization: Bearer 1|abc123xyz..." ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/custom/tokens/1/stats", { headers: { "Authorization": "Bearer 1|abc123xyz..." } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/custom/tokens/1/stats", headers={ "Authorization": "Bearer 1|abc123xyz..." }) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->get("https://front.rmz.gg/api/custom/tokens/1/stats"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "usage_count": 1500, "last_used_at": "2024-06-15T12:00:00.000000Z", "created_at": "2024-06-15T10:30:00.000000Z", "is_active": true, "environment": "production", "days_until_expiry": 365 } } ``` #### Error Responses | Status | Description | | ------ | ----------------- | | 401 | Not authenticated | | 404 | Token not found | *** ## POST /custom/tokens/validate Validate a custom storefront token. This is a public endpoint that does not require Bearer authentication. ### Authentication None required. The token to validate is passed in the request body. ### Headers | Header | Value | Required | | ------------ | ---------------- | -------- | | Content-Type | application/json | Yes | ### Body Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------- | | token | string | Yes | The raw token value to validate | ### Example Request ```bash cURL theme={null} curl -X POST "https://front.rmz.gg/api/custom/tokens/validate" \ -H "Content-Type: application/json" \ -d '{"token": "cst_42_a1b2c3d4e5f6..."}' ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/custom/tokens/validate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token: "cst_42_a1b2c3d4e5f6..." }) }); const data = await response.json(); ``` ```python Python theme={null} response = requests.post("https://front.rmz.gg/api/custom/tokens/validate", json={ "token": "cst_42_a1b2c3d4e5f6..." }) data = response.json() ``` ```php PHP theme={null} $response = Http::post("https://front.rmz.gg/api/custom/tokens/validate", [ "token" => "cst_42_a1b2c3d4e5f6..." ]); $data = $response->json(); ``` ### Response #### Valid Token (200) ```json theme={null} { "success": true, "data": { "valid": true, "store_id": 42, "permissions": ["read_products", "read_categories", "read_store", "write_cart"], "environment": "production" } } ``` #### Error Responses | Status | Description | | ------ | ----------------------------------------------------------------------------------- | | 401 | Invalid or expired token | | 403 | Token domain mismatch (production tokens are validated against the `Origin` header) | | 422 | Validation error (missing token) | For production tokens, the `Origin` or `Referer` header of the request must match the domain registered with the token. Development tokens skip this check. *** ## GET /custom/tokens/permissions Get the permissions and metadata for a token identified by the `X-Custom-Token` header. This is a public endpoint. ### Authentication None required. Pass the token via the `X-Custom-Token` header. ### Headers | Header | Value | Required | | -------------- | ----- | -------- | | X-Custom-Token | Yes | | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/custom/tokens/permissions" \ -H "X-Custom-Token: cst_42_a1b2c3d4e5f6..." ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/custom/tokens/permissions", { headers: { "X-Custom-Token": "cst_42_a1b2c3d4e5f6..." } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/custom/tokens/permissions", headers={ "X-Custom-Token": "cst_42_a1b2c3d4e5f6..." }) data = response.json() ``` ```php PHP theme={null} $response = Http::withHeaders(["X-Custom-Token" => "cst_42_a1b2c3d4e5f6..."]) ->get("https://front.rmz.gg/api/custom/tokens/permissions"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "permissions": ["read_products", "read_categories", "read_store", "write_cart"], "environment": "production", "domain": "https://mystore.example.com", "expires_at": "2025-06-15T00:00:00.000000Z" } } ``` #### Error Responses | Status | Description | | ------ | ---------------------------------- | | 401 | Missing, invalid, or expired token | # Management API Source: https://docs.rmz.gg/storefront-api/management Server-to-server endpoints for analytics, inventory management, order access, customer export, and webhook data. The Management API provides server-to-server access to store data for integrations and automation. All endpoints require authentication via a secret key. These endpoints are intended for backend-to-backend communication only. Never expose your secret key in client-side code. ## Authentication All Management API endpoints require the store's secret key, passed via the `SecretKeyAuthMiddleware`. Include the key in the `X-Secret-Key` header: ``` X-Secret-Key: sk_live_... ``` Management endpoints are subject to a separate, lower rate limit (`throttle:management`). *** ## GET /management/analytics Get store analytics and insights for a given time period. ### Headers | Header | Value | Required | | ------------ | ----- | -------- | | X-Secret-Key | Yes | | ### Query Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------- | | period | string | No | Time period: `today`, `week`, `month`, `quarter`, `year`. Default: `month` | | metrics | array | No | Metrics to include: `sales`, `orders`, `customers`, `products`, `reviews`. Default: `["sales", "orders", "customers"]` | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/management/analytics?period=month&metrics[]=sales&metrics[]=orders" \ -H "X-Secret-Key: sk_live_abc123..." ``` ```javascript JavaScript theme={null} const params = new URLSearchParams({ period: "month", "metrics[]": "sales", }); params.append("metrics[]", "orders"); const response = await fetch(`https://front.rmz.gg/api/management/analytics?${params}`, { headers: { "X-Secret-Key": "sk_live_abc123..." } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/management/analytics", headers={"X-Secret-Key": "sk_live_abc123..."}, params={"period": "month", "metrics": ["sales", "orders"]} ) data = response.json() ``` ```php PHP theme={null} $response = Http::withHeaders(["X-Secret-Key" => "sk_live_abc123..."]) ->get("https://front.rmz.gg/api/management/analytics", [ "period" => "month", "metrics" => ["sales", "orders"] ]); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "sales": { "total_orders": 150, "total_revenue": 45000.00, "average_order_value": 300.00 }, "orders": { "total_orders": 180, "pending_orders": 12, "completed_orders": 150, "cancelled_orders": 18 }, "customers": { "new_customers": 45, "verified_customers": 38 }, "products": { "new_products": 8, "active_products": 120, "featured_products": 15 }, "reviews": { "total_reviews": 35, "average_rating": 4.6, "published_reviews": 30 } } } ``` *** ## POST /management/inventory/update Bulk update product inventory. ### Headers | Header | Value | Required | | ------------ | ---------------- | -------- | | X-Secret-Key | Yes | | | Content-Type | application/json | Yes | ### Body Parameters | Parameter | Type | Required | Description | | --------------------------- | ------- | -------- | -------------------------------------------- | | updates | array | Yes | Array of inventory update objects | | updates\[].product\_id | integer | Yes | Product ID | | updates\[].stock\_change | integer | Yes | Stock quantity change (positive or negative) | | updates\[].unlimited\_stock | boolean | No | Set to true for unlimited stock | ### Example Request ```bash cURL theme={null} curl -X POST "https://front.rmz.gg/api/management/inventory/update" \ -H "X-Secret-Key: sk_live_abc123..." \ -H "Content-Type: application/json" \ -d '{ "updates": [ { "product_id": 101, "stock_change": 50 }, { "product_id": 102, "stock_change": -5 }, { "product_id": 103, "unlimited_stock": true } ] }' ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/management/inventory/update", { method: "POST", headers: { "X-Secret-Key": "sk_live_abc123...", "Content-Type": "application/json" }, body: JSON.stringify({ updates: [ { product_id: 101, stock_change: 50 }, { product_id: 102, stock_change: -5 }, { product_id: 103, unlimited_stock: true } ] }) }); const data = await response.json(); ``` ```python Python theme={null} response = requests.post("https://front.rmz.gg/api/management/inventory/update", headers={"X-Secret-Key": "sk_live_abc123..."}, json={ "updates": [ {"product_id": 101, "stock_change": 50}, {"product_id": 102, "stock_change": -5}, {"product_id": 103, "unlimited_stock": True} ] } ) data = response.json() ``` ```php PHP theme={null} $response = Http::withHeaders(["X-Secret-Key" => "sk_live_abc123..."]) ->post("https://front.rmz.gg/api/management/inventory/update", [ "updates" => [ ["product_id" => 101, "stock_change" => 50], ["product_id" => 102, "stock_change" => -5], ["product_id" => 103, "unlimited_stock" => true] ] ]); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": [], "message": "Inventory updated successfully" } ``` #### Error Responses | Status | Description | | ------ | --------------------------------------------------------------- | | 422 | Validation error (invalid product\_id, missing required fields) | | 500 | Update failed | Inventory updates run within a database transaction. If any single update fails, all changes are rolled back. Product caches are automatically invalidated after updates. *** ## GET /management/orders Get store orders with filtering and sorting. ### Headers | Header | Value | Required | | ------------ | ----- | -------- | | X-Secret-Key | Yes | | ### Query Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ----------------------------------------------------------------------------------------------- | | status | string | No | Filter by order status | | per\_page | integer | No | Orders per page (1-100). Default: `20` | | sort | string | No | Sort order: `created_desc`, `created_asc`, `amount_desc`, `amount_asc`. Default: `created_desc` | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/management/orders?per_page=20&sort=created_desc" \ -H "X-Secret-Key: sk_live_abc123..." ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/management/orders?per_page=20", { headers: { "X-Secret-Key": "sk_live_abc123..." } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/management/orders", headers={"X-Secret-Key": "sk_live_abc123..."}, params={"per_page": 20, "sort": "created_desc"} ) data = response.json() ``` ```php PHP theme={null} $response = Http::withHeaders(["X-Secret-Key" => "sk_live_abc123..."]) ->get("https://front.rmz.gg/api/management/orders", [ "per_page" => 20, "sort" => "created_desc" ]); $data = $response->json(); ``` ### Response #### Success (200) Paginated list of orders with customer and item details. ```json theme={null} { "success": true, "data": [ { "id": 78901, "total": 399.98, "status": "Completed", "customer": { "id": 123, "first_name": "Ahmed", "last_name": "Ali" }, "items": [...], "created_at": "2024-06-15T14:30:00.000000Z" } ], "pagination": { ... } } ``` *** ## GET /management/export/customers Export customer data in JSON or CSV format. ### Headers | Header | Value | Required | | ------------ | ----- | -------- | | X-Secret-Key | Yes | | ### Query Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------------------------------------------- | | format | string | No | Export format: `json` or `csv`. Default: `json` | | filters | object | No | Optional filters | ### Example Request ```bash cURL theme={null} # JSON format curl "https://front.rmz.gg/api/management/export/customers?format=json" \ -H "X-Secret-Key: sk_live_abc123..." # CSV format curl "https://front.rmz.gg/api/management/export/customers?format=csv" \ -H "X-Secret-Key: sk_live_abc123..." \ --output customers.csv ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/management/export/customers?format=json", { headers: { "X-Secret-Key": "sk_live_abc123..." } }); const data = await response.json(); ``` ```python Python theme={null} # JSON response = requests.get("https://front.rmz.gg/api/management/export/customers", headers={"X-Secret-Key": "sk_live_abc123..."}, params={"format": "json"} ) data = response.json() # CSV response = requests.get("https://front.rmz.gg/api/management/export/customers", headers={"X-Secret-Key": "sk_live_abc123..."}, params={"format": "csv"} ) with open("customers.csv", "wb") as f: f.write(response.content) ``` ```php PHP theme={null} $response = Http::withHeaders(["X-Secret-Key" => "sk_live_abc123..."]) ->get("https://front.rmz.gg/api/management/export/customers", ["format" => "json"]); $data = $response->json(); ``` ### Response #### JSON Format (200) ```json theme={null} { "success": true, "data": [ { "id": 123, "first_name": "Ahmed", "last_name": "Ali", "email": "ahmed@example.com", "phone": "501234567", "country_code": "966" } ], "message": "Customer data exported successfully" } ``` #### CSV Format (200) Returns a downloadable CSV file with `Content-Type: text/csv`. *** ## GET /management/webhooks/data Get recent data for webhook-style integrations, filtered by event type. ### Headers | Header | Value | Required | | ------------ | ----- | -------- | | X-Secret-Key | Yes | | ### Query Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ----------------------------------------------------------------------------------- | | event | string | Yes | Event type: `order.created`, `order.updated`, `product.updated`, `customer.created` | | limit | integer | No | Number of records (1-100). Default: `50` | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/management/webhooks/data?event=order.created&limit=10" \ -H "X-Secret-Key: sk_live_abc123..." ``` ```javascript JavaScript theme={null} const response = await fetch( "https://front.rmz.gg/api/management/webhooks/data?event=order.created&limit=10", { headers: { "X-Secret-Key": "sk_live_abc123..." } } ); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/management/webhooks/data", headers={"X-Secret-Key": "sk_live_abc123..."}, params={"event": "order.created", "limit": 10} ) data = response.json() ``` ```php PHP theme={null} $response = Http::withHeaders(["X-Secret-Key" => "sk_live_abc123..."]) ->get("https://front.rmz.gg/api/management/webhooks/data", [ "event" => "order.created", "limit" => 10 ]); $data = $response->json(); ``` ### Response #### Success (200) The response shape depends on the event type. For `order.created` / `order.updated`: ```json theme={null} { "success": true, "data": [ { "id": 78901, "total": 399.98, "status": "Completed", "customer": { ... }, "items": [ ... ], "created_at": "2024-06-15T14:30:00.000000Z" } ], "message": "Webhook data for order.created" } ``` For `product.updated`: ```json theme={null} { "success": true, "data": [ { "id": 101, "name": "Premium Game Key", "slug": "premium-game-key", "price": 199.99, "type": "code", "image": { ... }, "categories": [ ... ] } ], "message": "Webhook data for product.updated" } ``` For `customer.created`: ```json theme={null} { "success": true, "data": [ { "id": 123, "first_name": "Ahmed", "last_name": "Ali", "email": "ahmed@example.com" } ], "message": "Webhook data for customer.created" } ``` #### Error Responses | Status | Description | | ------ | ------------------------------------------------ | | 422 | Validation error (missing or invalid event type) | ### Supported Event Types | Event | Description | | ------------------ | -------------------------- | | `order.created` | Recently created orders | | `order.updated` | Recently updated orders | | `product.updated` | Recently updated products | | `customer.created` | Recently created customers | # Orders & Subscriptions Source: https://docs.rmz.gg/storefront-api/orders View customer orders, order details, VAT invoices, subscriptions, and submit order reviews. All order endpoints require customer authentication. *** ## GET /customer/orders List the authenticated customer's orders, sorted by newest first. ### Authentication Requires Bearer token (`auth:customer_api`). ### Headers | Header | Value | Required | | ------------- | ------- | -------- | | Authorization | Bearer | Yes | ### Query Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ------------------------------------- | | per\_page | integer | No | Orders per page (1-50). Default: `12` | | page | integer | No | Page number | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/customer/orders?per_page=10&page=1" \ -H "Authorization: Bearer 1|abc123xyz..." ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/customer/orders?per_page=10", { headers: { "Authorization": "Bearer 1|abc123xyz..." } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/customer/orders", headers={"Authorization": "Bearer 1|abc123xyz..."}, params={"per_page": 10} ) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->get("https://front.rmz.gg/api/customer/orders", [ "per_page" => 10 ]); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": [ { "id": 78901, "order_number": 78901, "status": { "id": 1, "status": 4, "name": "Completed", "color": "success", "human_format": { "text": "Completed", "color": "success", "date_human": "2 hours ago", "date_normal": "2024-06-15 14:30:00" } }, "total": { "amount": 399.98, "formatted": "399.98 ر.س", "currency": "SAR" }, "financial_breakdown": { "subtotal": 399.98, "discount_amount": 0, "final_total": 399.98 }, "items": [ { "id": 1001, "product_name": "Premium Game Key", "name": "Premium Game Key", "product_id": 101, "quantity": 2, "price": 199.99, "total": 399.98, "formatted_price": "199.99 ر.س", "formatted_total": "399.98 ر.س", "fields": null, "notes": null, "item_type": "App\\Models\\StoreProduct", "item_id": 101, "product": { "id": 101, "name": "Premium Game Key", "type": "code", "description": "...", "full_link": null, "activation_info": "Enter this code on the platform", "image": { "url": "https://cdn.rmz.gg/...", "full_link": "https://cdn.rmz.gg/...", "alt": "Premium Game Key" } }, "item": { "id": 101, "name": "Premium Game Key", "type": "code", "description": "...", "full_link": null, "activation_info": "Enter this code on the platform", "image": { "url": "https://cdn.rmz.gg/...", "full_link": "https://cdn.rmz.gg/...", "alt": "Premium Game Key" }, "product": { "full_link": null } }, "codes": [], "licenses": [] } ], "customer": { "id": 123, "name": "Ahmed Ali", "email": "ahmed@example.com", "phone": "501234567" }, "payment": { "method": "card", "status": null, "transaction_id": "ch_abc123" }, "discount_amount": 0, "customer_note": null, "meta": null, "eligibility": { "can_review": true, "can_complain": true, "can_refund": false }, "human_format": { "created_at_human": "2 hours ago", "created_at_normal": "2024-06-15 14:30:00", "total_formatted": "399.98 ر.س" }, "created_at": "2024-06-15T14:30:00.000000Z", "updated_at": "2024-06-15T14:30:00.000000Z" } ], "pagination": { "current_page": 1, "last_page": 3, "per_page": 10, "total": 25, "from": 1, "to": 10, "has_more_pages": true, "next_page_url": "...", "prev_page_url": null } } ``` *** ## GET /customer/orders/ Get detailed information for a specific order, including items, codes, licenses, subscriptions, course enrollments, transaction details, and review status. ### Authentication Requires Bearer token (`auth:customer_api`). ### Path Parameters | Parameter | Type | Description | | --------- | ------- | ----------- | | id | integer | Order ID | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/customer/orders/78901" \ -H "Authorization: Bearer 1|abc123xyz..." ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/customer/orders/78901", { headers: { "Authorization": "Bearer 1|abc123xyz..." } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/customer/orders/78901", headers={ "Authorization": "Bearer 1|abc123xyz..." }) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->get("https://front.rmz.gg/api/customer/orders/78901"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "id": 78901, "order_number": 78901, "status": { "id": 1, "status": 4, "name": "Completed", "color": "success", "human_format": { "text": "Completed", "color": "success", "date_human": "2 hours ago", "date_normal": "2024-06-15 14:30:00" } }, "statuses": [ { "id": 1, "status": 1, "name": "Received", "color": "info", "created_at": "2024-06-15T14:30:00.000000Z", "human_format": { "text": "Received", "color": "info", "date_human": "2 hours ago", "date_normal": "2024-06-15 14:30:00" } } ], "total": { "amount": 399.98, "formatted": "399.98 ر.س", "currency": "SAR" }, "financial_breakdown": { "subtotal": 399.98, "discount_amount": 0, "final_total": 399.98 }, "items": [ { "id": 1001, "product_name": "Premium Game Key", "name": "Premium Game Key", "product_id": 101, "quantity": 2, "price": 199.99, "total": 399.98, "formatted_price": "199.99 ر.س", "formatted_total": "399.98 ر.س", "fields": { "platform": "PC" }, "notes": null, "item_type": "App\\Models\\StoreProduct", "item_id": 101, "product": { "id": 101, "name": "Premium Game Key", "type": "code", "description": "...", "full_link": null, "activation_info": "Enter this code on the platform", "image": { "url": "https://cdn.rmz.gg/...", "full_link": "https://cdn.rmz.gg/...", "alt": "Premium Game Key" } }, "item": { "id": 101, "name": "Premium Game Key", "type": "code", "description": "...", "full_link": null, "activation_info": "Enter this code on the platform", "image": { "url": "https://cdn.rmz.gg/...", "full_link": "https://cdn.rmz.gg/...", "alt": "Premium Game Key" }, "product": { "full_link": null } }, "codes": [ { "id": 1, "code": "XXXX-YYYY-ZZZZ", "used_at": null, "is_used": false } ], "licenses": [], "subscription": null, "course_enrollment": null } ], "customer": { "id": 123, "name": "Ahmed Ali", "email": "ahmed@example.com", "phone": "501234567" }, "transaction": { "payment_method": "card", "id": 1, "payment_id": "ch_abc123", "total": 399.98, "is_refunded": false, "human_format": { "payment_method": "card", "is_refunded": "No", "total_formatted": "399.98 ر.س" }, "receipt": null }, "payment": { "method": "card", "status": null, "transaction_id": "ch_abc123" }, "coupon": null, "discount_amount": 0, "customer_note": null, "meta": null, "review": null, "complain": null, "eligibility": { "can_review": true, "can_complain": true, "can_refund": false }, "human_format": { "created_at_human": "2 hours ago", "created_at_normal": "2024-06-15 14:30:00", "total_formatted": "399.98 ر.س" }, "created_at": "2024-06-15T14:30:00.000000Z", "updated_at": "2024-06-15T14:30:00.000000Z" } } ``` #### Error Responses | Status | Description | | ------ | ---------------------------------------------- | | 401 | Not authenticated | | 404 | Order not found or belongs to another customer | *** ## GET /customer/orders//vat-invoice Download the VAT invoice PDF for an order. ### Authentication Requires Bearer token (`auth:customer_api`). ### Path Parameters | Parameter | Type | Description | | --------- | ------- | ----------- | | id | integer | Order ID | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/customer/orders/78901/vat-invoice" \ -H "Authorization: Bearer 1|abc123xyz..." \ --output invoice.pdf ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/customer/orders/78901/vat-invoice", { headers: { "Authorization": "Bearer 1|abc123xyz..." } }); const blob = await response.blob(); // Download or display the PDF ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/customer/orders/78901/vat-invoice", headers={ "Authorization": "Bearer 1|abc123xyz..." }) with open("invoice.pdf", "wb") as f: f.write(response.content) ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...") ->get("https://front.rmz.gg/api/customer/orders/78901/vat-invoice"); file_put_contents("invoice.pdf", $response->body()); ``` ### Response #### Success (200) Returns a PDF file with `Content-Type: application/pdf` and `Content-Disposition: attachment`. #### Error Responses | Status | Description | | ------ | -------------------------- | | 400 | Order does not include VAT | | 401 | Not authenticated | | 404 | Order not found | *** ## POST /orders//review Submit a review for a completed order, including individual item ratings. ### Authentication Requires Bearer token (`auth:customer_api`). ### Path Parameters | Parameter | Type | Description | | --------- | ------- | ----------- | | id | integer | Order ID | ### Body Parameters | Parameter | Type | Required | Description | | -------------- | ------- | -------- | -------------------------------------------------------------- | | rating | integer | Yes | Overall order rating (1-5) | | comment | string | Yes | Overall review comment (3-255 chars) | | item\_ratings | object | Yes | Item ratings as `{item_id: rating}` pairs (1-5 each) | | item\_comments | object | Yes | Item comments as `{item_id: comment}` pairs (3-255 chars each) | ### Example Request ```bash cURL theme={null} curl -X POST "https://front.rmz.gg/api/orders/78901/review" \ -H "Authorization: Bearer 1|abc123xyz..." \ -H "Content-Type: application/json" \ -d '{ "rating": 5, "comment": "Great order, fast delivery!", "item_ratings": { "1001": 5, "1002": 4 }, "item_comments": { "1001": "Excellent product quality", "1002": "Good but could be better" } }' ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/orders/78901/review", { method: "POST", headers: { "Authorization": "Bearer 1|abc123xyz...", "Content-Type": "application/json" }, body: JSON.stringify({ rating: 5, comment: "Great order, fast delivery!", item_ratings: { "1001": 5, "1002": 4 }, item_comments: { "1001": "Excellent product quality", "1002": "Good but could be better" } }) }); ``` ```python Python theme={null} response = requests.post("https://front.rmz.gg/api/orders/78901/review", headers={"Authorization": "Bearer 1|abc123xyz..."}, json={ "rating": 5, "comment": "Great order, fast delivery!", "item_ratings": {"1001": 5, "1002": 4}, "item_comments": {"1001": "Excellent product quality", "1002": "Good but could be better"} } ) ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->post("https://front.rmz.gg/api/orders/78901/review", [ "rating" => 5, "comment" => "Great order, fast delivery!", "item_ratings" => ["1001" => 5, "1002" => 4], "item_comments" => ["1001" => "Excellent product quality", "1002" => "Good but could be better"] ]); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": null, "message": "تم ارسال التعليق بنجاح" } ``` Reviews may be auto-published or require store owner approval depending on the store's `auto_accept_reviews` setting. #### Error Responses | Status | Description | | ------ | ------------------------------------------------------ | | 400 | Order not in completed status (status 3 or 4 required) | | 401 | Not authenticated | | 404 | Order not found | | 409 | Order already has a review | | 422 | Validation error | *** ## GET /customer/subscriptions List the authenticated customer's active subscriptions. ### Authentication Requires Bearer token (`auth:customer_api`). ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/customer/subscriptions" \ -H "Authorization: Bearer 1|abc123xyz..." ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/customer/subscriptions", { headers: { "Authorization": "Bearer 1|abc123xyz..." } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/customer/subscriptions", headers={ "Authorization": "Bearer 1|abc123xyz..." }) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->get("https://front.rmz.gg/api/customer/subscriptions"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": [ { "id": 1, "status": "active", "starts_at": "2024-06-15T00:00:00.000000Z", "ends_at": "2024-07-15T00:00:00.000000Z", "start_date": "2024-06-15T00:00:00.000000Z", "end_date": "2024-07-15T00:00:00.000000Z", "duration": "monthly", "order_id": 78901, "auto_renew": false, "price": { "amount": 49.99, "formatted": "49.99 ر.س", "currency": "SAR" }, "product": { "id": 50, "name": "Premium Membership", "slug": "premium-membership", "type": "subscription" }, "variant": null, "features": null, "is_active": true, "is_expired": false, "days_remaining": 30, "created_at": "2024-06-15T00:00:00.000000Z", "updated_at": "2024-06-15T00:00:00.000000Z" } ] } ``` #### Error Responses | Status | Description | | ------ | ----------------- | | 401 | Not authenticated | # Storefront API Overview Source: https://docs.rmz.gg/storefront-api/overview Build custom storefronts for RMZ stores using the headless Storefront API. The RMZ Storefront API lets you build fully custom storefronts that connect to any RMZ store. It provides endpoints for browsing products, managing carts, processing checkouts, handling customer authentication, and more. ## Base URL ``` https://front.rmz.gg/api ``` The store is identified automatically by the `Origin` or `Referer` header of your request. Your custom storefront domain must be configured in the store's dashboard for requests to be accepted. ## Authentication Model The Storefront API uses two authentication mechanisms: ### 1. Customer Authentication (Bearer Token) Customers authenticate via an OTP (one-time password) flow. After verifying their phone number or email, they receive a Bearer token used for authenticated endpoints (orders, profile, wishlist, courses, checkout). ``` Authorization: Bearer 1|abc123xyz... ``` ### 2. Guest Cart Token Unauthenticated visitors can still browse products and manage a cart using the `X-Cart-Token` header. A cart token is issued when items are first added to the cart and is also returned upon customer authentication. ``` X-Cart-Token: cart_abc123 ``` ### 3. Management Secret Key The Management API endpoints use a secret key for server-to-server authentication. This key is configured in the store dashboard. ``` X-Secret-Key: sk_live_... ``` ### 4. Custom Storefront Token Store owners can generate custom API tokens for external developers building storefronts. Tokens are scoped by domain, environment, and permissions. ``` X-Custom-Token: cst_42_abc123... ``` ## Product Types RMZ stores can sell five types of products: | Type | Description | | -------------- | ----------------------------------------------------------- | | `product` | Standard digital product | | `code` | Digital codes or keys (game keys, license keys, gift cards) | | `service` | Service-based product | | `subscription` | Recurring subscription with variants and billing periods | | `course` | Online course with modules and progress tracking | ## Response Format All responses follow a consistent JSON structure. ### Success Response ```json theme={null} { "success": true, "data": { }, "message": "Optional success message" } ``` ### Paginated Response ```json theme={null} { "success": true, "data": [], "pagination": { "current_page": 1, "last_page": 10, "per_page": 12, "total": 120, "from": 1, "to": 12, "has_more_pages": true, "next_page_url": "https://front.rmz.gg/api/products?page=2", "prev_page_url": null } } ``` ### Error Response ```json theme={null} { "success": false, "message": "Error description", "data": { } } ``` ## Rate Limits | Endpoint | Limit | | -------------------- | ----------------------------------------------------------- | | General API | 60 requests per minute | | Auth Start | 50 sessions per day per IP | | Phone Auth | 10 attempts per day per phone number | | OTP Verification | 5 attempts per minute per IP | | OTP Resend | 3 resends per 10 minutes | | Management API | Lower rate limit (separate throttle group) | | Analytics Collection | 120 events per minute per IP (separate from API rate limit) | When a rate limit is exceeded, you receive a `429 Too Many Requests` response. ## Error Codes | HTTP Code | Description | | --------- | --------------------------------------- | | 200 | Success | | 201 | Created | | 400 | Bad Request / Validation Error | | 401 | Unauthorized (missing or invalid token) | | 403 | Forbidden (access denied) | | 404 | Not Found | | 409 | Conflict (duplicate resource) | | 422 | Validation Error | | 429 | Too Many Requests (rate limit exceeded) | | 500 | Internal Server Error | ## Supported Country Codes | Code | Country | | ---- | ------------ | | 966 | Saudi Arabia | | 973 | Bahrain | | 971 | UAE | | 974 | Qatar | | 968 | Oman | | 965 | Kuwait | ## Caching Responses include cache headers to help optimize your storefront: * `X-Cache-Status`: `HIT` or `MISS` indicating whether the response was served from cache * `Cache-Control`: `public, max-age=N` with appropriate TTL per resource type | Resource | Cache TTL | | --------------- | ------------ | | Store info | \~2 minutes | | Categories | \~30 minutes | | Products | \~3 minutes | | Product details | \~3 minutes | | Reviews | \~5 minutes | ## CORS The API supports cross-origin requests. Your storefront domain must be registered in the store's settings for CORS to allow the request. The `Origin` header is used to identify the store. # Pages Source: https://docs.rmz.gg/storefront-api/pages Retrieve custom store pages such as About Us, Terms of Service, and other static content. ## GET /pages List all active custom pages for the store. ### Authentication None required. ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/pages" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/pages"); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/pages") data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/pages"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": [ { "id": 1, "title": "About Us", "url": "about-us", "content": "

About Our Store

...

", "excerpt": "Learn more about our store", "meta_title": "About Us", "meta_description": "Learn more about our store", "is_active": true, "created_at": "2024-01-01T00:00:00.000000Z", "updated_at": "2024-06-15T10:30:00.000000Z" }, { "id": 2, "title": "Terms of Service", "url": "terms-of-service", "content": "...", "excerpt": null, "meta_title": null, "meta_description": null, "is_active": true, "created_at": "2024-01-01T00:00:00.000000Z", "updated_at": "2024-06-15T10:30:00.000000Z" } ] } ``` *** ## GET /pages/ Get the full content of a specific page by its URL slug. ### Authentication None required. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | -------------------------------- | | url | string | Page URL slug (e.g., `about-us`) | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/pages/about-us" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/pages/about-us"); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/pages/about-us") data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/pages/about-us"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "id": 1, "title": "About Us", "url": "about-us", "content": "

About Our Store

We are a leading provider of digital products...

", "excerpt": "Learn more about our store", "meta_title": "About Us", "meta_description": "Learn more about our store", "is_active": true, "created_at": "2024-01-01T00:00:00.000000Z", "updated_at": "2024-06-15T10:30:00.000000Z" } } ``` #### Error Responses | Status | Description | | ------ | -------------------------- | | 404 | Page not found or inactive | # Get Product Details Source: https://docs.rmz.gg/storefront-api/products/get-product Retrieve a single product by slug, its subscription variants, and related products. ## GET /products/ Get detailed product information by slug. Includes categories, subscription variants, course structure, and codes availability. ### Authentication None required. ### Path Parameters | Parameter | Type | Description | | --------- | ------ | ---------------- | | slug | string | Product URL slug | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/products/premium-game-key" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/products/premium-game-key"); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/products/premium-game-key") data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/products/premium-game-key"); $data = $response->json(); ``` ### Response #### Success (200) The response uses the same `ProductResource` structure as the list endpoint, including nested `price`, `stock`, and `seo` objects. Subscription variants and course data are included when applicable. ```json theme={null} { "success": true, "data": { "id": 101, "name": "Premium Game Key", "marketing_title": null, "slug": "premium-game-key", "description": "Premium game activation key for PC", "short_description": "Premium game activation key for PC...", "type": "code", "status": 1, "is_featured": false, "is_noticeable": false, "is_new": false, "is_discounted": false, "is_discount_expired": false, "show_reviews": true, "price": { "original": 199.99, "actual": 199.99, "discount": null, "discount_expiry": null, "show_discount_countdown": false, "show_discount_savings": false, "savings_amount": 0, "formatted": "199.99 ر.س", "formatted_original": "199.99 ر.س", "discount_percentage": 0, "currency": "SAR" }, "stock": { "available": 50, "unlimited": false, "min_qty": 1, "codes_count": 50, "is_in_stock": true }, "sales": { "badge": null }, "image": { "id": 1, "url": "https://...", "full_link": "https://...", "path": "...", "filename": "image.webp", "alt_text": "Premium Game Key" }, "categories": [ { "id": 1, "name": "Games", "slug": "games", "description": null, "image": null, "icon": null, "is_active": true, "sort_order": 0, "created_at": "2024-01-01T00:00:00.000000Z", "updated_at": "2024-06-15T10:30:00.000000Z" } ], "subscription_variants": [], "course": null, "fields": null, "meta": null, "metadata": null, "tags": null, "seo": { "meta_title": null, "meta_description": null, "meta_keywords": null }, "created_at": "2024-06-15T10:30:00.000000Z", "updated_at": "2024-06-15T10:30:00.000000Z" } } ``` For a **subscription** product, the `subscription_variants` field contains plan options with localized formatting: ```json theme={null} { "subscription_variants": [ { "id": 1, "price": 49.99, "duration": "monthly", "duration_text": "شهري", "duration_text_en": "1 Month", "formatted_price": "49.99 ر.س", "features": [ { "id": 1, "name": "Feature A", "description": "..." }, { "id": 2, "name": "Feature B", "description": "..." } ] } ] } ``` For a **course** product, the `course` field contains the course structure: ```json theme={null} { "course": { "id": 5, "instructor": "John Doe", "level": "beginner", "level_arabic": "مبتدئ", "sections": [ { "id": 1, "title": "Getting Started", "description": "Introduction to the course", "sort_index": 0, "modules": [ { "id": 1, "title": "Introduction", "description": "...", "type": "video", "sort_index": 0, "duration_minutes": 15 }, { "id": 2, "title": "Setup", "description": "...", "type": "text", "sort_index": 1, "duration_minutes": 10 } ] } ], "total_modules": 2, "estimated_duration": 25 } } ``` #### Error Responses | Status | Description | | ------ | ---------------------------------------------------------- | | 404 | Product not found (invalid slug, inactive, or wrong store) | Products with status `3` (hidden/deleted) are excluded from this endpoint. *** ## GET /products//variants Get subscription variants for a subscription product. Returns active variants sorted by display order. ### Authentication None required. ### Path Parameters | Parameter | Type | Description | | --------- | ------- | ------------------------------------------------ | | id | integer | Product ID (must be a subscription-type product) | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/products/101/variants" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/products/101/variants"); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/products/101/variants") data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/products/101/variants"); $data = $response->json(); ``` ### Response #### Success (200) The variants endpoint returns raw database fields (not the ProductResource format): ```json theme={null} { "success": true, "data": [ { "id": 1, "name": "Monthly Plan", "description": "Access for one month", "price": 49.99, "duration_type": "month", "duration_value": 1, "features": "..." }, { "id": 2, "name": "Annual Plan", "description": "Access for one year", "price": 399.99, "duration_type": "year", "duration_value": 1, "features": "..." } ] } ``` #### Error Responses | Status | Description | | ------ | -------------------------------------------- | | 404 | Product not found or not a subscription type | *** ## GET /products//related Get products related to the specified product, based on shared categories. ### Authentication None required. ### Path Parameters | Parameter | Type | Description | | --------- | ------- | ----------- | | id | integer | Product ID | ### Query Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ----------------------------------------- | | limit | integer | No | Number of related products (default: `8`) | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/products/101/related?limit=4" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/products/101/related?limit=4"); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/products/101/related", params={"limit": 4}) data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/products/101/related", ["limit" => 4]); $data = $response->json(); ``` ### Response #### Success (200) Returns the same `ProductResource` structure as `GET /products`. See [List Products](/storefront-api/products/list-products) for the full response schema. ```json theme={null} { "success": true, "data": [ { "id": 102, "name": "Standard Game Key", "marketing_title": null, "slug": "standard-game-key", "description": "Standard game activation key", "short_description": "Standard game activation key...", "type": "code", "status": 1, "is_featured": false, "is_noticeable": false, "is_new": false, "is_discounted": false, "is_discount_expired": false, "show_reviews": true, "price": { "original": 99.99, "actual": 99.99, "discount": null, "discount_expiry": null, "show_discount_countdown": false, "show_discount_savings": false, "savings_amount": 0, "formatted": "99.99 ر.س", "formatted_original": "99.99 ر.س", "discount_percentage": 0, "currency": "SAR" }, "stock": { "available": 25, "unlimited": false, "min_qty": 1, "codes_count": 25, "is_in_stock": true }, "sales": { "badge": null }, "image": { "id": 2, "url": "https://...", "full_link": "https://...", "path": "...", "filename": "image.webp", "alt_text": "Standard Game Key" }, "categories": [], "fields": null, "meta": null, "metadata": null, "tags": null, "seo": { "meta_title": null, "meta_description": null, "meta_keywords": null }, "created_at": "2024-06-10T08:00:00.000000Z", "updated_at": "2024-06-10T08:00:00.000000Z" } ] } ``` Related products are randomly shuffled from products sharing the same categories. Results will vary between requests. Only the `image` relation is loaded for related products. #### Error Responses | Status | Description | | ------ | ----------------- | | 404 | Product not found | # List & Search Products Source: https://docs.rmz.gg/storefront-api/products/list-products Browse, filter, sort, and search the store's product catalog. ## GET /products List products with filtering, sorting, and pagination. ### Authentication None required. ### Query Parameters | Parameter | Type | Required | Description | | ---------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------- | | category | string | No | Filter by category slug | | search | string | No | Search in product name and description (max 255 chars) | | sort | string | No | Sort order: `price_asc`, `price_desc`, `name_asc`, `name_desc`, `created_asc`, `created_desc`. Default: newest first | | per\_page | integer | No | Items per page (1-50). Default: `12` | | page | integer | No | Page number. Default: `1` | | featured | boolean | No | Filter featured products only | | type | string | No | Filter by product type: `digital`, `subscription`, `course` | | price\_min | number | No | Minimum price filter | | price\_max | number | No | Maximum price filter | | in\_stock | boolean | No | Filter to only in-stock products | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/products?category=electronics&sort=price_asc&per_page=12&page=1" ``` ```javascript JavaScript theme={null} const params = new URLSearchParams({ category: "electronics", sort: "price_asc", per_page: "12", page: "1" }); const response = await fetch(`https://front.rmz.gg/api/products?${params}`); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/products", params={ "category": "electronics", "sort": "price_asc", "per_page": 12, "page": 1 }) data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/products", [ "category" => "electronics", "sort" => "price_asc", "per_page" => 12, "page" => 1 ]); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": [ { "id": 101, "name": "Premium Game Key", "marketing_title": null, "slug": "premium-game-key", "description": "Premium game activation key for PC", "short_description": "Premium game activation key for PC...", "type": "code", "status": 1, "is_featured": false, "is_noticeable": false, "is_new": false, "is_discounted": false, "is_discount_expired": false, "show_reviews": true, "price": { "original": 199.99, "actual": 199.99, "discount": null, "discount_expiry": null, "show_discount_countdown": false, "show_discount_savings": false, "savings_amount": 0, "formatted": "199.99 ر.س", "formatted_original": "199.99 ر.س", "discount_percentage": 0, "currency": "SAR" }, "stock": { "available": 50, "unlimited": false, "min_qty": 1, "codes_count": 50, "is_in_stock": true }, "sales": { "badge": null }, "image": { "id": 1, "url": "https://...", "full_link": "https://...", "path": "...", "filename": "image.webp", "alt_text": "Premium Game Key" }, "categories": [ { "id": 1, "name": "Games", "slug": "games", "description": null, "image": null, "icon": null, "is_active": true, "sort_order": 0, "created_at": "2024-01-01T00:00:00.000000Z", "updated_at": "2024-06-15T10:30:00.000000Z" } ], "fields": null, "meta": null, "metadata": null, "tags": null, "seo": { "meta_title": null, "meta_description": null, "meta_keywords": null }, "created_at": "2024-06-15T10:30:00.000000Z", "updated_at": "2024-06-15T10:30:00.000000Z" } ], "pagination": { "current_page": 1, "last_page": 5, "per_page": 12, "total": 60, "from": 1, "to": 12, "has_more_pages": true, "next_page_url": "https://front.rmz.gg/api/products?page=2", "prev_page_url": null } } ``` *** ## GET /products/search Search products with advanced filters and relevance scoring. ### Authentication None required. ### Query Parameters | Parameter | Type | Required | Description | | ---------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------- | | q | string | Yes | Search query (2-255 characters) | | category | string | No | Filter by category slug | | price\_min | number | No | Minimum price filter | | price\_max | number | No | Maximum price filter | | type | string | No | Filter by type: `digital`, `subscription`, `course` | | sort | string | No | Sort order: `relevance`, `price_asc`, `price_desc`, `name_asc`, `name_desc`, `created_desc`. Default: `created_desc` | | per\_page | integer | No | Items per page (1-50). Default: `12` | When `sort=relevance`, results are ranked by a relevance score. Name matches score higher than description matches. ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/products/search?q=game+key&sort=relevance&per_page=12" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/products/search?q=game+key&sort=relevance"); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/products/search", params={ "q": "game key", "sort": "relevance", "per_page": 12 }) data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/products/search", [ "q" => "game key", "sort" => "relevance", "per_page" => 12 ]); $data = $response->json(); ``` ### Response Same paginated format as `GET /products`. #### Error Responses | Status | Description | | ------ | ---------------------------------------------------- | | 422 | Validation error (`q` is required, min 2 characters) | *** ## GET /featured-products Get featured (highlighted) products. ### Authentication None required. ### Query Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ------------------------------------------------- | | limit | integer | No | Number of products to return (1-20). Default: `8` | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/featured-products?limit=8" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/featured-products?limit=8"); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/featured-products", params={"limit": 8}) data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/featured-products", ["limit" => 8]); $data = $response->json(); ``` ### Response #### Success (200) Returns the same `ProductResource` structure as `GET /products`. See the full response schema above. ```json theme={null} { "success": true, "data": [ { "id": 101, "name": "Premium Game Key", "marketing_title": null, "slug": "premium-game-key", "description": "Premium game activation key for PC", "short_description": "Premium game activation key for PC...", "type": "code", "status": 1, "is_featured": false, "is_noticeable": true, "is_new": false, "is_discounted": false, "is_discount_expired": false, "show_reviews": true, "price": { "original": 199.99, "actual": 199.99, "discount": null, "discount_expiry": null, "show_discount_countdown": false, "show_discount_savings": false, "savings_amount": 0, "formatted": "199.99 ر.س", "formatted_original": "199.99 ر.س", "discount_percentage": 0, "currency": "SAR" }, "stock": { "available": 50, "unlimited": false, "min_qty": 1, "codes_count": 50, "is_in_stock": true }, "sales": { "badge": null }, "image": { "id": 1, "url": "https://...", "full_link": "https://...", "path": "...", "filename": "image.webp", "alt_text": "Premium Game Key" }, "categories": [ { "id": 1, "name": "Games", "slug": "games", "description": null, "image": null, "icon": null, "is_active": true, "sort_order": 0, "created_at": "2024-01-01T00:00:00.000000Z", "updated_at": "2024-06-15T10:30:00.000000Z" } ], "fields": null, "meta": null, "metadata": null, "tags": null, "seo": { "meta_title": null, "meta_description": null, "meta_keywords": null }, "created_at": "2024-06-15T10:30:00.000000Z", "updated_at": "2024-06-15T10:30:00.000000Z" } ] } ``` Featured products are those marked as "noticeable" in the store dashboard. They are returned sorted by newest first. # Product Reviews Source: https://docs.rmz.gg/storefront-api/products/product-reviews Get product reviews and statistics, and submit reviews for purchased products. ## GET /products//reviews Get published reviews for a specific product. ### Authentication None required. ### Path Parameters | Parameter | Type | Description | | --------- | ------- | ----------- | | id | integer | Product ID | ### Query Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | -------------------------------------- | | per\_page | integer | No | Reviews per page (1-50). Default: `10` | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/products/101/reviews?per_page=10" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/products/101/reviews?per_page=10"); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/products/101/reviews", params={"per_page": 10}) data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/products/101/reviews", ["per_page" => 10]); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": [ { "id": 1, "rating": 5, "comment": "Excellent product, received instantly!", "is_published": true, "reviewer": { "id": 123, "name": "Ahmed Ali", "first_name": "Ahmed", "last_name": "Ali" }, "product": { "id": 101, "type": "App\\Models\\StoreProduct" }, "created_at": "2024-06-15T10:30:00.000000Z", "updated_at": "2024-06-15T10:30:00.000000Z" } ], "pagination": { "current_page": 1, "last_page": 3, "per_page": 10, "total": 25, "from": 1, "to": 10, "has_more_pages": true, "next_page_url": "...", "prev_page_url": null } } ``` #### Error Responses | Status | Description | | ------ | ----------------- | | 404 | Product not found | *** ## GET /products//reviews/stats Get review statistics for a specific product including total count, average rating, and distribution. ### Authentication None required. ### Path Parameters | Parameter | Type | Description | | --------- | ------- | ----------- | | id | integer | Product ID | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/products/101/reviews/stats" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/products/101/reviews/stats"); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/products/101/reviews/stats") data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/products/101/reviews/stats"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "total_reviews": 142, "average_rating": 4.7, "rating_distribution": { "1": 2, "2": 5, "3": 15, "4": 38, "5": 82 } } } ``` #### Error Responses | Status | Description | | ------ | ----------------- | | 404 | Product not found | *** ## POST /products//reviews Submit a review for a product. The customer must have purchased and received the product (completed order). ### Authentication Requires Bearer token (`auth:customer_api`). ### Path Parameters | Parameter | Type | Description | | --------- | ------- | ----------- | | id | integer | Product ID | ### Headers | Header | Value | Required | | ------------- | ---------------- | -------- | | Authorization | Bearer | Yes | | Content-Type | application/json | Yes | ### Body Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | --------------------------------- | | rating | integer | Yes | Rating from 1 to 5 | | comment | string | Yes | Review text (max 1000 characters) | ### Example Request ```bash cURL theme={null} curl -X POST "https://front.rmz.gg/api/products/101/reviews" \ -H "Authorization: Bearer 1|abc123xyz..." \ -H "Content-Type: application/json" \ -d '{ "rating": 5, "comment": "Excellent product, received instantly!" }' ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/products/101/reviews", { method: "POST", headers: { "Authorization": "Bearer 1|abc123xyz...", "Content-Type": "application/json" }, body: JSON.stringify({ rating: 5, comment: "Excellent product, received instantly!" }) }); const data = await response.json(); ``` ```python Python theme={null} response = requests.post("https://front.rmz.gg/api/products/101/reviews", headers={"Authorization": "Bearer 1|abc123xyz..."}, json={"rating": 5, "comment": "Excellent product, received instantly!"} ) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->post("https://front.rmz.gg/api/products/101/reviews", [ "rating" => 5, "comment" => "Excellent product, received instantly!" ]); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "id": 50, "rating": 5, "comment": "Excellent product, received instantly!", "is_published": false, "reviewer": null, "product": { "id": 101, "type": "App\\Models\\StoreProduct" }, "created_at": "2024-06-15T10:30:00.000000Z", "updated_at": "2024-06-15T10:30:00.000000Z" }, "message": "Review submitted successfully" } ``` Reviews are submitted with `is_published: false` and require store owner approval before they appear publicly. #### Error Responses | Status | Description | | ------ | ----------------------------------------------------------------------- | | 400 | Already reviewed this product | | 401 | Not authenticated | | 403 | Customer has not purchased this product (only completed orders qualify) | | 404 | Product not found | | 422 | Validation error (missing rating/comment, rating out of range) | *** ## GET /customer/reviews/ Get the authenticated customer's review for a specific product. ### Authentication Requires Bearer token (`auth:customer_api`). ### Path Parameters | Parameter | Type | Description | | --------- | ------- | ----------- | | id | integer | Product ID | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/customer/reviews/101" \ -H "Authorization: Bearer 1|abc123xyz..." ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/customer/reviews/101", { headers: { "Authorization": "Bearer 1|abc123xyz..." } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/customer/reviews/101", headers={ "Authorization": "Bearer 1|abc123xyz..." }) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->get("https://front.rmz.gg/api/customer/reviews/101"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "id": 50, "rating": 5, "comment": "Excellent product, received instantly!", "is_published": true, "reviewer": null, "product": { "id": 101, "type": "App\\Models\\StoreProduct" }, "created_at": "2024-06-15T10:30:00.000000Z", "updated_at": "2024-06-15T10:30:00.000000Z" } } ``` #### Error Responses | Status | Description | | ------ | ----------------- | | 401 | Not authenticated | | 404 | Review not found | # Store Reviews Source: https://docs.rmz.gg/storefront-api/reviews Get store-wide reviews, recent reviews, and review statistics. These public endpoints return reviews at the store level. For product-specific reviews, see [Product Reviews](/storefront-api/products/product-reviews). *** ## GET /reviews/recent Get recent high-rated reviews (4+ stars). Useful for homepage testimonial sections. ### Authentication None required. ### Query Parameters | Parameter | Type | Required | Description | | --------- | ------- | -------- | ----------------------------------------------------------------------------------------------- | | limit | integer | No | Number of reviews (1-50). Default: `6` | | type | string | No | Filter by review type: `store` (order reviews) or `product` (product reviews). Default: `store` | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/reviews/recent?limit=6&type=store" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/reviews/recent?limit=6&type=store"); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/reviews/recent", params={ "limit": 6, "type": "store" }) data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/reviews/recent", [ "limit" => 6, "type" => "store" ]); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": [ { "id": 1, "rating": 5, "comment": "Amazing store with fast delivery!", "created_at": "2024-06-15T10:30:00.000000Z", "reviewer": { "id": 123, "name": "Ahmed Ali" }, "product": null }, { "id": 2, "rating": 4, "comment": "Great quality products", "created_at": "2024-06-14T08:15:00.000000Z", "reviewer": { "id": 456, "name": "Sara Mohammed" }, "product": { "id": 101, "name": "Premium Game Key", "slug": "premium-game-key" } } ] } ``` When `type=store`, only order-level reviews (not associated with a specific product) are returned. When `type=product`, only product-specific reviews are returned. *** ## GET /reviews List all published reviews with filtering, sorting, and pagination. ### Authentication None required. ### Query Parameters | Parameter | Type | Required | Description | | ----------- | ------- | -------- | ---------------------------------------------------------------------- | | per\_page | integer | No | Reviews per page (1-50). Default: `15` | | rating | integer | No | Filter by exact rating (1-5) | | type | string | No | Filter: `store` (order reviews) or `product` (product reviews) | | product\_id | integer | No | Filter by specific product ID | | sort | string | No | Sort order: `newest`, `oldest`, `highest`, `lowest`. Default: `newest` | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/reviews?sort=highest&per_page=10" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/reviews?sort=highest&per_page=10"); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/reviews", params={ "sort": "highest", "per_page": 10 }) data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/reviews", [ "sort" => "highest", "per_page" => 10 ]); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": [ { "id": 1, "rating": 5, "comment": "Amazing store with fast delivery!", "created_at": "2024-06-15T10:30:00.000000Z", "reviewer": { "id": 123, "name": "Ahmed Ali" }, "product": null } ], "pagination": { "current_page": 1, "last_page": 5, "per_page": 10, "total": 48, "from": 1, "to": 10, "has_more_pages": true, "next_page_url": "...", "prev_page_url": null } } ``` *** ## GET /reviews/stats Get aggregate review statistics for the store. ### Authentication None required. ### Query Parameters | Parameter | Type | Required | Description | | ----------- | ------- | -------- | -------------------------------- | | type | string | No | Filter: `store` or `product` | | product\_id | integer | No | Get stats for a specific product | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/reviews/stats" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/reviews/stats"); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/reviews/stats") data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/reviews/stats"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "total_reviews": 142, "average_rating": 4.7, "rating_distribution": { "1": 2, "2": 5, "3": 15, "4": 38, "5": 82 } } } ``` Use the `rating_distribution` to render star distribution bars on your storefront. The keys represent star counts (1-5) and the values represent the number of reviews with that rating. # Store Information Source: https://docs.rmz.gg/storefront-api/store Retrieve store details, settings, currencies, features, and banners. These public endpoints return information about the store resolved from your request's origin domain. No authentication is required. *** ## GET /store Get store information including categories, pages, announcements, and payment methods. ### Authentication None required. ### Query Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------------------------------------------------------------------- | | include | string | No | Comma-separated list of relations to include. Default: `categories,pages,announcements,payment_methods` | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/store" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/store"); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/store") data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/store"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "id": 1, "name": "My Store", "description": "Welcome to my store", "short_description": null, "logo": "https://cdn.rmz.gg/...", "favicon": "https://cdn.rmz.gg/...", "cover_url": null, "domain": { "domain": "mystore", "is_active": true }, "custom_domain": null, "currency": "SAR", "default_currency": "SAR", "timezone": "Asia/Riyadh", "language": "ar", "status": 1, "is_maintenance": false, "maintenance_message": null, "theme": { "color": "#3B82F6", "font_family": "Inter", "enable_rtl": true, "layout": "default" }, "features": { "reviews_enabled": true, "wishlist_enabled": true, "courses_enabled": false, "subscriptions_enabled": true, "coupons_enabled": true }, "seo": { "title": "My Store", "description": "Welcome to my store", "keywords": null }, "contact_info": { "email": "support@mystore.com", "phone": null, "address": null, "working_hours": null }, "social_links": {}, "payment_methods": [ { "id": 1, "method": "card", "nickname": "Credit Card", "is_enabled": true, "is_auto_process": true } ], "sbc_id": null, "categories": [ { "id": 1, "name": "Electronics", "slug": "electronics", "description": null, "image": null, "icon": null, "is_active": true, "sort_order": 0, "products_count": 24, "created_at": "2024-01-01T00:00:00.000000Z", "updated_at": "2024-06-15T10:30:00.000000Z" } ], "pages": [ { "id": 1, "title": "About Us", "url": "about-us", "content": "

About Our Store

...

", "excerpt": "Learn more about our store", "meta_title": "About Us", "meta_description": "Learn more about our store", "is_active": true, "created_at": "2024-01-01T00:00:00.000000Z", "updated_at": "2024-06-15T10:30:00.000000Z" } ], "announcements": [ { "id": 1, "icon": "megaphone", "content": "Free shipping on orders over 100 SAR", "color": "#3B82F6", "text_color": "#FFFFFF", "route": null, "href": null, "is_enabled": true } ], "components": [ { "id": 1, "type": "featured_products", "name": "Featured Products", "sort_order": 0, "settings": {} } ], "features_list": [ { "id": 1, "title": "Instant Delivery", "description": "Get your products instantly", "icon": "zap", "sort_order": 0 } ], "banners": [ { "id": 1, "title": "Summer Sale", "description": "Up to 50% off", "image_url": "https://cdn.rmz.gg/...", "link_url": "/products", "sort_order": 0 } ], "stats": { "total_products": null, "total_categories": null, "total_reviews": null, "average_rating": null }, "created_at": "2024-01-01 00:00:00", "updated_at": "2024-06-15 10:30:00" } } ``` Use the `include` parameter to request only the data you need. For example, `?include=categories` to load only categories. The following fields are **conditional** and only appear in the response when their corresponding relations are loaded via the `include` parameter or when the data is available: * `payment_methods` - included when `payment_methods` is in the `include` list * `categories` - included when `categories` is in the `include` list (uses [CategoryResource](/storefront-api/categories)) * `pages` - included when `pages` is in the `include` list (uses [PageResource](/storefront-api/pages)) * `announcements` - included when `announcements` is in the `include` list * `components` - included when `components` relation is loaded * `features_list` - included when `features` relation is loaded * `banners` - included when `banners` relation is loaded * `stats.total_products`, `stats.total_categories`, `stats.total_reviews`, `stats.average_rating` - included when corresponding relations are loaded *** ## GET /store/currencies Get available currencies with exchange rates. ### Authentication None required. ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/store/currencies" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/store/currencies"); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/store/currencies") data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/store/currencies"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": [ { "id": 1, "code": "SAR", "symbol": "ر.س", "name": "Saudi Riyal", "rate": 1.0, "is_default": true }, { "id": 2, "code": "USD", "symbol": "$", "name": "US Dollar", "rate": 0.2667, "is_default": false } ] } ``` *** ## GET /store/settings Get frontend-related store settings including theme, fonts, SEO configuration, and contact information. ### Authentication None required. ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/store/settings" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/store/settings"); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/store/settings") data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/store/settings"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "theme_color": "#3B82F6", "font_family": "Inter", "enable_rtl": false, "enable_reviews": true, "enable_wishlist": true, "social_links": { "twitter": "https://twitter.com/mystore", "instagram": "https://instagram.com/mystore" }, "contact_info": { "email": "support@mystore.com", "phone": "+966501234567", "address": "Riyadh, Saudi Arabia" }, "seo": { "meta_title": "My Store - Best Digital Products", "meta_description": "Shop the best digital products", "meta_keywords": "digital, products, store" } } } ``` *** ## GET /store/features Get store feature highlights (marketing features displayed on the storefront). ### Authentication None required. ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/store/features" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/store/features"); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/store/features") data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/store/features"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": [ { "id": 1, "title": "Instant Delivery", "description": "Get your digital products instantly after purchase" }, { "id": 2, "title": "Secure Payments", "description": "All transactions are encrypted and secure" } ] } ``` *** ## GET /store/banners Get promotional banners configured for the store. ### Authentication None required. ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/store/banners" ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/store/banners"); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/store/banners") data = response.json() ``` ```php PHP theme={null} $response = Http::get("https://front.rmz.gg/api/store/banners"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": [ { "id": 1, "type": "single" }, { "id": 2, "type": "carousel" } ] } ``` #### Error Responses | Status | Description | | ------ | --------------- | | 404 | Store not found | # Wishlist Source: https://docs.rmz.gg/storefront-api/wishlist Manage a customer's product wishlist. All wishlist endpoints require customer authentication. *** ## GET /wishlist Get the authenticated customer's wishlist. ### Authentication Requires Bearer token (`auth:customer_api`). ### Headers | Header | Value | Required | | ------------- | ------- | -------- | | Authorization | Bearer | Yes | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/wishlist" \ -H "Authorization: Bearer 1|abc123xyz..." ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/wishlist", { headers: { "Authorization": "Bearer 1|abc123xyz..." } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/wishlist", headers={ "Authorization": "Bearer 1|abc123xyz..." }) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->get("https://front.rmz.gg/api/wishlist"); $data = $response->json(); ``` ### Response #### Success (200) Each item in the `items` array uses the full `ProductResource` format (see [List Products](/storefront-api/products/list-products) for the complete schema). Abbreviated example: ```json theme={null} { "success": true, "data": { "items": [ { "id": 101, "name": "Premium Game Key", "slug": "premium-game-key", "type": "code", "price": { "original": 199.99, "actual": 199.99, "formatted": "199.99 ر.س", "currency": "SAR" }, "stock": { "is_in_stock": true }, "image": { "url": "https://..." }, "categories": [ { "id": 1, "name": "Games", "slug": "games" } ] } ], "count": 1 } } ``` *** ## POST /wishlist Add a product to the wishlist. ### Authentication Requires Bearer token (`auth:customer_api`). ### Headers | Header | Value | Required | | ------------- | ---------------- | -------- | | Authorization | Bearer | Yes | | Content-Type | application/json | Yes | ### Body Parameters | Parameter | Type | Required | Description | | ----------- | ------- | -------- | --------------------------------------------- | | product\_id | integer | Yes | Product ID (must belong to the current store) | ### Example Request ```bash cURL theme={null} curl -X POST "https://front.rmz.gg/api/wishlist" \ -H "Authorization: Bearer 1|abc123xyz..." \ -H "Content-Type: application/json" \ -d '{"product_id": 101}' ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/wishlist", { method: "POST", headers: { "Authorization": "Bearer 1|abc123xyz...", "Content-Type": "application/json" }, body: JSON.stringify({ product_id: 101 }) }); const data = await response.json(); ``` ```python Python theme={null} response = requests.post("https://front.rmz.gg/api/wishlist", headers={"Authorization": "Bearer 1|abc123xyz..."}, json={"product_id": 101} ) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->post("https://front.rmz.gg/api/wishlist", [ "product_id" => 101 ]); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": null, "message": "Product added to wishlist successfully" } ``` #### Error Responses | Status | Description | | ------ | -------------------------------------- | | 400 | Product already in wishlist | | 401 | Not authenticated | | 422 | Validation error (invalid product\_id) | *** ## DELETE /wishlist/ Remove a product from the wishlist. ### Authentication Requires Bearer token (`auth:customer_api`). ### Path Parameters | Parameter | Type | Description | | --------- | ------- | -------------------- | | productId | integer | Product ID to remove | ### Example Request ```bash cURL theme={null} curl -X DELETE "https://front.rmz.gg/api/wishlist/101" \ -H "Authorization: Bearer 1|abc123xyz..." ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/wishlist/101", { method: "DELETE", headers: { "Authorization": "Bearer 1|abc123xyz..." } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.delete("https://front.rmz.gg/api/wishlist/101", headers={ "Authorization": "Bearer 1|abc123xyz..." }) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->delete("https://front.rmz.gg/api/wishlist/101"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": null, "message": "Product removed from wishlist successfully" } ``` #### Error Responses | Status | Description | | ------ | ----------------------------- | | 401 | Not authenticated | | 404 | Product not found in wishlist | *** ## GET /wishlist/check/ Check if a specific product is in the customer's wishlist. ### Authentication Requires Bearer token (`auth:customer_api`). ### Path Parameters | Parameter | Type | Description | | --------- | ------- | ------------------- | | productId | integer | Product ID to check | ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/wishlist/check/101" \ -H "Authorization: Bearer 1|abc123xyz..." ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/wishlist/check/101", { headers: { "Authorization": "Bearer 1|abc123xyz..." } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/wishlist/check/101", headers={ "Authorization": "Bearer 1|abc123xyz..." }) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->get("https://front.rmz.gg/api/wishlist/check/101"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "in_wishlist": true } } ``` *** ## GET /wishlist/count Get the total number of items in the customer's wishlist. ### Authentication Requires Bearer token (`auth:customer_api`). ### Example Request ```bash cURL theme={null} curl "https://front.rmz.gg/api/wishlist/count" \ -H "Authorization: Bearer 1|abc123xyz..." ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/wishlist/count", { headers: { "Authorization": "Bearer 1|abc123xyz..." } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.get("https://front.rmz.gg/api/wishlist/count", headers={ "Authorization": "Bearer 1|abc123xyz..." }) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->get("https://front.rmz.gg/api/wishlist/count"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": { "count": 5 } } ``` *** ## DELETE /wishlist/clear Remove all items from the wishlist. ### Authentication Requires Bearer token (`auth:customer_api`). ### Example Request ```bash cURL theme={null} curl -X DELETE "https://front.rmz.gg/api/wishlist/clear" \ -H "Authorization: Bearer 1|abc123xyz..." ``` ```javascript JavaScript theme={null} const response = await fetch("https://front.rmz.gg/api/wishlist/clear", { method: "DELETE", headers: { "Authorization": "Bearer 1|abc123xyz..." } }); const data = await response.json(); ``` ```python Python theme={null} response = requests.delete("https://front.rmz.gg/api/wishlist/clear", headers={ "Authorization": "Bearer 1|abc123xyz..." }) data = response.json() ``` ```php PHP theme={null} $response = Http::withToken("1|abc123xyz...")->delete("https://front.rmz.gg/api/wishlist/clear"); $data = $response->json(); ``` ### Response #### Success (200) ```json theme={null} { "success": true, "data": null, "message": "Wishlist cleared successfully" } ``` # Authentication Source: https://docs.rmz.gg/storefront-sdk/authentication How the Storefront SDK authenticates requests using public keys, HMAC-SHA256 signatures, and Bearer tokens. The SDK uses a layered authentication model. Public endpoints only need a `publicKey`. Authenticated customer endpoints need a Bearer token. Server-side operations use HMAC-SHA256 signatures generated from a `secretKey`. ## Authentication Layers ### 1. Public Key (client-side) Every request includes an `X-Public-Key` header that identifies the store. This is all you need for public endpoints like product listing, store info, and cart management. ```typescript theme={null} const sdk = createStorefrontSDK({ publicKey: 'pk_your_public_key_here', // No secretKey — safe for browser usage }); ``` Headers sent automatically: ``` X-Public-Key: pk_your_public_key_here X-Timestamp: 1711814400 X-Signature-Version: v1 X-Client-Auth: true ``` ### 2. HMAC-SHA256 Signatures (server-side) When a `secretKey` is provided, the SDK generates an HMAC-SHA256 signature for every request. This is required for server-side operations and the Management API. ```typescript theme={null} const sdk = createStorefrontSDK({ publicKey: 'pk_your_public_key_here', secretKey: 'sk_your_secret_key_here', // enables HMAC }); ``` The signature is computed over a payload that includes: ``` {signatureVersion}\n{timestamp}\n{METHOD}\n{path}\n{bodyHash} ``` Headers sent automatically: ``` X-Public-Key: pk_your_public_key_here X-Timestamp: 1711814400 X-Signature-Version: v1 X-Signature: a1b2c3d4e5f6... ``` The SDK uses constant-time string comparison when verifying signatures to prevent timing attacks. Timestamps are validated within a 5-minute tolerance window to prevent replay attacks. ### 3. Bearer Token (customer authentication) After a customer logs in via the OTP flow, you receive a Bearer token. Set it on the SDK instance to unlock authenticated endpoints (orders, wishlist, profile, courses): ```typescript theme={null} // After successful OTP verification const { token, customer } = await sdk.auth.verifyOTP(otp, sessionToken); // Set the token for subsequent requests sdk.setAuthToken(token); // Now authenticated endpoints work const orders = await sdk.orders.getAll(); const profile = await sdk.auth.getProfile(); ``` To log out and clear the token: ```typescript theme={null} await sdk.auth.logout(); sdk.setAuthToken(null); ``` ### 4. Cart Token (guest sessions) Unauthenticated visitors get a cart token when they first interact with the cart. The SDK manages this automatically — cart tokens are extracted from API responses and included in subsequent requests via the `X-Cart-Token` header. ```typescript theme={null} // Cart token is handled automatically const cart = await sdk.cart.addItem(productId, 1); // sdk internally stores the cart_token from the response // You can also manage it manually sdk.setCartToken('cart_abc123'); const currentToken = sdk.getCartToken(); ``` ## Security Best Practices ### Separate client and server configs ```typescript theme={null} // Client-side config (browser-safe) const clientSDK = createStorefrontSDK({ publicKey: process.env.NEXT_PUBLIC_RMZ_PUBLIC_KEY!, apiUrl: 'https://front.rmz.gg/api', }); // Server-side config (Node.js only) const serverSDK = createStorefrontSDK({ publicKey: process.env.RMZ_PUBLIC_KEY!, secretKey: process.env.RMZ_SECRET_KEY!, // NEVER expose in browser apiUrl: 'https://front.rmz.gg/api', }); ``` The SDK warns in the console if it detects a `secretKey` in a browser environment. This is a security risk — secret keys must only exist on the server. ### Environment variables ```bash Next.js theme={null} # .env.local NEXT_PUBLIC_RMZ_PUBLIC_KEY=pk_your_public_key RMZ_PUBLIC_KEY=pk_your_public_key RMZ_SECRET_KEY=sk_your_secret_key ``` ```bash Vite theme={null} # .env VITE_RMZ_PUBLIC_KEY=pk_your_public_key ``` ```bash Node.js theme={null} # .env RMZ_PUBLIC_KEY=pk_your_public_key RMZ_SECRET_KEY=sk_your_secret_key ``` ### HTTPS only Always use HTTPS URLs in production. The SDK does not enforce this, but transmitting keys over HTTP is insecure. ```typescript theme={null} // Correct const sdk = createStorefrontSDK({ apiUrl: 'https://front.rmz.gg/api', publicKey: 'pk_your_key', }); ``` ## Data Sanitization The SDK automatically removes sensitive fields from API responses on the client side. Fields like `password`, `secret`, `api_key`, `private_key`, and `webhook_secret` are stripped. Legitimate tokens such as `session_token`, `access_token`, `cart_token`, and `token` are preserved. ```typescript theme={null} const profile = await sdk.auth.getProfile(); // Sensitive server fields are never exposed to the client ``` ## Advanced: Direct Security Manager For custom integrations, you can use the `SecurityManager` and `UniversalHttpClient` directly: ```typescript theme={null} import { SecurityManager } from 'rmz-storefront-sdk'; const security = new SecurityManager({ publicKey: 'pk_your_key', secretKey: 'sk_your_key', // server-side only signatureVersion: 'v1', timestampTolerance: 300 }); // Generate a signature manually const signature = await security.generateSignature( timestamp, 'POST', '/checkout', JSON.stringify(body) ); ``` # Cart Source: https://docs.rmz.gg/storefront-sdk/cart Manage the shopping cart — add items, apply coupons, validate, and get summaries using sdk.cart. The `sdk.cart` namespace manages the shopping cart. Cart operations work for both authenticated customers and guest visitors. The SDK automatically handles cart token management — tokens are extracted from API responses and sent with subsequent requests via the `X-Cart-Token` header. ## Methods ### `cart.get()` Retrieve the current cart contents. ```typescript theme={null} const cart = await sdk.cart.get(); console.log(cart.items); // CartItem[] console.log(cart.total); // number console.log(cart.currency); // string ``` **Returns:** `Cart` ```typescript theme={null} interface Cart { items: CartItem[]; count: number; subtotal: number; total: number; currency: string; } interface CartItem { id: number; product_id: number; product: Product; quantity: number; price: number; total: number; } ``` *** ### `cart.addItem(productId, quantity?, options?)` Add a product to the cart. ```typescript theme={null} // Add 1 unit const cart = await sdk.cart.addItem(productId); // Add 2 units with custom fields const cart = await sdk.cart.addItem(productId, 2, { fields: { color: 'red', size: 'L' }, notice: 'Gift wrap please' }); ``` **Parameters:** | Field | Type | Default | Description | | ---------------- | --------------------- | ------- | ----------------------------------------- | | `productId` | `number` | — | Product ID to add | | `quantity` | `number` | `1` | Quantity to add | | `options.fields` | `Record` | — | Custom product fields (variants, options) | | `options.notice` | `string` | — | Special instructions or notes | **Returns:** `Cart` *** ### `cart.updateItem(itemId, quantity)` Update the quantity of a cart item. ```typescript theme={null} await sdk.cart.updateItem(itemId, 3); ``` **Parameters:** | Field | Type | Description | | ---------- | -------- | ------------ | | `itemId` | `string` | Cart item ID | | `quantity` | `number` | New quantity | **Returns:** `Cart` *** ### `cart.removeItem(itemId)` Remove an item from the cart. ```typescript theme={null} await sdk.cart.removeItem(itemId); ``` **Returns:** `Cart` *** ### `cart.clear()` Remove all items from the cart and clear the stored cart token. ```typescript theme={null} await sdk.cart.clear(); ``` **Returns:** `void` *** ### `cart.getCount()` Get the number of items in the cart without fetching the full cart. ```typescript theme={null} const count = await sdk.cart.getCount(); // 3 ``` **Returns:** `number` *** ### `cart.applyCoupon(code)` Apply a discount coupon to the cart. ```typescript theme={null} const cart = await sdk.cart.applyCoupon('DISCOUNT10'); ``` **Parameters:** | Field | Type | Description | | ------ | -------- | ----------- | | `code` | `string` | Coupon code | **Returns:** `Cart` If the coupon is invalid or expired, the API returns an error. Wrap this call in a try/catch to display the error message to the customer. *** ### `cart.removeCoupon()` Remove the currently applied coupon. ```typescript theme={null} const cart = await sdk.cart.removeCoupon(); ``` **Returns:** `Cart` *** ### `cart.validate()` Check whether the current cart contents are valid for checkout (e.g., all items in stock, no conflicts). ```typescript theme={null} const validation = await sdk.cart.validate(); if (!validation.valid) { console.error('Cart issues:', validation.errors); } ``` **Returns:** ```typescript theme={null} { valid: boolean; errors?: string[] } ``` *** ### `cart.getSummary()` Get the cart totals breakdown without fetching individual items. ```typescript theme={null} const summary = await sdk.cart.getSummary(); // { subtotal: 100, tax: 15, shipping: 0, discount: 10, total: 105 } ``` **Returns:** ```typescript theme={null} { subtotal: number; tax: number; shipping: number; discount: number; total: number; } ``` ## Cart Token Management The SDK manages cart tokens automatically, but you can also control them manually: ```typescript theme={null} // Set a cart token (e.g., restored from localStorage) sdk.setCartToken('cart_abc123'); // Read the current cart token const token = sdk.getCartToken(); // Clear the cart token sdk.setCartToken(null); ``` For guest users, persist the cart token in `localStorage` so the cart survives page reloads: ```typescript theme={null} // After any cart operation const token = sdk.getCartToken(); if (token) localStorage.setItem('rmz_cart_token', token); // On app initialization const saved = localStorage.getItem('rmz_cart_token'); if (saved) sdk.setCartToken(saved); ``` ## Example: Add to Cart Flow ```typescript theme={null} try { const cart = await sdk.cart.addItem(product.id, quantity, { fields: selectedOptions, }); // Cart token is automatically stored by the SDK console.log(`Cart now has ${cart.count} items, total: ${cart.total} ${cart.currency}`); } catch (error) { // Handle errors (out of stock, invalid product, etc.) console.error('Failed to add item:', error.message); } ``` # Checkout Source: https://docs.rmz.gg/storefront-sdk/checkout Create checkout sessions and handle payment results using sdk.checkout. The `sdk.checkout` namespace handles the checkout flow. After the customer's cart is ready, create a checkout session to initiate payment. Once payment is complete, retrieve the result. ## Methods ### `checkout.create()` Create a new checkout session from the current cart. The response varies based on the order total: ```typescript theme={null} const result = await sdk.checkout.create(); if (result.type === 'free_order') { // No payment needed — order is created immediately console.log('Order created:', result.order_id); } else if (result.type === 'payment_required') { // Redirect the customer to the payment page window.location.href = result.checkout_url; } ``` **Returns:** ```typescript theme={null} { type: 'free_order' | 'payment_required'; checkout_id?: string; // Payment session ID checkout_url?: string; // URL to redirect customer to order_id?: number; // Order ID (for free orders) amount?: number; // Payment amount redirect_url?: string; // Post-payment redirect URL } ``` For free orders (100% discount, free products), the order is created immediately and no payment redirect is needed. For paid orders, redirect the customer to `checkout_url` to complete payment. *** ### `checkout.getResult(sessionId)` After the customer returns from payment, check the result of the checkout session. ```typescript theme={null} const { status, order } = await sdk.checkout.getResult(sessionId); if (status === 'completed') { console.log('Payment successful! Order:', order.id); } else { console.log('Payment status:', status); } ``` **Parameters:** | Field | Type | Description | | ----------- | -------- | ------------------------------------------------- | | `sessionId` | `string` | The `checkout_id` returned by `checkout.create()` | **Returns:** ```typescript theme={null} { status: string; // e.g., 'completed', 'pending', 'failed' order?: Order; // The created order (if completed) } ``` ## Complete Checkout Flow ```typescript theme={null} // 1. Validate the cart const validation = await sdk.cart.validate(); if (!validation.valid) { throw new Error('Cart validation failed: ' + validation.errors?.join(', ')); } // 2. Create checkout session const result = await sdk.checkout.create(); // 3. Handle the result switch (result.type) { case 'free_order': // Navigate to order confirmation router.push(`/orders/${result.order_id}`); break; case 'payment_required': // Redirect to payment gateway window.location.href = result.checkout_url!; break; } // 4. On the return/callback page, check the result const sessionId = new URLSearchParams(window.location.search).get('session_id'); if (sessionId) { const { status, order } = await sdk.checkout.getResult(sessionId); if (status === 'completed') { // Show success page } } ``` Ensure the customer is either authenticated (Bearer token set) or has a valid cart token before calling `checkout.create()`. The API needs to identify the cart to process. # Configuration Source: https://docs.rmz.gg/storefront-sdk/configuration All configuration options for the Storefront SDK — API URL, keys, timeouts, retries, caching, and logging. The `createStorefrontSDK()` function accepts a configuration object that controls authentication, network behavior, and debugging. ## Configuration Interface ```typescript theme={null} interface StorefrontConfig { apiUrl: string; // API base URL publicKey: string; // Public key for store identification secretKey?: string; // Secret key (server-side only, enables HMAC) environment?: 'production' | 'development'; // Default: 'production' version?: string; // API version (default: '1.0.0') timeout?: number; // Request timeout in ms (default: 30000) maxRetries?: number; // Maximum retry attempts (default: 3) retryDelay?: number; // Delay between retries in ms (default: 1000) enableLogging?: boolean; // Enable debug logging (default: false) } ``` ## Options Reference ### `apiUrl` **Required.** The base URL of the Storefront API. The default and most common value is `https://front.rmz.gg/api`. This is the production endpoint used by all RMZ stores. ```typescript theme={null} const sdk = createStorefrontSDK({ apiUrl: 'https://front.rmz.gg/api', publicKey: 'pk_...', }); ``` *** ### `publicKey` **Required.** Your store's public key. Found in the store dashboard under API settings. This key is safe to expose in client-side code. *** ### `secretKey` **Optional. Server-side only.** Your store's secret key. When provided, the SDK generates HMAC-SHA256 signatures for every request. Required for Management API endpoints. Never include `secretKey` in client-side code. The SDK will log a warning if it detects a secret key in a browser environment. *** ### `environment` **Optional.** Default: `'production'`. Controls environment-specific behavior. In `'development'` mode, error messages may be more verbose. *** ### `timeout` **Optional.** Default: `30000` (30 seconds). Maximum time in milliseconds to wait for a response before the request is aborted. ```typescript theme={null} const sdk = createStorefrontSDK({ apiUrl: 'https://front.rmz.gg/api', publicKey: 'pk_...', timeout: 15000, // 15 seconds }); ``` *** ### `maxRetries` **Optional.** Default: `3`. Number of times to retry a failed request before throwing an error. Retries are triggered on network errors and 5xx server errors. *** ### `retryDelay` **Optional.** Default: `1000` (1 second). Delay in milliseconds between retry attempts. *** ### `enableLogging` **Optional.** Default: `false`. When enabled, the SDK logs internal operations to the console (initialization, token updates, request details). Useful during development. ```typescript theme={null} const sdk = createStorefrontSDK({ apiUrl: 'https://front.rmz.gg/api', publicKey: 'pk_...', enableLogging: true, // logs to console }); ``` ## Singleton Pattern The SDK uses a singleton pattern internally. Calling `createStorefrontSDK()` with the same `apiUrl` and `publicKey` returns the same instance: ```typescript theme={null} const sdk1 = createStorefrontSDK(config); const sdk2 = createStorefrontSDK(config); // sdk1 === sdk2 (same instance) ``` This prevents redundant initialization and ensures consistent state (auth tokens, cart tokens) across your application. ## Environment Variables Recommended environment variable setup for common frameworks: ```bash Next.js (.env.local) theme={null} # Client-side (prefixed with NEXT_PUBLIC_) NEXT_PUBLIC_RMZ_API_URL=https://front.rmz.gg/api NEXT_PUBLIC_RMZ_PUBLIC_KEY=pk_your_public_key # Server-side RMZ_API_URL=https://front.rmz.gg/api RMZ_PUBLIC_KEY=pk_your_public_key RMZ_SECRET_KEY=sk_your_secret_key ``` ```bash Vite (.env) theme={null} VITE_RMZ_API_URL=https://front.rmz.gg/api VITE_RMZ_PUBLIC_KEY=pk_your_public_key ``` ```bash Node.js (.env) theme={null} RMZ_API_URL=https://front.rmz.gg/api RMZ_PUBLIC_KEY=pk_your_public_key RMZ_SECRET_KEY=sk_your_secret_key ``` ## Full Example ```typescript theme={null} import { createStorefrontSDK } from 'rmz-storefront-sdk'; // Client-side (Next.js) const clientSDK = createStorefrontSDK({ apiUrl: process.env.NEXT_PUBLIC_RMZ_API_URL || 'https://front.rmz.gg/api', publicKey: process.env.NEXT_PUBLIC_RMZ_PUBLIC_KEY!, environment: process.env.NODE_ENV === 'production' ? 'production' : 'development', timeout: 15000, maxRetries: 2, enableLogging: process.env.NODE_ENV === 'development', }); // Server-side (Next.js API route) const serverSDK = createStorefrontSDK({ apiUrl: process.env.RMZ_API_URL || 'https://front.rmz.gg/api', publicKey: process.env.RMZ_PUBLIC_KEY!, secretKey: process.env.RMZ_SECRET_KEY!, environment: 'production', timeout: 30000, maxRetries: 3, }); ``` ## Error Handling The SDK throws standard JavaScript errors. Handle them with try/catch: ```typescript theme={null} try { const products = await sdk.products.getAll(); } catch (error) { if (error.message.includes('401')) { // Authentication error } else if (error.message.includes('429')) { // Rate limit exceeded } else if (error.message.includes('timeout')) { // Request timed out } else { // Other error console.error('API error:', error.message); } } ``` # Courses Source: https://docs.rmz.gg/storefront-sdk/courses Access purchased courses, track progress, view modules, and mark completion using sdk.courses. The `sdk.courses` namespace provides access to digital courses purchased by the customer. All methods require authentication (Bearer token set via `sdk.setAuthToken()`). ## Methods ### `courses.getAll(params?)` Get a paginated list of courses available to the customer. ```typescript theme={null} const { data: courses, pagination } = await sdk.courses.getAll({ page: 1, per_page: 10 }); ``` **Parameters:** | Field | Type | Default | Description | | ---------- | -------- | ------- | ---------------- | | `page` | `number` | `1` | Page number | | `per_page` | `number` | `10` | Courses per page | **Returns:** `{ data: Course[]; pagination?: Pagination }` *** ### `courses.getById(id)` Get details for a specific course, including its modules. ```typescript theme={null} const course = await sdk.courses.getById(15); console.log(course.title); console.log(course.modules); // CourseModule[] ``` **Returns:** `Course` *** ### `courses.getProgress(courseId)` Get the customer's progress for a specific course. ```typescript theme={null} const progress = await sdk.courses.getProgress(15); console.log(`${progress.progress_percentage}% complete`); console.log(`${progress.completed_modules} of ${progress.total_modules} modules`); ``` **Returns:** `CourseProgress` *** ### `courses.getModule(courseId, moduleId)` Get the content of a specific module within a course. ```typescript theme={null} const module = await sdk.courses.getModule(15, 3); console.log(module.title); console.log(module.content); ``` **Returns:** `CourseModule` *** ### `courses.completeModule(courseId, moduleId)` Mark a module as completed. ```typescript theme={null} const result = await sdk.courses.completeModule(15, 3); if (result.success) { console.log('Module completed!'); } ``` **Returns:** `{ success: boolean }` ## Legacy Customer Endpoints For backward compatibility, the SDK also provides these legacy methods that use the `/customer/courses` endpoints: ```typescript theme={null} // Get all customer courses const courses = await sdk.courses.getCustomerCourses(); // Get a specific customer course const course = await sdk.courses.getCustomerCourse(15); // Get a module via the customer endpoint const module = await sdk.courses.getCustomerCourseModule(15, 3); ``` ## Types ```typescript theme={null} interface Course { id: number; title: string; description: string; modules: CourseModule[]; progress?: CourseProgress; } interface CourseModule { id: number; title: string; content: string; order: number; is_completed?: boolean; } interface CourseProgress { course_id: number; completed_modules: number; total_modules: number; progress_percentage: number; } ``` ## Example: Course Player ```typescript theme={null} async function loadCoursePlayer(courseId: number) { const [course, progress] = await Promise.all([ sdk.courses.getById(courseId), sdk.courses.getProgress(courseId), ]); // Find the next uncompleted module const nextModule = course.modules.find(m => !m.is_completed); return { course, progress, nextModule }; } async function completeAndAdvance(courseId: number, moduleId: number) { await sdk.courses.completeModule(courseId, moduleId); const progress = await sdk.courses.getProgress(courseId); return progress; } ``` # Customer Authentication Source: https://docs.rmz.gg/storefront-sdk/customer Handle customer login via OTP, registration, profile management, and logout using sdk.auth. The `sdk.auth` namespace handles the complete customer authentication lifecycle: phone-based OTP login, registration for new customers, profile retrieval and updates, and logout. ## OTP Authentication Flow RMZ uses phone-based OTP (one-time password) authentication. The flow has three steps: 1. **Start authentication** — send an OTP to the customer's phone 2. **Verify OTP** — confirm the code the customer received 3. **Complete registration** — (new customers only) provide name and email ### Step 1: Start Phone Auth ```typescript theme={null} const { session_token } = await sdk.auth.startPhoneAuth('50505050', '966'); // Store session_token for the next step ``` **Parameters:** | Field | Type | Description | | ------------- | -------- | --------------------------------------------- | | `phone` | `string` | Phone number (without country code) | | `countryCode` | `string` | Country code (e.g., `'966'` for Saudi Arabia) | **Returns:** `{ session_token: string }` *** ### Step 2: Verify OTP ```typescript theme={null} const result = await sdk.auth.verifyOTP('1337', session_token); if (result.token) { // Existing customer — authentication complete sdk.setAuthToken(result.token); console.log('Welcome back,', result.customer.firstName); } else { // New customer — needs to complete registration } ``` **Parameters:** | Field | Type | Description | | -------------- | -------- | ------------------------------------ | | `otp` | `string` | The OTP code entered by the customer | | `sessionToken` | `string` | The `session_token` from step 1 | **Returns:** `{ token: string; customer: Customer }` *** ### Step 2b: Resend OTP If the customer did not receive the code: ```typescript theme={null} await sdk.auth.resendOTP(session_token); ``` *** ### Step 3: Complete Registration (New Customers) For first-time customers, collect their details and complete registration: ```typescript theme={null} const { token, customer } = await sdk.auth.completeRegistration({ firstName: 'Ahmed', lastName: 'Ali', email: 'ahmed@example.com', sessionToken: session_token }); sdk.setAuthToken(token); ``` **Parameters:** | Field | Type | Description | | -------------- | -------- | ------------------------------- | | `firstName` | `string` | Customer's first name | | `lastName` | `string` | Customer's last name | | `email` | `string` | Customer's email address | | `sessionToken` | `string` | The `session_token` from step 1 | **Returns:** `{ token: string; customer: Customer }` ## Profile Management ### `auth.getProfile()` Retrieve the authenticated customer's profile. ```typescript theme={null} const profile = await sdk.auth.getProfile(); console.log(profile.firstName, profile.lastName); console.log(profile.email, profile.phone); ``` **Returns:** `Customer` ```typescript theme={null} interface Customer { id: number; firstName: string; lastName: string; email: string; phone?: string; } ``` *** ### `auth.updateProfile(data)` Update the authenticated customer's profile. ```typescript theme={null} await sdk.auth.updateProfile({ firstName: 'Mohammed', email: 'new-email@example.com' }); ``` **Parameters:** `Partial` — any subset of profile fields. **Returns:** `Customer` *** ### `auth.logout()` Log out the customer and invalidate the current token. ```typescript theme={null} await sdk.auth.logout(); sdk.setAuthToken(null); // Clear the local token ``` **Returns:** `void` ## Token Management After authentication, manage the Bearer token on the SDK instance: ```typescript theme={null} // Set token (after login) sdk.setAuthToken(token); // Check current token const currentToken = sdk.getAuthToken(); // Clear token (after logout) sdk.setAuthToken(null); ``` Persist the auth token in `localStorage` or a secure cookie so customers stay logged in across page reloads: ```typescript theme={null} // After login localStorage.setItem('rmz_auth_token', token); // On app init const saved = localStorage.getItem('rmz_auth_token'); if (saved) sdk.setAuthToken(saved); ``` ## Complete Login Example ```typescript theme={null} async function loginCustomer(phone: string, countryCode: string) { // Step 1: Request OTP const { session_token } = await sdk.auth.startPhoneAuth(phone, countryCode); // Step 2: Prompt user for OTP code (UI dependent) const otpCode = await promptUserForOTP(); // Step 3: Verify const { token, customer } = await sdk.auth.verifyOTP(otpCode, session_token); if (token) { sdk.setAuthToken(token); localStorage.setItem('rmz_auth_token', token); return { customer, isNewUser: false }; } // Step 4: New user — collect details const details = await promptUserForDetails(); const result = await sdk.auth.completeRegistration({ ...details, sessionToken: session_token, }); sdk.setAuthToken(result.token); localStorage.setItem('rmz_auth_token', result.token); return { customer: result.customer, isNewUser: true }; } ``` # Angular Source: https://docs.rmz.gg/storefront-sdk/frameworks/angular Use the Storefront SDK with Angular as an injectable service. In Angular, wrap the SDK in an injectable service to share a single instance across your application. ## Service Setup ```typescript theme={null} // services/storefront.service.ts import { Injectable } from '@angular/core'; import { createStorefrontSDK, type SecureStorefrontSDK } from 'rmz-storefront-sdk'; import { environment } from '../environments/environment'; @Injectable({ providedIn: 'root' }) export class StorefrontService { private sdk: SecureStorefrontSDK; constructor() { this.sdk = createStorefrontSDK({ apiUrl: environment.rmzApiUrl || 'https://front.rmz.gg/api', publicKey: environment.rmzPublicKey, }); } get store() { return this.sdk.store; } get products() { return this.sdk.products; } get cart() { return this.sdk.cart; } get auth() { return this.sdk.auth; } get orders() { return this.sdk.orders; } get wishlist() { return this.sdk.wishlist; } get checkout() { return this.sdk.checkout; } get reviews() { return this.sdk.reviews; } get courses() { return this.sdk.courses; } setAuthToken(token: string | null) { this.sdk.setAuthToken(token); } setCartToken(token: string | null) { this.sdk.setCartToken(token); } } ``` ## Using in Components ```typescript theme={null} // components/product-list.component.ts import { Component, OnInit } from '@angular/core'; import { StorefrontService } from '../services/storefront.service'; @Component({ selector: 'app-product-list', template: `
Loading...

{{ product.name }}

{{ product.price }}

` }) export class ProductListComponent implements OnInit { products: any[] = []; loading = true; constructor(private storefront: StorefrontService) {} async ngOnInit() { try { const { data } = await this.storefront.products.getAll({ per_page: 12 }); this.products = data; } finally { this.loading = false; } } async addToCart(productId: number) { await this.storefront.cart.addItem(productId, 1); } } ``` ## Cart Service Example ```typescript theme={null} // services/cart.service.ts import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; import { StorefrontService } from './storefront.service'; @Injectable({ providedIn: 'root' }) export class CartService { private cartSubject = new BehaviorSubject(null); private countSubject = new BehaviorSubject(0); cart$ = this.cartSubject.asObservable(); count$ = this.countSubject.asObservable(); constructor(private storefront: StorefrontService) {} async addItem(productId: number, quantity = 1) { const cart = await this.storefront.cart.addItem(productId, quantity); this.cartSubject.next(cart); this.countSubject.next(cart.count); return cart; } async refresh() { const cart = await this.storefront.cart.get(); this.cartSubject.next(cart); this.countSubject.next(cart.count); } async applyCoupon(code: string) { const cart = await this.storefront.cart.applyCoupon(code); this.cartSubject.next(cart); return cart; } } ``` ## Environment Configuration ```typescript theme={null} // environments/environment.ts export const environment = { production: false, rmzApiUrl: 'https://front.rmz.gg/api', rmzPublicKey: 'pk_your_public_key', }; ``` ```typescript theme={null} // environments/environment.prod.ts export const environment = { production: true, rmzApiUrl: 'https://front.rmz.gg/api', rmzPublicKey: 'pk_your_public_key', }; ``` # React & Next.js Source: https://docs.rmz.gg/storefront-sdk/frameworks/react Use the Storefront SDK with React and Next.js — hooks, SSR, API routes, and the official Next.js starter. The SDK provides a built-in React hook and works seamlessly with Next.js for both client-side rendering and server-side rendering. Full example storefront built with Next.js, TypeScript, and the Storefront SDK. ## React Hook The SDK exports a `useStorefrontSDK` hook that memoizes the SDK instance: ```tsx theme={null} import { useStorefrontSDK } from 'rmz-storefront-sdk'; import { useEffect, useState } from 'react'; function ProductList() { const sdk = useStorefrontSDK({ apiUrl: process.env.NEXT_PUBLIC_RMZ_API_URL!, publicKey: process.env.NEXT_PUBLIC_RMZ_PUBLIC_KEY!, }); const [products, setProducts] = useState([]); useEffect(() => { async function loadProducts() { const { data } = await sdk.products.getAll({ per_page: 12 }); setProducts(data); } loadProducts(); }, [sdk]); return (
{products.map(product => (

{product.name}

{product.price} {product.currency}

))}
); } ``` ## Next.js Server-side Rendering Use the SDK in `getServerSideProps` or server components with HMAC authentication: ```typescript theme={null} // lib/sdk.ts import { createStorefrontSDK } from 'rmz-storefront-sdk'; // Client-side SDK (no secret key) export const clientSDK = createStorefrontSDK({ apiUrl: process.env.NEXT_PUBLIC_RMZ_API_URL || 'https://front.rmz.gg/api', publicKey: process.env.NEXT_PUBLIC_RMZ_PUBLIC_KEY!, }); // Server-side SDK (with HMAC) export const serverSDK = createStorefrontSDK({ apiUrl: process.env.RMZ_API_URL || 'https://front.rmz.gg/api', publicKey: process.env.RMZ_PUBLIC_KEY!, secretKey: process.env.RMZ_SECRET_KEY!, environment: 'production', }); ``` ### Pages Router (getServerSideProps) ```typescript theme={null} // pages/index.tsx import { serverSDK } from '@/lib/sdk'; export async function getServerSideProps() { const [store, { data: products }] = await Promise.all([ serverSDK.store.get(), serverSDK.products.getFeatured(8), ]); return { props: { store, products }, }; } export default function Home({ store, products }) { return (

{store.name}

{products.map(p => )}
); } ``` ### App Router (Server Components) ```typescript theme={null} // app/page.tsx import { serverSDK } from '@/lib/sdk'; export default async function HomePage() { const store = await serverSDK.store.get(); const featured = await serverSDK.products.getFeatured(8); return (

{store.name}

{featured.map(p => )}
); } ``` ### API Routes ```typescript theme={null} // app/api/cart/route.ts import { serverSDK } from '@/lib/sdk'; import { NextRequest } from 'next/server'; export async function POST(request: NextRequest) { const { productId, quantity } = await request.json(); const cart = await serverSDK.cart.addItem(productId, quantity); return Response.json(cart); } ``` ## Cart Context Example A common pattern is to wrap cart state in a React context: ```tsx theme={null} // contexts/CartContext.tsx import { createContext, useContext, useState, useCallback } from 'react'; import { clientSDK } from '@/lib/sdk'; const CartContext = createContext(null); export function CartProvider({ children }) { const [cart, setCart] = useState(null); const [count, setCount] = useState(0); const addItem = useCallback(async (productId, quantity = 1) => { const updated = await clientSDK.cart.addItem(productId, quantity); setCart(updated); setCount(updated.count); return updated; }, []); const refresh = useCallback(async () => { const current = await clientSDK.cart.get(); setCart(current); setCount(current.count); }, []); return ( {children} ); } export const useCart = () => useContext(CartContext); ``` ## Environment Variables ```bash theme={null} # .env.local NEXT_PUBLIC_RMZ_API_URL=https://front.rmz.gg/api NEXT_PUBLIC_RMZ_PUBLIC_KEY=pk_your_public_key RMZ_API_URL=https://front.rmz.gg/api RMZ_PUBLIC_KEY=pk_your_public_key RMZ_SECRET_KEY=sk_your_secret_key ``` # Vanilla JavaScript Source: https://docs.rmz.gg/storefront-sdk/frameworks/vanilla-js Use the Storefront SDK via CDN or UMD bundle without any framework. The SDK can be used without a framework by loading it via a CDN `
``` When loaded via a script tag, the SDK is available as `window.RMZStorefront`. The `createStorefrontSDK` factory function is the main entry point. ## ES Module Import If you are using ES modules without a bundler: ```html theme={null} ``` ## Complete Example: Product Page ```html theme={null} Store

Loading...

Cart: 0 items
``` ## Node.js (CommonJS) For server-side scripts without ESM: ```javascript theme={null} const { createStorefrontSDK } = require('rmz-storefront-sdk'); const sdk = createStorefrontSDK({ publicKey: process.env.RMZ_PUBLIC_KEY, secretKey: process.env.RMZ_SECRET_KEY, apiUrl: 'https://front.rmz.gg/api', environment: 'production', }); async function main() { const store = await sdk.store.get(); const { data: products } = await sdk.products.getAll({ per_page: 100 }); console.log(`${store.name}: ${products.length} products`); } main(); ``` # Vue 3 Source: https://docs.rmz.gg/storefront-sdk/frameworks/vue Use the Storefront SDK with Vue 3 Composition API and Nuxt. The SDK provides a Vue 3 composable and works with both the Composition API and Nuxt. ## Vue Composable The SDK exports a `useStorefront` composable: ```vue theme={null} ``` ## Direct Initialization You can also use `createStorefrontSDK` directly and provide it via Vue's dependency injection: ```typescript theme={null} // plugins/storefront.ts import { createStorefrontSDK } from 'rmz-storefront-sdk'; import type { App } from 'vue'; const sdk = createStorefrontSDK({ apiUrl: import.meta.env.VITE_RMZ_API_URL || 'https://front.rmz.gg/api', publicKey: import.meta.env.VITE_RMZ_PUBLIC_KEY, }); export const StorefrontKey = Symbol('storefront'); export default { install(app: App) { app.provide(StorefrontKey, sdk); } }; ``` ```typescript theme={null} // main.ts import { createApp } from 'vue'; import App from './App.vue'; import storefrontPlugin from './plugins/storefront'; createApp(App).use(storefrontPlugin).mount('#app'); ``` ```vue theme={null} ``` ## Cart Composable Example ```typescript theme={null} // composables/useCart.ts import { ref, readonly } from 'vue'; import { createStorefrontSDK } from 'rmz-storefront-sdk'; const sdk = createStorefrontSDK({ apiUrl: import.meta.env.VITE_RMZ_API_URL || 'https://front.rmz.gg/api', publicKey: import.meta.env.VITE_RMZ_PUBLIC_KEY, }); const cart = ref(null); const count = ref(0); const loading = ref(false); export function useCart() { async function addItem(productId: number, quantity = 1) { loading.value = true; try { const updated = await sdk.cart.addItem(productId, quantity); cart.value = updated; count.value = updated.count; } finally { loading.value = false; } } async function refresh() { const current = await sdk.cart.get(); cart.value = current; count.value = current.count; } return { cart: readonly(cart), count: readonly(count), loading: readonly(loading), addItem, refresh, }; } ``` ## Environment Variables ```bash theme={null} # .env VITE_RMZ_API_URL=https://front.rmz.gg/api VITE_RMZ_PUBLIC_KEY=pk_your_public_key ``` Vite exposes environment variables prefixed with `VITE_` to client-side code. Never prefix secret keys with `VITE_`. # Management, Pages & Components Source: https://docs.rmz.gg/storefront-sdk/management Server-side management operations, static pages, and homepage components using sdk.management, sdk.pages, and sdk.components. This page covers three SDK namespaces: the server-side Management API, the Pages API, and the Components API. ## Management API (Server-side Only) The `sdk.management` namespace provides server-side operations for analytics, inventory management, order management, and data export. These methods require a `secretKey` in the SDK configuration. Management methods must only be called from a server-side environment (Node.js, SSR, API routes). They will fail without a `secretKey`. ```typescript theme={null} const sdk = createStorefrontSDK({ publicKey: process.env.RMZ_PUBLIC_KEY!, secretKey: process.env.RMZ_SECRET_KEY!, // required for management apiUrl: 'https://front.rmz.gg/api', }); ``` ### `management.getAnalytics(params?)` Retrieve store analytics data. ```typescript theme={null} const analytics = await sdk.management.getAnalytics({ start_date: '2024-01-01', end_date: '2024-12-31', metrics: ['sales', 'customers', 'revenue'] }); ``` **Parameters:** | Field | Type | Description | | ------------ | ---------- | --------------------------------------------------- | | `start_date` | `string` | Start date (YYYY-MM-DD) | | `end_date` | `string` | End date (YYYY-MM-DD) | | `metrics` | `string[]` | Metrics to include: `sales`, `customers`, `revenue` | *** ### `management.updateInventory(data)` Update product inventory. ```typescript theme={null} await sdk.management.updateInventory({ product_id: 123, quantity: 50, operation: 'set' // 'set', 'add', or 'subtract' }); ``` **Parameters:** | Field | Type | Description | | ------------ | -------- | ------------------------------------- | | `product_id` | `number` | Product ID | | `quantity` | `number` | Quantity value | | `operation` | `string` | One of `'set'`, `'add'`, `'subtract'` | **Returns:** `{ success: boolean }` *** ### `management.getOrders(params?)` Get orders with server-side filters (more powerful than customer-facing order listing). ```typescript theme={null} const { data: orders, pagination } = await sdk.management.getOrders({ page: 1, per_page: 50, status: 'completed', date_from: '2024-01-01', date_to: '2024-12-31' }); ``` **Parameters:** | Field | Type | Description | | ----------- | -------- | ----------------- | | `page` | `number` | Page number | | `per_page` | `number` | Orders per page | | `status` | `string` | Filter by status | | `date_from` | `string` | Start date filter | | `date_to` | `string` | End date filter | **Returns:** `{ data: Order[]; pagination?: Pagination }` *** ### `management.exportCustomers(params?)` Export customer data. ```typescript theme={null} const data = await sdk.management.exportCustomers({ format: 'csv', date_from: '2024-01-01' }); ``` **Parameters:** | Field | Type | Description | | ----------- | -------- | ----------------------------------------- | | `format` | `string` | Export format: `'csv'` or `'json'` | | `date_from` | `string` | Filter customers created after this date | | `date_to` | `string` | Filter customers created before this date | *** ### `management.getWebhookData(params?)` Retrieve webhook event data. ```typescript theme={null} const events = await sdk.management.getWebhookData({ type: 'order_created', limit: 100 }); ``` *** ## Pages API The `sdk.pages` namespace retrieves static pages configured in the store (e.g., About Us, Terms, Privacy Policy). ### `pages.getAll()` Get all published pages. ```typescript theme={null} const pages = await sdk.pages.getAll(); ``` **Returns:** `Page[]` ### `pages.getByUrl(url)` Get a specific page by its URL slug. ```typescript theme={null} const page = await sdk.pages.getByUrl('about-us'); console.log(page.title); console.log(page.content); // HTML content ``` **Returns:** `Page` ```typescript theme={null} interface Page { id: number; title: string; url: string; content: string; meta_title?: string; meta_description?: string; is_active: boolean; } ``` *** ## Components API The `sdk.components` namespace retrieves homepage components configured by the store owner (featured sections, promotional blocks, etc.). ### `components.getAll()` Get all homepage components. ```typescript theme={null} const components = await sdk.components.getAll(); ``` ### `components.getById(id)` Get a specific component by ID. ```typescript theme={null} const component = await sdk.components.getById(5); ``` ### `components.getProducts(id, params?)` Get the products associated with a component. ```typescript theme={null} const { data: products, pagination } = await sdk.components.getProducts(5, { page: 1, per_page: 12 }); ``` ## Example: Server-side Analytics Dashboard ```typescript theme={null} // In a Next.js API route or server action import { createStorefrontSDK } from 'rmz-storefront-sdk'; const sdk = createStorefrontSDK({ publicKey: process.env.RMZ_PUBLIC_KEY!, secretKey: process.env.RMZ_SECRET_KEY!, }); export async function GET() { const [analytics, orders] = await Promise.all([ sdk.management.getAnalytics({ start_date: '2024-01-01', end_date: '2024-12-31', metrics: ['sales', 'revenue'], }), sdk.management.getOrders({ per_page: 10, status: 'completed' }), ]); return Response.json({ analytics, recentOrders: orders.data }); } ``` # Orders Source: https://docs.rmz.gg/storefront-sdk/orders Retrieve customer orders, subscriptions, and submit order reviews using sdk.orders. The `sdk.orders` namespace provides access to a customer's order history, subscriptions, and courses. All methods require the customer to be authenticated (Bearer token set via `sdk.setAuthToken()`). ## Methods ### `orders.getAll(params?)` Get a paginated list of the customer's orders. ```typescript theme={null} const { data: orders, pagination } = await sdk.orders.getAll({ page: 1, per_page: 10 }); ``` **Parameters:** | Field | Type | Default | Description | | ---------- | -------- | ------- | --------------- | | `page` | `number` | `1` | Page number | | `per_page` | `number` | `10` | Orders per page | **Returns:** `{ data: Order[]; pagination?: Pagination }` *** ### `orders.getById(id)` Get a specific order by its ID. ```typescript theme={null} const order = await sdk.orders.getById(1234); console.log(order.status); // 'completed' console.log(order.items); // OrderItem[] ``` **Returns:** `Order` ```typescript theme={null} interface Order { id: number; status: string; items: OrderItem[]; total: number; created_at: string; } interface OrderItem { id: number; product: Product; quantity: number; price: number; } ``` *** ### `orders.getSubscriptions()` Get the customer's active subscriptions. ```typescript theme={null} const subscriptions = await sdk.orders.getSubscriptions(); ``` **Returns:** `any[]` *** ### `orders.getCourses()` Get courses the customer has purchased access to. ```typescript theme={null} const courses = await sdk.orders.getCourses(); ``` **Returns:** `any[]` For detailed course management (progress tracking, module completion), use the [`sdk.courses`](/storefront-sdk/courses) namespace instead. *** ### `orders.submitReview(orderId, reviewData)` Submit a review for a completed order. ```typescript theme={null} const review = await sdk.orders.submitReview(1234, { rating: 5, comment: 'Excellent service and fast delivery!' }); ``` **Parameters:** | Field | Type | Description | | -------------------- | -------- | ------------------ | | `orderId` | `number` | Order ID to review | | `reviewData` | `object` | Review content | | `reviewData.rating` | `number` | Rating (1-5) | | `reviewData.comment` | `string` | Review text | **Returns:** `any` ## Example: Order History Page ```typescript theme={null} async function loadOrderHistory(page: number = 1) { const { data: orders, pagination } = await sdk.orders.getAll({ page, per_page: 10 }); return { orders, currentPage: pagination?.current_page ?? 1, totalPages: pagination?.last_page ?? 1, hasMore: pagination?.has_more_pages ?? false, }; } ``` # Storefront SDK Overview Source: https://docs.rmz.gg/storefront-sdk/overview Install and configure the RMZ Storefront SDK — a secure, framework-agnostic TypeScript SDK for building custom storefronts. The RMZ Storefront SDK is a TypeScript-first, framework-agnostic library that wraps the [Storefront API](/storefront-api/overview) into a clean, type-safe interface. Use it with React, Vue, Angular, Svelte, or vanilla JavaScript in both client-side and server-side environments. Source code, issues, and releases. Full example storefront built with Next.js and the SDK. ## Features * **HMAC-SHA256 authentication** with automatic signature generation for server-to-server requests * **Firebase/Supabase-style query builder** with intuitive method chaining * **Universal compatibility** — works in browsers, Node.js, Web Workers, and React Native * **TypeScript first** with full type safety and IntelliSense * **Automatic retry, caching, and request deduplication** * **Singleton pattern** for efficient resource usage * **\~15 KB gzipped**, zero external runtime dependencies, tree-shakeable ## Installation ```bash npm theme={null} npm install rmz-storefront-sdk ``` ```bash yarn theme={null} yarn add rmz-storefront-sdk ``` ```bash pnpm theme={null} pnpm add rmz-storefront-sdk ``` The current version is **2.1.2**. The package is published as `rmz-storefront-sdk` on npm. ## Quick Start ### Client-side (browser) On the client side, only a `publicKey` is needed. The default API URL points to `https://front.rmz.gg/api`. ```typescript theme={null} import { createStorefrontSDK } from 'rmz-storefront-sdk'; const sdk = createStorefrontSDK({ publicKey: 'pk_your_public_key_here', apiUrl: 'https://front.rmz.gg/api', // default, can be omitted environment: 'production' }); // Fetch store information const store = await sdk.store.get(); // Browse products with the query builder const featured = await sdk.products .where('featured', '=', true) .orderBy('created_at', 'desc') .limit(8) .get(); // Add a product to the cart const cart = await sdk.cart.addItem(productId, 2); ``` ### Server-side (Node.js / SSR) On the server, provide both `publicKey` and `secretKey` to enable HMAC-SHA256 authentication for every request. ```typescript theme={null} import { createStorefrontSDK } from 'rmz-storefront-sdk'; const sdk = createStorefrontSDK({ publicKey: process.env.RMZ_PUBLIC_KEY!, secretKey: process.env.RMZ_SECRET_KEY!, // enables HMAC signatures apiUrl: 'https://front.rmz.gg/api', environment: 'production' }); const store = await sdk.store.get(); const products = await sdk.products.getAll({ per_page: 100 }); ``` Never expose your `secretKey` in client-side code. It must only be used in server-side environments (Node.js, SSR, API routes). ## SDK Modules The SDK exposes the following namespaces on the instance returned by `createStorefrontSDK()`: | Namespace | Description | Auth Required | | ---------------------------------------------- | -------------------------------------------------------- | :------------------: | | [`sdk.store`](/storefront-sdk/store) | Store info, currencies, settings, features, banners | No | | [`sdk.products`](/storefront-sdk/products) | Product listing, search, query builder, related products | No | | [`sdk.categories`](/storefront-sdk/products) | Category listing and category products | No | | [`sdk.cart`](/storefront-sdk/cart) | Cart management, coupons, validation, summary | No (uses cart token) | | [`sdk.checkout`](/storefront-sdk/checkout) | Create checkout sessions, get payment results | No | | [`sdk.auth`](/storefront-sdk/customer) | OTP login, registration, profile management | Partial | | [`sdk.orders`](/storefront-sdk/orders) | Order history, subscriptions, courses | Yes | | [`sdk.wishlist`](/storefront-sdk/wishlist) | Wishlist add/remove/check | Yes | | [`sdk.reviews`](/storefront-sdk/reviews) | Store reviews, submit product reviews | Partial | | [`sdk.courses`](/storefront-sdk/courses) | Course access, progress tracking, module completion | Yes | | [`sdk.pages`](/storefront-sdk/management) | Static pages | No | | [`sdk.components`](/storefront-sdk/management) | Homepage components | No | | [`sdk.management`](/storefront-sdk/management) | Analytics, inventory, exports (server-side only) | Secret key | | `sdk.customTokens` | Generate, list, revoke, and validate API tokens | Yes | ## Environment Detection The SDK automatically detects its runtime environment and adjusts behavior: ```typescript theme={null} import { Environment } from 'rmz-storefront-sdk'; console.log(Environment.info); // { isServer: false, isBrowser: true, isWebWorker: false, isNode: false, platform: 'browser' } ``` * **Browser**: Uses `X-Client-Auth` header, warns if `secretKey` is present. * **Node.js**: Generates HMAC-SHA256 signatures on every request when `secretKey` is provided. * **React Native**: Treated as a browser environment. ## Health Check Verify API connectivity at any time: ```typescript theme={null} const health = await sdk.healthCheck(); if (health.status === 'ok') { console.log('API is reachable'); } else { console.error('API error:', health.message); } ``` ## Next Steps Understand the HMAC security model and key management. All configuration options, environment variables, and defaults. Query builder, search, and product retrieval. React, Vue, Angular, and vanilla JS examples. # Products Source: https://docs.rmz.gg/storefront-sdk/products Browse, search, and query products using sdk.products — including Firebase/Supabase-style method chaining. The `sdk.products` namespace provides both direct methods and a chainable query builder for retrieving products. All product listing methods are public and do not require authentication. ## Query Builder The SDK provides a Firebase/Supabase-style query builder for intuitive product filtering: ```typescript theme={null} // Chain where, orderBy, and limit const products = await sdk.products .where('featured', '=', true) .orderBy('created_at', 'desc') .limit(8) .get(); // Filter by category and price const expensive = await sdk.products .where('category', '=', 'electronics') .orderBy('price', 'desc') .limit(10) .get(); // Price range filtering const midRange = await sdk.products .where('price', '>=', 50) .get(); ``` **Supported `where` fields and operators:** | Field | Operators | Example | | ---------- | ---------- | ---------------------------------------- | | `featured` | `=` | `.where('featured', '=', true)` | | `category` | `=` | `.where('category', '=', 'electronics')` | | `price` | `>=`, `<=` | `.where('price', '>=', 100)` | **Chainable methods:** | Method | Description | | -------------------------------- | ---------------------------------------- | | `.where(field, operator, value)` | Add a filter condition | | `.orderBy(field, direction)` | Sort results (`'asc'` or `'desc'`) | | `.limit(count)` | Limit the number of results | | `.get()` | Execute the query and return `Product[]` | ## Direct Methods ### `products.getAll(params?)` Get a paginated list of products with optional filtering. ```typescript theme={null} const { data, pagination } = await sdk.products.getAll({ page: 1, per_page: 12, category: 'electronics', sort: 'price_asc' }); ``` **Parameters:** | Field | Type | Description | | ---------- | -------- | --------------------------------------------------------------- | | `page` | `number` | Page number (default: 1) | | `per_page` | `number` | Items per page (default: 12) | | `search` | `string` | Search query | | `category` | `string` | Filter by category slug | | `sort` | `string` | Sort order (e.g., `price_asc`, `price_desc`, `created_at_desc`) | **Returns:** `{ data: Product[]; pagination?: Pagination }` *** ### `products.getBySlug(slug)` Get a single product by its URL slug. ```typescript theme={null} const product = await sdk.products.getBySlug('premium-software-license'); ``` **Returns:** `Product` *** ### `products.getById(id)` Get a single product by its numeric ID. ```typescript theme={null} const product = await sdk.products.getById(42); ``` **Returns:** `Product` *** ### `products.search(query, options?)` Search products by keyword with optional filters. ```typescript theme={null} const { data, pagination } = await sdk.products.search('laptop', { category: 'electronics', price_min: 500, price_max: 2000, per_page: 20 }); ``` **Parameters:** | Field | Type | Description | | ----------- | -------- | ----------------------- | | `query` | `string` | Search keyword | | `category` | `string` | Filter by category slug | | `price_min` | `number` | Minimum price filter | | `price_max` | `number` | Maximum price filter | | `per_page` | `number` | Results per page | **Returns:** `{ data: Product[]; pagination?: Pagination }` *** ### `products.getFeatured(limit?)` Get featured products. ```typescript theme={null} const featured = await sdk.products.getFeatured(8); ``` **Parameters:** | Field | Type | Default | Description | | ------- | -------- | ------- | ------------------------------------ | | `limit` | `number` | `8` | Maximum number of products to return | **Returns:** `Product[]` *** ### `products.getRelated(productId, limit?)` Get products related to a specific product. ```typescript theme={null} const related = await sdk.products.getRelated(42, 4); ``` **Returns:** `Product[]` *** ### `products.getReviews(productId, params?)` Get reviews for a specific product. ```typescript theme={null} const { data: reviews, pagination } = await sdk.products.getReviews(42, { page: 1, per_page: 10 }); ``` **Returns:** `{ data: Review[]; pagination?: Pagination }` ## Categories The `sdk.categories` namespace provides methods for browsing categories and their products: ```typescript theme={null} // Get all categories const categories = await sdk.categories.getAll(); // Get a category by slug const category = await sdk.categories.getBySlug('electronics'); // Get a category by ID const category = await sdk.categories.getById(5); // Get products in a category with pagination const { data, pagination } = await sdk.categories.getProducts('electronics', { page: 1, per_page: 12, sort: 'price_asc' }); ``` ## Types ```typescript theme={null} interface Product { id: number; name: string; slug: string; description?: string; price: number; image?: { url: string; alt?: string; }; category?: Category; is_featured?: boolean; stock?: number; } interface Category { id: number; name: string; slug: string; description?: string; image?: string; } interface Pagination { current_page: number; last_page: number; per_page: number; total: number; has_more_pages: boolean; } ``` ## Example: Product Listing Page ```typescript theme={null} // Load category products with pagination async function loadCategoryPage(slug: string, page: number) { const [category, { data: products, pagination }] = await Promise.all([ sdk.categories.getBySlug(slug), sdk.categories.getProducts(slug, { page, per_page: 12 }), ]); return { category, products, pagination }; } ``` # Reviews Source: https://docs.rmz.gg/storefront-sdk/reviews Retrieve store reviews, filter by rating, submit product reviews, and view statistics using sdk.reviews. The `sdk.reviews` namespace provides methods to fetch store-wide reviews, get review statistics, and submit new reviews. Listing reviews is public; submitting a review requires authentication. ## Methods ### `reviews.getAll(params?)` Get a paginated list of store reviews, optionally filtered by rating. ```typescript theme={null} const { data: reviews, pagination } = await sdk.reviews.getAll({ page: 1, per_page: 10, rating: 5 // only 5-star reviews }); ``` **Parameters:** | Field | Type | Description | | ---------- | -------- | ---------------------- | | `page` | `number` | Page number | | `per_page` | `number` | Reviews per page | | `rating` | `number` | Filter by rating (1-5) | **Returns:** `{ data: Review[]; pagination?: Pagination }` *** ### `reviews.getRecent(limit?)` Get the most recent reviews. ```typescript theme={null} const recent = await sdk.reviews.getRecent(6); ``` **Parameters:** | Field | Type | Default | Description | | ------- | -------- | ------- | --------------------------- | | `limit` | `number` | `6` | Number of reviews to return | **Returns:** `Review[]` *** ### `reviews.submit(productId, data)` Submit a review for a product. Requires authentication. ```typescript theme={null} const review = await sdk.reviews.submit(42, { rating: 5, comment: 'Great product, highly recommended!' }); ``` **Parameters:** | Field | Type | Description | | -------------- | -------- | ------------------ | | `productId` | `number` | Product to review | | `data.rating` | `number` | Rating from 1 to 5 | | `data.comment` | `string` | Review text | **Returns:** `Review` *** ### `reviews.getStats()` Get aggregate review statistics for the store. ```typescript theme={null} const stats = await sdk.reviews.getStats(); ``` **Returns:** Review statistics object (varies by store configuration). ## Types ```typescript theme={null} interface Review { id: number; rating: number; comment: string; reviewer: { id: number; name: string; email?: string; } | null; product?: { id: number; name: string; slug: string; } | null; created_at: string; } ``` You can also fetch reviews for a specific product using `sdk.products.getReviews(productId)`. See [Products](/storefront-sdk/products). # Store Source: https://docs.rmz.gg/storefront-sdk/store Retrieve store information, currencies, settings, features, and banners using sdk.store. The `sdk.store` namespace provides methods to fetch store metadata, configuration, and display content. All methods are public and do not require authentication. ## Methods ### `store.get(params?)` Retrieve the store's basic information. Optionally include related data. ```typescript theme={null} const store = await sdk.store.get(); // With optional includes const storeWithExtras = await sdk.store.get({ include: ['categories', 'pages', 'announcements'] }); ``` **Parameters:** | Field | Type | Description | | --------- | ---------- | ------------------------------------------------------------------------------- | | `include` | `string[]` | Optional. Related resources to include: `categories`, `pages`, `announcements`. | **Returns:** `Store` ```typescript theme={null} interface Store { id: number; name: string; description?: string; logo?: string; currency: string; settings?: Record; } ``` *** ### `store.getCurrencies()` Get the list of currencies supported by the store. ```typescript theme={null} const currencies = await sdk.store.getCurrencies(); // [{ code: 'SAR', symbol: 'ر.س', name: 'Saudi Riyal' }, ...] ``` **Returns:** `Array<{ code: string; symbol: string; name: string }>` *** ### `store.changeCurrency(currency)` Switch the active currency for the current session. ```typescript theme={null} await sdk.store.changeCurrency('USD'); ``` **Parameters:** | Field | Type | Description | | ---------- | -------- | --------------------------------------- | | `currency` | `string` | Currency code (e.g., `'SAR'`, `'USD'`). | **Returns:** `void` *** ### `store.getSettings()` Retrieve the store's public settings (theme configuration, social links, contact info, etc.). ```typescript theme={null} const settings = await sdk.store.getSettings(); ``` **Returns:** `Record` *** ### `store.getFeatures()` Get the store's feature highlights, typically displayed on the homepage. ```typescript theme={null} const features = await sdk.store.getFeatures(); // [{ id: 1, title: 'Fast Delivery', description: '...', icon: '...', sort_order: 0 }] ``` **Returns:** ```typescript theme={null} Array<{ id: number; title: string; description: string; icon: string; sort_order: number; }> ``` *** ### `store.getBanners()` Get promotional banners configured for the store. ```typescript theme={null} const banners = await sdk.store.getBanners(); // [{ id: 1, title: 'Summer Sale', image_url: '...', link_url: '/sale', sort_order: 0 }] ``` **Returns:** ```typescript theme={null} Array<{ id: number; title: string; description: string; image_url: string; link_url: string; sort_order: number; }> ``` ## Example: Store Landing Page ```typescript theme={null} // Fetch everything needed for a landing page in parallel const [store, banners, features, currencies] = await Promise.all([ sdk.store.get({ include: ['categories', 'pages'] }), sdk.store.getBanners(), sdk.store.getFeatures(), sdk.store.getCurrencies(), ]); ``` # Wishlist Source: https://docs.rmz.gg/storefront-sdk/wishlist Manage the customer's wishlist — add, remove, check, and clear items using sdk.wishlist. The `sdk.wishlist` namespace manages the authenticated customer's wishlist. All methods require a Bearer token (set via `sdk.setAuthToken()`). ## Methods ### `wishlist.get()` Retrieve the customer's full wishlist. ```typescript theme={null} const { items, count } = await sdk.wishlist.get(); console.log(`${count} items in wishlist`); ``` **Returns:** `{ items: Product[]; count: number }` *** ### `wishlist.addItem(productId)` Add a product to the wishlist. ```typescript theme={null} await sdk.wishlist.addItem(42); ``` **Parameters:** | Field | Type | Description | | ----------- | -------- | ----------------- | | `productId` | `number` | Product ID to add | **Returns:** `void` *** ### `wishlist.removeItem(productId)` Remove a product from the wishlist. ```typescript theme={null} await sdk.wishlist.removeItem(42); ``` **Returns:** `void` *** ### `wishlist.check(productId)` Check whether a specific product is in the customer's wishlist. ```typescript theme={null} const { in_wishlist } = await sdk.wishlist.check(42); if (in_wishlist) { console.log('Product is wishlisted'); } ``` **Returns:** `{ in_wishlist: boolean }` *** ### `wishlist.clear()` Remove all items from the wishlist. ```typescript theme={null} await sdk.wishlist.clear(); ``` **Returns:** `void` ## Example: Wishlist Toggle Button ```typescript theme={null} async function toggleWishlist(productId: number) { const { in_wishlist } = await sdk.wishlist.check(productId); if (in_wishlist) { await sdk.wishlist.removeItem(productId); return false; // removed } else { await sdk.wishlist.addItem(productId); return true; // added } } ``` # Webhook Events Source: https://docs.rmz.gg/webhooks/events All webhook event types available in RMZ and what triggers them. RMZ supports webhook event types for orders and subscriptions. Each webhook you create listens for a single event type. ## Available Events | Event | Trigger | | ----------------------------- | ----------------------------------------------------------------------------------------- | | `order.created` | A new order is placed in your store (after successful payment or for free orders) | | `order.status.changed` | An order's status is updated (e.g., from pending to completed, or completed to cancelled) | | `subscription.created` | A new subscription is created for a customer | | `subscription.activated` | A subscription transitions from trialing to active | | `subscription.renewed` | A subscription is successfully renewed with payment | | `subscription.renewal_failed` | An auto-renewal payment attempt failed | | `subscription.past_due` | A subscription enters the past due state after a failed payment | | `subscription.expired` | A subscription has expired (terminal state) | | `subscription.canceled` | A subscription has been canceled | | `subscription.paused` | A subscription has been paused (auto-renewal stops, access retained until period end) | | `subscription.unpaused` | A paused subscription has been resumed (period extended by remaining days) | | `subscription.resumed` | A canceled subscription has been resumed before the period ended | | `subscription.updated` | A subscription has been updated (variant change, extension, etc.) | Every subscription webhook payload includes an `external_customer_id` field at `data.subscription.external_customer_id`. This is the identifier the merchant supplied when creating the checkout session — use it to correlate incoming webhooks back to your own user records. The value is `null` if it was not set at checkout, but the key is always present. *** ## order.created Fired when a customer successfully places an order. This includes orders from all sources: the storefront, embed checkout, and the Storefront API. **Common use cases:** * Send a custom notification (Slack, Telegram, email) * Sync the order to an external system (CRM, ERP, accounting) * Trigger a fulfillment workflow * Update inventory in an external system * Log the sale for analytics **Payload:** Contains the full order object including customer, items, transaction details, and status history. See [Payload Format](/webhooks/payload-format) for the complete structure. *** ## order.status.changed Fired when an order's status changes after the initial creation. This event is not triggered for the first status (order creation) — only for subsequent status updates. **Common use cases:** * Notify customers of shipping or delivery updates via your own channels * Update external dashboards or reporting tools * Trigger post-purchase workflows (e.g., send a review request after completion) * Sync order status with third-party fulfillment systems **Payload:** Contains the same full order object as `order.created`, reflecting the current state of the order after the status change. The `statuses` array includes the complete status history. ### Order Status Values The `status` field in the payload corresponds to these values: | Status | Meaning | | ------ | ------------------- | | 1 | Waiting for Payment | | 2 | Under Review | | 3 | Processing | | 4 | Completed | | 5 | Cancelled | | 6 | Refunded | Use the `statuses` array in the payload to see the full history of status changes for an order, including timestamps for each transition. *** ## subscription.created Fired when a new subscription is created for a customer, either through a storefront purchase or via the Merchant API checkout session. **Common use cases:** * Provision access to a service or SaaS product * Send a welcome email to the new subscriber * Sync the subscription to your billing system * Log the event for analytics **Payload example:** ```json theme={null} { "event": "subscription.created", "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "data": { "subscription": { "id": 501, "status": "active", "external_customer_id": "usr_abc123", "product_id": 102, "product_name": "Pro Plan", "variant": { "id": 15, "price": 49, "duration": "monthly" }, "duration": "monthly", "current_period_start": "2025-06-01T00:00:00.000000Z", "current_period_end": "2025-07-01T00:00:00.000000Z", "auto_renew": true, "metadata": { "external_user_id": "usr_abc123" }, "customer": { "id": 123, "email": "ahmed@example.com", "name": "Ahmed Ali" } } } } ``` *** ## subscription.activated Fired when a subscription transitions from `trialing` to `active`, typically after a successful first payment at the end of a trial period. **Common use cases:** * Upgrade the customer from trial to full access * Trigger a billing confirmation notification * Update your CRM with the activation date **Payload example:** ```json theme={null} { "event": "subscription.activated", "event_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "data": { "subscription": { "id": 501, "status": "active", "external_customer_id": "usr_abc123", "product_id": 102, "product_name": "Pro Plan", "variant": { "id": 15, "price": 49, "duration": "monthly" }, "duration": "monthly", "current_period_start": "2025-06-15T00:00:00.000000Z", "current_period_end": "2025-07-15T00:00:00.000000Z", "auto_renew": true, "metadata": { "external_user_id": "usr_abc123" }, "customer": { "id": 123, "email": "ahmed@example.com", "name": "Ahmed Ali" } } } } ``` *** ## subscription.renewed Fired when a subscription is successfully renewed with a payment charge. **Common use cases:** * Send a payment receipt to the customer * Log the renewal for accounting * Extend access in your external system **Payload example:** ```json theme={null} { "event": "subscription.renewed", "event_id": "c3d4e5f6-a7b8-9012-cdef-123456789012", "data": { "subscription": { "id": 501, "status": "active", "external_customer_id": "usr_abc123", "product_id": 102, "product_name": "Pro Plan", "variant": { "id": 15, "price": 49, "duration": "monthly" }, "duration": "monthly", "current_period_start": "2025-07-01T00:00:00.000000Z", "current_period_end": "2025-08-01T00:00:00.000000Z", "auto_renew": true, "metadata": { "external_user_id": "usr_abc123" }, "customer": { "id": 123, "email": "ahmed@example.com", "name": "Ahmed Ali" } } } } ``` *** ## subscription.renewal\_failed Fired when an auto-renewal payment attempt fails. The subscription may be retried automatically. **Common use cases:** * Notify the customer to update their payment method * Send a dunning email sequence * Log the failure for monitoring **Payload example:** ```json theme={null} { "event": "subscription.renewal_failed", "event_id": "d4e5f6a7-b8c9-0123-defa-234567890123", "data": { "subscription": { "id": 501, "status": "past_due", "external_customer_id": "usr_abc123", "product_id": 102, "product_name": "Pro Plan", "variant": { "id": 15, "price": 49, "duration": "monthly" }, "duration": "monthly", "current_period_start": "2025-06-01T00:00:00.000000Z", "current_period_end": "2025-07-01T00:00:00.000000Z", "auto_renew": true, "metadata": { "external_user_id": "usr_abc123" }, "customer": { "id": 123, "email": "ahmed@example.com", "name": "Ahmed Ali" } } } } ``` *** ## subscription.past\_due Fired when a subscription enters the `past_due` state after the first failed payment attempt. **Common use cases:** * Trigger a dunning workflow * Display a warning banner in your application * Downgrade the customer to a limited access tier **Payload example:** ```json theme={null} { "event": "subscription.past_due", "event_id": "e5f6a7b8-c9d0-1234-efab-345678901234", "data": { "subscription": { "id": 501, "status": "past_due", "external_customer_id": "usr_abc123", "product_id": 102, "product_name": "Pro Plan", "variant": { "id": 15, "price": 49, "duration": "monthly" }, "duration": "monthly", "current_period_start": "2025-06-01T00:00:00.000000Z", "current_period_end": "2025-07-01T00:00:00.000000Z", "auto_renew": true, "metadata": { "external_user_id": "usr_abc123" }, "customer": { "id": 123, "email": "ahmed@example.com", "name": "Ahmed Ali" } } } } ``` *** ## subscription.expired Fired when a subscription reaches its terminal expired state, either after all retry attempts are exhausted or when a non-renewing subscription period ends. **Common use cases:** * Revoke access to your service * Send a win-back email * Archive the subscription in your system **Payload example:** ```json theme={null} { "event": "subscription.expired", "event_id": "f6a7b8c9-d0e1-2345-fabc-456789012345", "data": { "subscription": { "id": 501, "status": "expired", "external_customer_id": "usr_abc123", "product_id": 102, "product_name": "Pro Plan", "variant": { "id": 15, "price": 49, "duration": "monthly" }, "duration": "monthly", "current_period_start": "2025-06-01T00:00:00.000000Z", "current_period_end": "2025-07-01T00:00:00.000000Z", "auto_renew": false, "metadata": { "external_user_id": "usr_abc123" }, "customer": { "id": 123, "email": "ahmed@example.com", "name": "Ahmed Ali" } } } } ``` *** ## subscription.canceled Fired when a subscription is canceled, either immediately or scheduled for end of period. **Common use cases:** * Log the cancellation reason * Trigger a retention offer * Schedule access revocation for end-of-period cancellations **Payload example:** ```json theme={null} { "event": "subscription.canceled", "event_id": "a7b8c9d0-e1f2-3456-abcd-567890123456", "data": { "subscription": { "id": 501, "status": "active", "external_customer_id": "usr_abc123", "product_id": 102, "product_name": "Pro Plan", "variant": { "id": 15, "price": 49, "duration": "monthly" }, "duration": "monthly", "current_period_start": "2025-06-01T00:00:00.000000Z", "current_period_end": "2025-07-01T00:00:00.000000Z", "auto_renew": false, "metadata": { "external_user_id": "usr_abc123" }, "customer": { "id": 123, "email": "ahmed@example.com", "name": "Ahmed Ali" } } } } ``` When canceled with `end_of_period`, the subscription `status` remains `active` and `auto_renew` is `false` until the period ends. The subscription will transition to `expired` when the period ends. *** ## subscription.paused Fired when a subscription is paused by the customer or via the Merchant API. Auto-renewal is suspended, but access is retained until the period end. **Common use cases:** * Suspend provisioned resources or downgrade service level * Log the pause for analytics * Send a "we'll miss you" email **Payload example:** ```json theme={null} { "event": "subscription.paused", "event_id": "c9d0e1f2-a3b4-5678-cdef-789012345678", "data": { "subscription": { "id": 501, "status": "paused", "external_customer_id": "usr_abc123", "product_id": 102, "product_name": "Pro Plan", "variant": { "id": 15, "price": 49, "duration": "monthly" }, "duration": "monthly", "current_period_start": "2025-06-01T00:00:00.000000Z", "current_period_end": "2025-07-01T00:00:00.000000Z", "auto_renew": false, "metadata": { "external_user_id": "usr_abc123" }, "customer": { "id": 123, "email": "ahmed@example.com", "name": "Ahmed Ali" } } } } ``` When a subscription is paused, `is_active` becomes `false` and auto-renewal stops. The remaining days at the time of pause are stored internally. When unpaused, the period is extended by those remaining days. *** ## subscription.unpaused Fired when a paused subscription is resumed. The subscription returns to `active` status and the billing period is extended by the number of days remaining when it was paused. **Common use cases:** * Re-provision access or restore service level * Resume billing in your external system * Send a "welcome back" notification **Payload example:** ```json theme={null} { "event": "subscription.unpaused", "event_id": "d0e1f2a3-b4c5-6789-defa-890123456789", "data": { "subscription": { "id": 501, "status": "active", "external_customer_id": "usr_abc123", "product_id": 102, "product_name": "Pro Plan", "variant": { "id": 15, "price": 49, "duration": "monthly" }, "duration": "monthly", "current_period_start": "2025-06-20T00:00:00.000000Z", "current_period_end": "2025-07-10T00:00:00.000000Z", "auto_renew": true, "metadata": { "external_user_id": "usr_abc123" }, "customer": { "id": 123, "email": "ahmed@example.com", "name": "Ahmed Ali" } } } } ``` *** ## subscription.resumed Fired when a canceled subscription is resumed before the current period ends. The subscription returns to `active` status and auto-renewal is re-enabled. **Common use cases:** * Restore access that was scheduled for revocation * Update your CRM to reflect the reactivation * Cancel any scheduled access revocation **Payload example:** ```json theme={null} { "event": "subscription.resumed", "event_id": "e1f2a3b4-c5d6-7890-efab-901234567890", "data": { "subscription": { "id": 501, "status": "active", "external_customer_id": "usr_abc123", "product_id": 102, "product_name": "Pro Plan", "variant": { "id": 15, "price": 49, "duration": "monthly" }, "duration": "monthly", "current_period_start": "2025-06-01T00:00:00.000000Z", "current_period_end": "2025-07-01T00:00:00.000000Z", "auto_renew": true, "metadata": { "external_user_id": "usr_abc123" }, "customer": { "id": 123, "email": "ahmed@example.com", "name": "Ahmed Ali" } } } } ``` *** ## subscription.updated Fired when a subscription is modified, such as a variant change (upgrade/downgrade) or a courtesy extension. **Common use cases:** * Update access level in your system for upgrades * Schedule a downgrade for the next billing cycle * Log the change for auditing **Payload example:** ```json theme={null} { "event": "subscription.updated", "event_id": "b8c9d0e1-f2a3-4567-bcde-678901234567", "data": { "subscription": { "id": 501, "status": "active", "external_customer_id": "usr_abc123", "product_id": 102, "product_name": "Pro Plan", "variant": { "id": 20, "price": 99, "duration": "monthly" }, "duration": "monthly", "current_period_start": "2025-06-01T00:00:00.000000Z", "current_period_end": "2025-07-01T00:00:00.000000Z", "auto_renew": true, "metadata": { "external_user_id": "usr_abc123" }, "customer": { "id": 123, "email": "ahmed@example.com", "name": "Ahmed Ali" } } } } ``` *** ## Choosing the Right Event Use when you need to react to new sales — send notifications, sync to external systems, or trigger fulfillment. Use when you need to track order lifecycle — notify customers of updates, sync status changes, or trigger post-purchase workflows. Use when you need to provision access for new subscribers or sync subscriptions to your system. Use when you need to confirm recurring payments, send receipts, or extend access in external systems. Use when you need to suspend provisioned resources or pause billing in your system. Use when you need to trigger retention flows or schedule access revocation. Use when you need to revoke access or trigger win-back campaigns. You can create multiple webhooks for the same event with different URLs. For example, one webhook sending `subscription.renewed` to your billing system and another sending it to a Slack channel. In the current implementation, **all enabled webhooks** receive all events regardless of the configured event type. The `event` field stored on the webhook is included for your reference, but event filtering is not enforced server-side. Your webhook handler should check the `event` field in the payload and ignore events it does not care about. # Webhooks Overview Source: https://docs.rmz.gg/webhooks/overview Receive real-time notifications when events happen in your RMZ store. RMZ webhooks send HTTP POST requests to your server whenever specific events occur in your store, such as a new order being created or an order status changing. This lets you build real-time integrations without polling the API. Webhooks require an **RMZ+** subscription plan. You can manage webhooks from your store dashboard. ## How Webhooks Work In your dashboard, create a webhook by specifying the event type, destination URL, and retry settings. When a matching event happens (e.g., a customer places an order), RMZ builds the payload and queues the webhook for delivery. RMZ sends an HTTP POST request to your URL with the event data as JSON. Custom headers identify the request. Return a 2xx status code to acknowledge receipt. Non-2xx responses trigger retries based on your configuration. ## Setting Up a Webhook Navigate to **Dashboard > Settings > Webhooks** and click **Add Webhook**. ### Configuration Fields | Field | Required | Description | | ----------- | -------- | -------------------------------------------------------------- | | **Name** | No | A label for your reference (e.g., "Order notifications") | | **Event** | Yes | The event type to listen for (e.g., `order.created`) | | **URL** | Yes | The HTTPS endpoint that will receive the webhook POST requests | | **Tries** | Yes | Number of delivery attempts (1 to 5) | | **Enabled** | No | Toggle the webhook on or off (default: off) | When you create a webhook, a **secret key** is auto-generated (28 random characters). This key can be used to verify webhook authenticity if you implement signature verification on your end. Webhook payload signing is **not enabled by default**. The secret key is generated and stored, but the `Signature` header is only included when signing is explicitly enabled for the webhook. See [Signature Verification](/webhooks/signature-verification) for details. Store your webhook secret key securely. If you suspect it has been compromised, delete the webhook and create a new one. ## Managing Webhooks From the webhooks settings page, you can: * **Create** new webhooks with different events and URLs * **Update** the name, event, URL, tries, or enabled status of existing webhooks * **Delete** webhooks you no longer need * **Toggle** webhooks on/off without deleting them ## Discord Integration RMZ automatically detects Discord webhook URLs and transforms the payload into a Discord embed format. If your webhook URL contains `discord`, the payload is converted to a rich embed with order details, customer info, and status — no middleware needed on your end. ## Request Headers Every webhook request includes these headers: | Header | Description | | ------------------ | ----------------------------------------------------------------------------------------------------------- | | `Content-Type` | `application/json` | | `X-RMZ-WEBHOOKS` | API version (`1.2`) | | `X-RMZ-REQUEST-ID` | Unique request ID for tracking and deduplication | | `Signature` | HMAC-SHA256 signature (**only present** when signing is enabled for the webhook -- not included by default) | ## Quick Links See all available webhook event types. Full payload structure with field descriptions. Verify webhook authenticity with HMAC signatures. Retry strategy, timeouts, and delivery guarantees. # Payload Format Source: https://docs.rmz.gg/webhooks/payload-format Full JSON payload structure for RMZ webhook events. Every webhook delivery is an HTTP POST request with a JSON body. This page documents the complete payload structure for each event type. ## Request Headers All webhook requests include these headers: ``` Content-Type: application/json X-RMZ-WEBHOOKS: 1.2 X-RMZ-REQUEST-ID: 12345 ``` The `Signature` header is only included when signing is enabled for the webhook. See [Signature Verification](/webhooks/signature-verification) for details. | Header | Description | | ------------------ | ----------------------------------------------------------------------------------------------- | | `Content-Type` | Always `application/json` | | `X-RMZ-WEBHOOKS` | Webhook API version (currently `1.2`) | | `X-RMZ-REQUEST-ID` | Unique integer ID for this delivery attempt. Use for deduplication and support inquiries. | | `Signature` | HMAC-SHA256 signature of the payload (**only present** when signing is enabled for the webhook) | *** ## order.created Payload ```json theme={null} { "event": "order.created", "data": { "id": 12345, "store_id": 7, "customer_id": 5678, "coupon_id": null, "total": "149.00", "discount_amount": null, "customer_note": null, "meta": null, "cost": "0.00", "current_status": 4, "tax_rate": "0.00", "tax_amount": "0.00", "prices_include_tax": true, "tax_country_code": null, "tax_registration_number": null, "order_review_notification_sent_at": null, "seen_at": null, "created_at": "2026-03-30T14:30:00.000000Z", "updated_at": "2026-03-30T14:30:00.000000Z", "human_format": { "date_human": "منذ ثانية واحدة", "date_normal": "الأحد، مارس 30، 2026 2:30 م" }, "transaction": { "id": 301, "deserved": "134.10", "platform_fees": "14.90", "total": "149.00", "order_id": 12345, "created_at": "2026-03-30T14:30:00.000000Z", "payment_method": "dokanpay", "is_by_platform": false, "human_format": { "payment.method": "البطائق الإئتمانية", "created_at": "30 مارس 2026 2:30 م", "created_at_text": "منذ ثانية واحدة", "store_balance_scheduled_at_text": null, "store_balance_scheduled_at": null } }, "items": [ { "id": 101, "quantity": 1, "item_id": 42, "item_type": "App\\Models\\StoreProduct", "price": "149.00", "fields": null, "notes": null, "order_id": 12345, "created_at": "2026-03-30T14:30:00.000000Z", "updated_at": "2026-03-30T14:30:00.000000Z", "item": { "id": 42, "name": "Premium Digital Course", "price": "199.00", "actual_price": "149.00", "type": "code", "...": "additional product fields" }, "codes": [ { "id": 201, "code": "XXXX-YYYY-ZZZZ", "is_used": true, "...": "additional code fields" } ] } ], "customer": { "id": 5678, "firstName": "Ahmed", "lastName": "Ali", "email": "ahmed@example.com", "country_code": 966, "phone": "501234567", "is_banned": false, "ban_reason": null, "store_id": 7, "created_at": "2026-01-15T10:00:00.000000Z", "updated_at": "2026-03-30T14:30:00.000000Z", "deleted_at": null }, "status": { "id": 401, "reason": null, "status": 4, "model_id": 12345, "model_type": "App\\Models\\Order", "created_at": "2026-03-30T14:30:00.000000Z", "updated_at": "2026-03-30T14:30:00.000000Z", "human_format": { "text": "مكتمل", "dashboard_color": "success", "color": "success", "date_human": "منذ ثانية واحدة", "date_normal": "الأحد، مارس 30، 2026 2:30 م" } }, "statuses": [ { "id": 401, "reason": null, "status": 4, "model_id": 12345, "model_type": "App\\Models\\Order", "created_at": "2026-03-30T14:30:00.000000Z", "updated_at": "2026-03-30T14:30:00.000000Z", "human_format": { "text": "مكتمل", "dashboard_color": "success", "color": "success", "date_human": "منذ ثانية واحدة", "date_normal": "الأحد، مارس 30، 2026 2:30 م" } } ] } } ``` ## order.status.changed Payload The payload structure is identical to `order.created` -- the same relations are loaded (`transaction`, `items.item`, `items.codes`, `customer`, `status`, `statuses`). The `status` field reflects the new (current) status, and the `statuses` array contains the full status history. The `order.status.changed` event is only fired when the order has more than one status (i.e., the initial status creation does not trigger this event -- only subsequent status changes do). ```json theme={null} { "event": "order.status.changed", "data": { "id": 12345, "store_id": 7, "customer_id": 5678, "coupon_id": null, "total": "149.00", "discount_amount": null, "customer_note": null, "meta": null, "cost": "0.00", "current_status": 5, "tax_rate": "0.00", "tax_amount": "0.00", "prices_include_tax": true, "tax_country_code": null, "tax_registration_number": null, "order_review_notification_sent_at": null, "seen_at": null, "created_at": "2026-03-30T14:30:00.000000Z", "updated_at": "2026-03-31T10:00:00.000000Z", "human_format": { "date_human": "منذ يوم واحد", "date_normal": "الأحد، مارس 30، 2026 2:30 م" }, "transaction": { "id": 301, "deserved": "134.10", "platform_fees": "14.90", "total": "149.00", "order_id": 12345, "created_at": "2026-03-30T14:30:00.000000Z", "payment_method": "dokanpay", "is_by_platform": false, "human_format": { "payment.method": "البطائق الإئتمانية", "created_at": "30 مارس 2026 2:30 م", "created_at_text": "منذ يوم واحد", "store_balance_scheduled_at_text": null, "store_balance_scheduled_at": null } }, "items": [ "..." ], "customer": { "..." : "same structure as order.created" }, "status": { "id": 502, "reason": null, "status": 5, "model_id": 12345, "model_type": "App\\Models\\Order", "created_at": "2026-03-31T10:00:00.000000Z", "updated_at": "2026-03-31T10:00:00.000000Z", "human_format": { "text": "ملغي", "dashboard_color": "error", "color": "danger", "date_human": "منذ ثانية واحدة", "date_normal": "الثلاثاء، مارس 31، 2026 10:00 ص" } }, "statuses": [ { "id": 401, "reason": null, "status": 4, "model_id": 12345, "model_type": "App\\Models\\Order", "created_at": "2026-03-30T14:30:00.000000Z", "updated_at": "2026-03-30T14:30:00.000000Z", "human_format": { "text": "مكتمل", "dashboard_color": "success", "color": "success", "date_human": "منذ يوم واحد", "date_normal": "الأحد، مارس 30، 2026 2:30 م" } }, { "id": 502, "reason": null, "status": 5, "model_id": 12345, "model_type": "App\\Models\\Order", "created_at": "2026-03-31T10:00:00.000000Z", "updated_at": "2026-03-31T10:00:00.000000Z", "human_format": { "text": "ملغي", "dashboard_color": "error", "color": "danger", "date_human": "منذ ثانية واحدة", "date_normal": "الثلاثاء، مارس 31، 2026 10:00 ص" } } ] } } ``` *** ## Field Reference ### Top Level | Field | Type | Description | | ------- | ------ | ----------------------------------------------------- | | `event` | string | Event type: `order.created` or `order.status.changed` | | `data` | object | The full order object | ### data (Order) | Field | Type | Description | | ----------------------------------- | ------------- | -------------------------------------------------------- | | `id` | integer | Order ID | | `store_id` | integer | Store ID | | `customer_id` | integer | Customer ID (foreign key) | | `coupon_id` | integer\|null | Coupon ID if a coupon was applied | | `total` | string | Total order amount | | `discount_amount` | string\|null | Discount amount (if coupon was applied) | | `customer_note` | string\|null | Note left by the customer | | `meta` | object\|null | Additional order metadata (JSON) | | `cost` | string | Order cost | | `current_status` | integer\|null | Denormalized current status code | | `tax_rate` | string\|null | Tax rate applied (e.g., `"15.00"`) | | `tax_amount` | string\|null | Tax amount | | `prices_include_tax` | boolean | Whether prices already include tax (defaults to `true`) | | `tax_country_code` | string\|null | Country code for tax calculation (2 chars) | | `tax_registration_number` | string\|null | Store's tax registration number | | `order_review_notification_sent_at` | string\|null | ISO 8601 timestamp when review notification was sent | | `seen_at` | string\|null | ISO 8601 timestamp when the store owner viewed the order | | `created_at` | string | ISO 8601 timestamp when the order was created | | `updated_at` | string | ISO 8601 timestamp when the order was last updated | | `human_format` | object | Human-readable date strings (Arabic locale) | | `human_format.date_human` | string | Relative time (e.g., "منذ ثانية واحدة") | | `human_format.date_normal` | string | Formatted date string | | `transaction` | object\|null | Payment transaction details (selected fields only) | | `items` | array | Line items in the order | | `customer` | object | Customer who placed the order | | `status` | object | Current (latest) status | | `statuses` | array | Full status history (oldest first) | ### data.transaction The transaction is loaded with a specific column selection. Only the following fields are included: | Field | Type | Description | | ---------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------- | | `id` | integer | Transaction ID | | `deserved` | string | Amount the store owner receives (after platform fees) | | `platform_fees` | string | RMZ platform fee amount | | `total` | string | Total transaction amount | | `order_id` | integer | Associated order ID | | `created_at` | string | ISO 8601 timestamp | | `payment_method` | string | Payment method: `dokanpay`, `bank`, `paypal`, `coinbase`, `bank_transfer`, `free` | | `is_by_platform` | boolean | Whether the payment was processed through the RMZ payment system | | `human_format` | object | Human-readable transaction info (appended attribute) | | `human_format.payment.method` | string | Arabic payment method name (e.g., "البطائق الإئتمانية") | | `human_format.created_at` | string\|null | Formatted creation date | | `human_format.created_at_text` | string\|null | Relative creation time | | `human_format.store_balance_scheduled_at` | string\|null | Formatted scheduled balance date (always `null` in webhook since column is not selected) | | `human_format.store_balance_scheduled_at_text` | string\|null | Relative scheduled balance time (always `null` in webhook) | ### data.customer The full customer model is loaded (minus `$hidden` fields: `remember_token`, `last_login_ip`, `last_login_at`). | Field | Type | Description | | -------------- | ------------- | ----------------------------------- | | `id` | integer | Customer ID | | `firstName` | string\|null | First name | | `lastName` | string\|null | Last name | | `email` | string\|null | Email address | | `country_code` | integer\|null | Phone country code (e.g., `966`) | | `phone` | string\|null | Phone number (without country code) | | `is_banned` | boolean | Whether the customer is banned | | `ban_reason` | string\|null | Ban reason (if banned) | | `store_id` | integer | Store this customer belongs to | | `deleted_at` | string\|null | Soft delete timestamp | | `created_at` | string | ISO 8601 timestamp | | `updated_at` | string | ISO 8601 timestamp | ### data.items\[] | Field | Type | Description | | ------------ | ------------ | ------------------------------------------------------------------------------------- | | `id` | integer | Order item ID | | `quantity` | integer | Quantity purchased | | `item_id` | integer | Polymorphic item ID (typically a product ID) | | `item_type` | string | Polymorphic item type (e.g., `App\Models\StoreProduct`) | | `price` | string | Line item total price | | `fields` | object\|null | Custom fields submitted with the order item (JSON) | | `notes` | string\|null | Notes for this item | | `order_id` | integer | Parent order ID | | `created_at` | string | ISO 8601 timestamp | | `updated_at` | string | ISO 8601 timestamp | | `item` | object | The related product (loaded via polymorphic relation, includes soft-deleted products) | | `codes` | array | Delivered digital codes (for `code` type products) | ### data.items\[].item (Product) The full product model is included via `toArray()`. Key fields include: | Field | Type | Description | | -------------- | ------- | --------------------------------------------------------------- | | `id` | integer | Product ID | | `name` | string | Product name | | `price` | string | Original listed price | | `actual_price` | string | Current effective price (after discount, appended attribute) | | `type` | string | Product type: `code`, `file`, `subscription`, `service`, `card` | | `...` | | Additional product fields from the `store_products` table | ### data.items\[].codes\[] The full code model is included. Key fields: | Field | Type | Description | | --------- | ------- | ------------------------------------------------------ | | `id` | integer | Code ID | | `code` | string | The delivered digital code | | `is_used` | boolean | Whether the code has been marked as used | | `...` | | Additional fields from the `store_product_codes` table | ### data.status / data.statuses\[] | Field | Type | Description | | ------------------------------ | ------------ | ------------------------------------------------------------------------------------------------------- | | `id` | integer | Status record ID | | `reason` | string\|null | Reason for the status change | | `status` | integer | Status code (1=Waiting for Payment, 2=Under Review, 3=Processing, 4=Completed, 5=Cancelled, 6=Refunded) | | `model_id` | integer | Order ID | | `model_type` | string | Always `App\Models\Order` | | `created_at` | string | ISO 8601 timestamp when this status was set | | `updated_at` | string | ISO 8601 timestamp | | `human_format` | object | Human-readable status info (appended attribute) | | `human_format.text` | string | Arabic status name (e.g., "مكتمل", "ملغي", "قيد التنفيذ") | | `human_format.dashboard_color` | string | Dashboard color key (e.g., `success`, `error`, `warning`, `default`, `processing`) | | `human_format.color` | string | UI color key (e.g., `success`, `danger`, `warning`, `info`, `secondary`) | | `human_format.date_human` | string | Relative time | | `human_format.date_normal` | string | Formatted date string | The `statuses` array is ordered chronologically. Use it to build a timeline of the order's lifecycle. The `status` object always reflects the latest/current status. # Reliability & Retries Source: https://docs.rmz.gg/webhooks/reliability How RMZ handles webhook delivery failures, retries, and logging. RMZ uses [Spatie Webhook Server](https://github.com/spatie/laravel-webhook-server) under the hood for reliable webhook delivery. This page covers the retry strategy, timeouts, and how delivery status is tracked. ## Timeout Each webhook delivery attempt has a **3-second timeout**. If your server does not respond within 3 seconds, the attempt is marked as failed and may be retried. Return a 200 response immediately and process the webhook payload asynchronously (e.g., in a background job). This prevents timeouts for heavy processing. ## Retry Strategy Failed deliveries are retried using an **exponential backoff** strategy. The delay between attempts increases exponentially: | Attempt | Approximate Delay | | --------- | ---------------------------- | | 1st retry | \~10 seconds | | 2nd retry | \~100 seconds | | 3rd retry | \~1,000 seconds (\~17 min) | | 4th retry | \~10,000 seconds (\~2.8 hrs) | ### Configurable Retries When creating a webhook, you choose the maximum number of tries (1 to 5): | Tries Setting | Behavior | | ------------- | ----------------------------- | | 1 | Single attempt, no retries | | 2 | 1 initial attempt + 1 retry | | 3 | 1 initial attempt + 2 retries | | 4 | 1 initial attempt + 3 retries | | 5 | 1 initial attempt + 4 retries | The `tries` setting includes the initial attempt. Setting tries to `3` means 1 initial attempt and 2 retries for a total of 3 delivery attempts. ## Success Criteria A delivery is considered **successful** when your server responds with any `2xx` HTTP status code (200, 201, 202, etc.). A delivery is considered **failed** when: * Your server responds with a non-2xx status code (4xx, 5xx) * The connection times out (exceeds 3 seconds) * The connection is refused or DNS resolution fails ## Delivery Logging Every webhook delivery attempt is logged with the following information: | Field | Description | | ------------- | ----------------------------------------------------- | | `uri` | The destination URL | | `payload` | The webhook payload (encrypted at rest) | | `status_code` | HTTP response status code (0 if no response received) | | `response` | Response body from your server | | `headers` | Response headers | | `is_pending` | `true` while delivery is in progress | | `is_error` | `true` if the final attempt failed | Webhook payloads are encrypted at rest in the database for security. The `X-RMZ-REQUEST-ID` header value corresponds to the log entry ID. ## Automatic Disabling of Failing Webhooks RMZ periodically evaluates webhook delivery history and automatically disables webhooks that are consistently failing. The default criteria are: | Parameter | Default | Description | | -------------------------- | -------- | ----------------------------------------------------------------------------------- | | **Failure rate threshold** | 80% | Webhooks with a failure rate at or above this percentage are disabled | | **Minimum requests** | 5 | A webhook must have at least this many delivery attempts before it can be evaluated | | **Lookback period** | 3 months | Only delivery attempts within this window are considered | A delivery is counted as failed if `is_error` is true or the response status code is outside the 2xx range. When a webhook is disabled, the store owner can re-enable it from the dashboard after fixing the endpoint issue. ## SSL Verification By default, RMZ verifies the SSL certificate of your webhook endpoint. Self-signed certificates or invalid certificates will cause delivery to fail. Always use HTTPS for your webhook endpoints. While the webhook will attempt delivery to HTTP URLs, sensitive order data (including customer details and digital codes) is included in the payload. ## Idempotency Webhooks may be delivered more than once due to retries or network issues. Design your webhook handler to be **idempotent** — processing the same event twice should produce the same result. Use the `X-RMZ-REQUEST-ID` header to detect duplicate deliveries: ```javascript theme={null} const processedRequests = new Set(); app.post("/webhooks/rmz", (req, res) => { const requestId = req.headers["x-rmz-request-id"]; if (processedRequests.has(requestId)) { // Already processed, return success to prevent further retries return res.status(200).json({ received: true, duplicate: true }); } // Process the webhook... processedRequests.add(requestId); res.status(200).json({ received: true }); }); ``` In production, store processed request IDs in a database or Redis rather than in-memory. Use a TTL of 7 days to automatically clean up old entries. ## Best Practices 1. **Respond quickly.** Return `200` immediately and process asynchronously. Do not perform slow operations (API calls, database writes, email sending) before responding. 2. **Handle retries.** Use the `X-RMZ-REQUEST-ID` header for deduplication. Store processed IDs to avoid duplicate processing. 3. **Use HTTPS.** Protect sensitive customer and order data in transit. 4. **Verify signatures.** When signing is enabled, always [verify the signature](/webhooks/signature-verification) before processing the payload. 5. **Set appropriate retry counts.** Use higher retry counts (3-5) for critical integrations and lower counts (1-2) for non-critical notifications. 6. **Monitor delivery status.** Check the webhook logs in your dashboard to identify persistent failures. Fix endpoint issues promptly to avoid missing events. 7. **Handle all status codes.** If your server returns a non-2xx status intentionally (e.g., `422` for an invalid payload), be aware that RMZ will retry the delivery. # Signature Verification Source: https://docs.rmz.gg/webhooks/signature-verification Verify the authenticity of RMZ webhook deliveries using HMAC-SHA256 signatures. Every RMZ webhook delivery is signed with HMAC-SHA256. Verifying the signature ensures the request genuinely came from RMZ and was not tampered with in transit. Signature verification is the **only** reliable way to confirm a webhook is from RMZ. Anyone who learns your webhook URL can POST to it — without verification, they could grant themselves paid access (e.g., on a SaaS paywall) or trigger fulfillment on fake orders. ## How signing works 1. RMZ serializes the webhook payload to JSON 2. The JSON string is signed using HMAC-SHA256 with your webhook's secret key 3. The resulting hex-encoded hash is sent in the `Signature` header 4. Your server recomputes the hash and compares it to the header ```http theme={null} POST /your-endpoint HTTP/1.1 Content-Type: application/json Signature: a1b2c3d4e5f6... X-RMZ-REQUEST-ID: 12345 X-RMZ-WEBHOOKS: 1.2 {"event":"order.created","data":{...}} ``` ## Where to find your secret key In the dashboard: 1. Go to **الإعدادات** → **الويب هوك (Webhooks)** 2. Click **معاينة** on the webhook you want to verify 3. The **المفتاح السري للتوقيع (HMAC Signing Key)** field shows the secret. Click the eye icon to reveal it, the copy icon to copy. If you ever suspect the secret has leaked, click the **regenerate** (↻) button next to the key. The old key stops working immediately — make sure your server-side verification is updated **before** the next webhook fires. ## Verification steps Read the `Signature` header from the incoming request. Read the raw body as a string. **Do not parse to JSON first** — the signature is computed on the exact bytes RMZ sent. Re-serializing changes whitespace and key ordering. Calculate `HMAC-SHA256(raw_body, secret_key)` and hex-encode the result. Use a constant-time comparison (`hash_equals` in PHP, `hmac.compare_digest` in Python, `crypto.timingSafeEqual` in Node) to avoid timing attacks. Reject the request with HTTP 401 on mismatch. ## Code examples ```javascript Node.js (Express) theme={null} const crypto = require("crypto"); const express = require("express"); const app = express(); function verifySignature(rawBody, signature, secret) { const expected = crypto .createHmac("sha256", secret) .update(rawBody) .digest("hex"); // Both buffers must be the same length for timingSafeEqual. if (signature.length !== expected.length) return false; return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } // IMPORTANT: use express.raw, not express.json — verification needs raw bytes. app.post( "/webhooks/rmz", express.raw({ type: "application/json" }), (req, res) => { const signature = req.headers["signature"]; const secret = process.env.RMZ_WEBHOOK_SECRET; if (!verifySignature(req.body.toString(), signature, secret)) { return res.status(401).json({ error: "Invalid signature" }); } const payload = JSON.parse(req.body); // Safe to process — handle payload.event and payload.data here. res.status(200).json({ received: true }); } ); ``` ```python Python (Flask) theme={null} import hmac import hashlib from flask import Flask, request, jsonify app = Flask(__name__) def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool: expected = hmac.new( secret.encode("utf-8"), raw_body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected) @app.route("/webhooks/rmz", methods=["POST"]) def handle_webhook(): signature = request.headers.get("Signature", "") secret = "your_webhook_secret_key" if not verify_signature(request.data, signature, secret): return jsonify({"error": "Invalid signature"}), 401 payload = request.get_json() # Safe to process — handle payload["event"] and payload["data"] here. return jsonify({"received": True}), 200 ``` ```php PHP theme={null} 'Invalid signature']); exit; } $payload = json_decode($rawBody, true); // Safe to process — handle $payload['event'] and $payload['data'] here. http_response_code(200); echo json_encode(['received' => true]); ``` ```php Laravel theme={null} use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; function verifyWebhookSignature(string $rawBody, string $signature, string $secret): bool { $expected = hash_hmac('sha256', $rawBody, $secret); return hash_equals($expected, $signature); } Route::post('/webhooks/rmz', function (Request $request) { $signature = $request->header('Signature'); $secret = config('services.rmz.webhook_secret'); $rawBody = $request->getContent(); if (!verifyWebhookSignature($rawBody, $signature, $secret)) { abort(401, 'Invalid signature'); } $payload = json_decode($rawBody, true); // Safe to process — handle $payload['event'] and $payload['data'] here. return response()->json(['received' => true]); }); ``` ```rust Rust (Axum) theme={null} // Cargo.toml dependencies: // axum = "0.7" // hmac = "0.12" // sha2 = "0.10" // hex = "0.4" // subtle = "2.5" // tokio = { version = "1", features = ["full"] } use axum::{ body::Bytes, extract::State, http::{HeaderMap, StatusCode}, routing::post, Json, Router, }; use hmac::{Hmac, Mac}; use sha2::Sha256; use subtle::ConstantTimeEq; type HmacSha256 = Hmac; #[derive(Clone)] struct AppState { webhook_secret: String, } fn verify_signature(body: &[u8], signature_hex: &str, secret: &str) -> bool { let mut mac = match HmacSha256::new_from_slice(secret.as_bytes()) { Ok(m) => m, Err(_) => return false, }; mac.update(body); let expected = hex::encode(mac.finalize().into_bytes()); // Constant-time comparison to prevent timing attacks. expected.as_bytes().ct_eq(signature_hex.as_bytes()).into() } async fn webhook_handler( State(state): State, headers: HeaderMap, body: Bytes, ) -> Result, StatusCode> { let signature = headers .get("Signature") .and_then(|h| h.to_str().ok()) .unwrap_or(""); if !verify_signature(&body, signature, &state.webhook_secret) { return Err(StatusCode::UNAUTHORIZED); } let payload: serde_json::Value = serde_json::from_slice(&body).map_err(|_| StatusCode::BAD_REQUEST)?; // Safe to process — handle payload["event"] and payload["data"] here. let _ = payload; Ok(Json(serde_json::json!({ "received": true }))) } #[tokio::main] async fn main() { let state = AppState { webhook_secret: std::env::var("RMZ_WEBHOOK_SECRET") .expect("RMZ_WEBHOOK_SECRET not set"), }; let app = Router::new() .route("/webhooks/rmz", post(webhook_handler)) .with_state(state); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, app).await.unwrap(); } ``` ```go Go (net/http) theme={null} package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "io" "net/http" "os" ) func verifySignature(body []byte, signature, secret string) bool { mac := hmac.New(sha256.New, []byte(secret)) mac.Write(body) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(signature), []byte(expected)) } func webhookHandler(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "bad body", http.StatusBadRequest) return } signature := r.Header.Get("Signature") secret := os.Getenv("RMZ_WEBHOOK_SECRET") if !verifySignature(body, signature, secret) { http.Error(w, "invalid signature", http.StatusUnauthorized) return } // Safe to process — parse body and handle the event. w.WriteHeader(http.StatusOK) } ``` ## Common pitfalls The signature is computed on the exact JSON string RMZ sends. If you parse and re-serialize it, whitespace and key ordering can change — producing a different hash and a false mismatch. Always verify against the raw request body. Most common causes: * You're computing the hash on parsed JSON instead of the raw body * Your framework strips trailing whitespace or BOM from the request body * You copied the secret with leading/trailing whitespace * You're using the wrong webhook's secret (each webhook has its own key) * The webhook was rotated and you haven't updated your server with the new key Check that `Content-Length` matches the actual body length, and log both the expected and received hashes during debugging (then remove the logs). In the dashboard: 1. Open the webhook in **معاينة** mode 2. Click the **regenerate** (↻) button next to the key 3. Confirm — the old key stops working immediately 4. Update your server's `RMZ_WEBHOOK_SECRET` environment variable with the new key 5. Redeploy Plan a brief outage window when rotating, since any in-flight webhook delivered between the regeneration and your redeploy will fail verification. Yes. Every retry uses the **current** webhook secret. If you rotate the secret while a webhook is being retried, the next retry will be signed with the new key. Use it for **idempotency** in your handler — if you receive two requests with the same `X-RMZ-REQUEST-ID`, treat them as the same event. RMZ may retry on transient failures (5xx, network errors) up to the configured `tries` count. Even with signature verification, validate the payload shape before acting on it: check that `event` is one you handle and that `data` contains the expected fields. Defensive parsing prevents bugs from breaking-but-still-signed payloads (e.g., during RMZ rolling deploys).