# 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 (
;
}
```
## 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
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": "