Arova Back to site
On this page
Overview Quickstart Authentication Referrals Rate Limits Errors Webhooks Rule Conditions Rule Rewards Templates (SMS) Messages (SMS) API Console
API Reference
Customers Transactions Redemptions Campaigns Tiers Rules Branches Send SMS Send SMS with Template List Messages Get Message Templates (SMS) Template Variables
Resources
Swagger UI ReDoc
Developer Support For technical queries or integration assistance, contact hello@arovadigital.com.

Overview

The Arova API lets you integrate loyalty, CRM, and wallet features into your business applications. You can manage customers, record transactions, handle redemptions, and configure loyalty campaigns. ## Base URL All API endpoints are available at: ``` https://arovadigital.com/api/v1/ ``` ## API Format The API uses RESTful conventions. Requests and responses use JSON. All responses follow a consistent envelope format for errors.

Quickstart

This guide walks you through your first API call. You need an API key and a working internet connection. ## Step 1: Get an API key 1. Sign in to your Arova dashboard. 2. Go to **Settings > Developer API**. 3. Click **Generate Key**. 4. Choose a **Live Key** for production or a **Test Key** for exploration. 5. Copy the key. You will not see it again after you close the modal. ## Step 2: Make your first request Replace `<YOUR_API_KEY>` with your key and run one of these commands. ### cURL ``` curl -X GET "https://arovadigital.com/api/v1/branches/" \ -H "Authorization: Bearer <YOUR_API_KEY>" ``` ### Python ```python import requests headers = {"Authorization": "Bearer <YOUR_API_KEY>"} response = requests.get("https://arovadigital.com/api/v1/branches/", headers=headers) print(response.json()) ``` ### Node.js ```javascript fetch("https://arovadigital.com/api/v1/branches/", { headers: {"Authorization": "Bearer <YOUR_API_KEY>"} }) .then(res => res.json()) .then(data => console.log(data)); ``` ## Step 3: Verify the response A successful response returns HTTP 200 with a JSON array of your branches.

Authentication

All API requests require authentication using a Bearer token. Your API key is the token. ## How to authenticate Include the `Authorization` header in every request: ``` Authorization: Bearer arv_live_abc123def456... ``` ## API key types There are two types of API keys: - **Live keys** start with `arv_live_`. Use them in production. Rate limits depend on your billing plan. - **Test keys** start with `arv_test_`. Use them for development and testing. Rate limited to 5 requests per minute. Expire after 30 days. ## Security best practices - Keep your API key secret. Do not share it in client-side code or public repositories. - Rotate keys regularly. Generate a new key and update your applications before the old one expires. - Use separate keys for different environments (development, staging, production). - Revoke compromised keys immediately from the dashboard.

Referrals

Your customers can refer others to your loyalty program. When a referred customer makes their first purchase, the person who referred them earns bonus points. ## How it works Each customer gets a unique referral code in their wallet. The code looks like `your-merchant-slug:CODE123`. Customers share this code with friends and family. When a new customer signs up with a referral code, a pending referral is created. The referral is marked as qualified when the new customer makes their first qualifying transaction. The referrer earns bonus points based on your loyalty rules. ## Referral rules You can create loyalty rules with the REFERRAL trigger type. These rules control how many bonus points the referrer gets. You can set conditions on: * Minimum spend amount for the referred customer's first purchase * Maximum spend amount * Customer tier of the referrer (for example, reward only Gold tier members) The reward can be a flat bonus, a points multiplier, or an override of the earn rate. ## Referral statuses A referral moves through these statuses: * PENDING - The referral code was applied but the new customer has not made a purchase yet. Referrals expire after 30 days. * QUALIFIED - The first purchase was detected. Points are being calculated. * REWARDED - Bonus points were awarded to the referrer's wallet. * EXPIRED - The referral expired before a purchase was made. * REVERSED - The qualifying transaction was voided and the bonus points were taken back. ## Requirements Referrals are available on plans that include the referrals feature. Check your billing page to confirm access. ## Best practices * Make sure your customers know their referral code. Show it in their wallet and remind them to share it. * Create a referral loyalty rule that rewards referrers with a flat bonus. This is the simplest setup. * Test the referral flow with a test account before launching to make sure rules work as expected.

Rate Limits

Rate limits protect the API from abuse and ensure fair usage for all merchants. ## Per-plan limits | Plan | Rate Limit | |------|-----------| | Test Key | 5 requests per minute | | Free | 100 requests per minute | | Launch | 300 requests per minute | | Growth | 500 requests per minute | | Scale | 1,000 requests per minute | | Enterprise | 2,000 requests per minute | ## Rate limit headers Every response includes rate limit information in the headers: ``` X-RateLimit-Limit: 100 X-RateLimit-Remaining: 87 X-RateLimit-Reset: 1705500000 ``` When you exceed the limit, the API returns HTTP 429. The `Retry-After` header tells you how many seconds to wait before retrying.

Errors

The API returns errors in a consistent JSON envelope format. ## Error envelope ```json { "status": "error", "error": { "code": "INVALID_REQUEST", "message": "A human-readable description of the problem.", "details": { "field_name": "Additional context about the error." } } } ``` ## Common error codes | Code | HTTP Status | Description | |------|------------|-------------| | `UNAUTHORIZED` | 401 | Missing or invalid API key. | | `RATE_LIMIT_EXCEEDED` | 429 | Too many requests. Check Retry-After header. | | `INVALID_REQUEST` | 400 | Request body validation failed. Check the details field. | | `NOT_FOUND` | 404 | The requested resource does not exist. | | `CONFLICT` | 409 | The request conflicts with existing data (e.g., duplicate idempotency key). | | `APPROVAL_PIN_REQUIRED` | 403 | An approval PIN is required to complete this action. | | `INVALID_APPROVAL_PIN` | 403 | The provided approval PIN is incorrect. |

Webhooks

Webhooks notify your application when events happen in your Arova account. Configure webhook endpoints from the dashboard. ## Event types | Event | Description | |-------|-------------| | `customer.points_earned` | A customer earned points from a transaction. | | `redemption.created` | A new redemption request was submitted. | | `redemption.approved` | A pending redemption request was approved. | | `redemption.rejected` | A pending redemption request was rejected. | ## Signature verification Each webhook payload includes an `X-Arova-Signature` header. The signature is an HMAC-SHA256 hash of the request body. Use your webhook secret to verify it. ```python import hashlib import hmac def verify_webhook_signature(payload_body, signature_header, secret): expected = hmac.new( secret.encode("utf-8"), payload_body, hashlib.sha256, ).hexdigest() return hmac.compare_digest(expected, signature_header) ``` ## Retry policy If your endpoint does not return HTTP 200, the webhook is retried up to 3 times with exponential backoff.

Rule Conditions

When creating or updating a **Loyalty Rule** via the API, the `condition_json` field defines **when** the rule fires. Conditions are evaluated together with AND logic — every condition present must be satisfied for the rule to match. If `condition_json` is an empty object `{}`, the rule matches every qualifying event. ## Trigger Types Each rule has a `trigger_type` that determines which condition keys are valid: | Trigger Type | Description | Fires When | |---|---|---| | `SPEND` | Transaction-based rules | A transaction is recorded via the API or dashboard | | `REFERRAL` | Referral reward rules | A referred customer completes a qualifying purchase | | `BIRTHDAY` | Birthday bonus rules | A customer's birthday occurs (auto-evaluated daily) | ## Condition Keys by Trigger Type | Condition Key | SPEND | REFERRAL | BIRTHDAY | Type | Description | |---|---|---|---|---|---| | `min_spend` | ✓ | ✓ | ✓ | `number` | Minimum transaction amount (inclusive) | | `max_spend` | ✓ | ✓ | ✓ | `number` | Maximum transaction amount (inclusive) | | `min_visits` | ✓ | — | — | `integer` | Minimum customer visit count (≥ 1) | | `max_visits` | ✓ | — | — | `integer` | Maximum customer visit count (≥ 1) | | `min_lifetime_spend` | ✓ | — | — | `number` | Minimum customer lifetime spend | | `days_of_week` | ✓ | — | — | `integer[]` | Days the rule is active (0=Mon … 6=Sun) | | `branches` | ✓ | — | — | `string[]` | Branch UUIDs where the rule applies | | `customer_tier` | ✓ | ✓ | ✓ | `string` or `string[]` | Tier UUID(s) the customer must belong to | | `time_range` | ✓ | — | — | `object` or `string` | Time window (supports overnight ranges) | ## Condition Key Details ### min_spend / max_spend Numeric values. The transaction amount must fall within this range (inclusive on both ends). If only one is set, the other is unbounded. ```json { "min_spend": 50, "max_spend": 200 } ``` `min_spend` must be ≤ `max_spend` if both are set. Values must be ≥ 0. ### min_visits / max_visits Integer values. The customer's total non-voided transaction count must fall within this range. ```json { "min_visits": 5, "max_visits": 20 } ``` This is useful for targeting first-time customers (`"min_visits": 1, "max_visits": 1`) or loyal regulars (`"min_visits": 10`). ### min_lifetime_spend Numeric. The customer's cumulative transaction total must meet or exceed this value. ```json { "min_lifetime_spend": 500 } ``` ### days_of_week Array of integers from 0 to 6, where **0 = Monday** and **6 = Sunday** (Python `weekday()` convention). ```json { "days_of_week": [4, 5] } ``` This example restricts the rule to Fridays and Saturdays only. ### branches Array of branch UUID strings. The transaction must occur at one of these branches. ```json { "branches": ["a1b2c3d4-...", "e5f6g7h8-..."] } ``` Use `GET /api/v1/branches/` to retrieve your branch IDs. ### customer_tier A single tier UUID string or an array of tier UUIDs. The customer's current wallet tier must match. ```json { "customer_tier": "tier-uuid-here" } ``` Or multiple tiers: ```json { "customer_tier": ["gold-tier-uuid", "platinum-tier-uuid"] } ``` Use `GET /api/v1/tiers/` to retrieve your tier IDs. ### time_range Defines a time window during which the rule is active. Supports overnight ranges (e.g. happy hour that crosses midnight). **Object format:** ```json { "time_range": { "start": "22:00", "end": "03:00" } } ``` **String format:** ```json { "time_range": "09:00-17:00" } ``` Times are in `HH:MM` format (24-hour clock), evaluated in the server's local timezone. ## Full Condition Examples **Double points on weekday evenings for Gold members spending ≥ 100:** ```json { "min_spend": 100, "days_of_week": [0, 1, 2, 3, 4], "time_range": { "start": "18:00", "end": "23:59" }, "customer_tier": "gold-tier-uuid" } ``` **First-purchase bonus at a specific branch:** ```json { "min_visits": 1, "max_visits": 1, "branches": ["flagship-branch-uuid"] } ``` **VIP reward for high-lifetime-spend customers:** ```json { "min_lifetime_spend": 5000, "customer_tier": ["platinum-tier-uuid", "diamond-tier-uuid"] } ```

Rule Rewards

The `reward_json` field defines **what bonus** a customer receives when the rule's conditions are met. Reward values stack — you can combine multiple reward types in a single rule. ## Reward Keys | Key | Type | Description | |---|---|---| | `flat_bonus` | `integer` | Fixed number of bonus points added. Must be a whole number ≥ 0. | | `multiplier` | `number` | Multiplies the base points earned. A value of `2.0` means double points (1× extra). | | `override_earn_rate` | `number` | Replaces the merchant's default earn rate with this value. Specified as points per currency unit. | ## Reward Precedence When a rule contains multiple reward keys, they are applied in this order and **stacked**: 1. **`override_earn_rate`** — recalculates base points using the override rate, then adds the difference as bonus. 2. **`multiplier`** — multiplies the original base points by `(multiplier - 1)` and adds that as bonus. 3. **`flat_bonus`** — adds the flat bonus on top. All bonuses are non-negative. If a calculation results in a negative bonus, it is clamped to zero. ## Reward Key Details ### flat_bonus Awards a fixed number of bonus points regardless of the transaction amount. ```json { "flat_bonus": 50 } ``` Must be a **whole number** (no decimals). A transaction of any amount matching the rule's conditions will receive exactly 50 bonus points on top of the standard earn. ### multiplier Multiplies the base points earned from the transaction. The **extra** points are added as a bonus. ```json { "multiplier": 3.0 } ``` If the base earn is 10 points, a `3.0` multiplier produces `10 × (3.0 - 1.0) = 20` bonus points, for a total of 30 points. | Multiplier | Effect | Bonus on 10 base pts | |---|---|---| | `1.5` | 1.5× points | +5 bonus | | `2.0` | Double points | +10 bonus | | `3.0` | Triple points | +20 bonus | | `5.0` | 5× points | +40 bonus | ### override_earn_rate Replaces the default earn rate for this transaction. The earn rate is expressed as **points per currency unit**. ```json { "override_earn_rate": 2.0 } ``` If the default earn rate is 1.0 and a customer spends 100, they'd normally earn 100 points. With `override_earn_rate: 2.0`, they earn 200 points — the 100-point difference is the bonus. ## Combining Rewards You can use multiple reward keys together. They stack additively: ```json { "multiplier": 2.0, "flat_bonus": 25 } ``` On a 100-point base earn: `100 × (2.0 - 1.0) = 100` bonus from multiplier + `25` flat = **125 total bonus points**. ## Full Rule Examples ### Weekend Double Points ```json { "name": "Weekend Double Points", "trigger_type": "SPEND", "condition_json": { "days_of_week": [5, 6], "min_spend": 20 }, "reward_json": { "multiplier": 2.0 } } ``` ### Birthday Bonus ```json { "name": "Birthday Gift", "trigger_type": "BIRTHDAY", "condition_json": {}, "reward_json": { "flat_bonus": 100 } } ``` ### VIP Happy Hour ```json { "name": "VIP Happy Hour", "trigger_type": "SPEND", "condition_json": { "time_range": { "start": "16:00", "end": "19:00" }, "customer_tier": "gold-tier-uuid", "min_spend": 50 }, "reward_json": { "multiplier": 3.0, "flat_bonus": 10 } } ``` ### First Purchase Welcome Bonus ```json { "name": "Welcome Bonus", "trigger_type": "SPEND", "condition_json": { "min_visits": 1, "max_visits": 1 }, "reward_json": { "flat_bonus": 50 } } ``` ### High-Spend Override Rate ```json { "name": "Premium Earn Rate", "trigger_type": "SPEND", "condition_json": { "min_spend": 500, "customer_tier": ["platinum-tier-uuid", "diamond-tier-uuid"] }, "reward_json": { "override_earn_rate": 3.0 } } ```

Templates (SMS)

SMS templates let you create reusable message bodies with variable placeholders. Instead of building the full message each time you use the API, you define a template once and use it with different variable values. ## Template syntax Variables use `<%variable_key%>` syntax inside the template body. When you send a message using the template, you provide values for each variable. The system replaces `<%variable_key%>` with your value before sending. For example, a template body might look like: ``` Hi <%customer_first_name%>, welcome to <%merchant_name%>! ``` When you send with `{"customer_first_name": "Jane", "merchant_name": "Arova"}`, the sent message becomes: ``` Hi Jane, welcome to Arova! ``` ## Template CRUD Templates are managed via the `/api/v1/templates/` endpoint. The API supports full CRUD operations. ### Creating a template Send a POST request with the template fields: ```json { "name": "Welcome SMS", "description": "Sent when a customer joins", "body": "Hi <%customer_first_name%>, welcome to <%merchant_name%>!", "required_variable_keys": ["customer_first_name"], "is_active": true } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | yes | Unique name per merchant. Must not conflict with an existing template. | | `description` | string | no | Optional description of the template's purpose. | | `body` | string | yes | Message body with optional `<%variable_key%>` placeholders. All variables in the body are validated against the tenant's variable registry. Unknown variables are rejected. | | `required_variable_keys` | string[] | no | Variables that must be provided when using this template. Each key listed here must appear as `<%key%>` in the body. | | `is_active` | boolean | no | Whether the template is available for sending. Defaults to `true`. | ### Listing templates ``` GET /api/v1/templates/ ``` Returns an array of all templates for your merchant, ordered by name. ### Retrieving a template ``` GET /api/v1/templates/{id}/ ``` Returns a single template by its ID. ### Updating a template ``` PUT /api/v1/templates/{id}/ ``` Send the full updated template object. All required fields must be present. ### Deleting a template ``` DELETE /api/v1/templates/{id}/ ``` Returns HTTP 204 on success. ## Validation rules Template creation and updates enforce these rules: 1. **Name uniqueness** — Template names must be unique within your merchant. A duplicate name returns a validation error. 2. **Body variable validation** — Every `<%variable%>` in the body must be a known variable registered for your merchant. Unknown variables are rejected with an error listing them. 3. **Required variable presence** — Each key listed in `required_variable_keys` must appear as `<%key%>` somewhere in the body text. If a required variable is missing from the body, the API returns a validation error. ## Available variables The API exposes a list of all supported variables at `GET /api/v1/templates/variables/`. Each variable includes its key, token, group label, and kind: ```json [ { "key": "customer_first_name", "token": "<%customer_first_name%>", "group": "Customer", "kind": "standard" }, { "key": "points_balance", "token": "<%points_balance%>", "group": "Rewards", "kind": "standard" } ] ``` Variables are grouped by category: | Group | Variables | |-------|-----------| | Customer | `customer_display_name`, `customer_first_name`, `customer_phone` | | Merchant | `merchant_name` | | Rewards | `current_tier`, `points_balance`, `lifetime_spend`, `last_visit_date` | | Campaign | `campaign_name`, `rule_name`, `reward_summary` | | Birthday | `birth_date` | | Custom fields | Any active custom fields prefixed with `custom_` | ## Required variables You can mark certain variables as required when creating a template. If a required variable is missing when you use the template, the API returns a validation error. Define required variables in the `required_variable_keys` array. ## Using a template to send SMS Pass `template_id` and `template_variables` to the messages endpoint instead of a raw `message`: ```json { "to_e164": "+233551234567", "template_id": "t1e2m3p4-...", "template_variables": { "customer_first_name": "Jane", "merchant_name": "Arova" } } ``` The system looks up the template, replaces the variables, and sends the rendered message. You cannot provide both `message` and `template_id` in the same request — choose one. ## Best practices * Create templates for common message types like welcome messages, order updates, and promotions. * Mark the variables your template body actually uses as required to catch missing values early. * Use the `GET /api/v1/templates/variables/` endpoint to discover available variables for your tenant. * Keep template bodies under 480 characters. Longer messages are split into multiple SMS segments.

Messages (SMS)

The Messages API lets you send SMS text messages through your Arova account. Messages are dispatched using your configured SMS provider and tracked in your dispatch logs. ## How it works Send a POST request to the messages endpoint with a phone number and message. The system validates the request, checks your subscription quota, and dispatches the message through your provider. ## Sending a message There are two ways to send a message: **Raw message** — provide the message body directly: ```json { "to_e164": "+233551234567", "message": "Your order #1234 is ready for pickup.", "idempotency_key": "550e8400-e29b-41d4-a716-446655440000" } ``` **Using a template** — provide a template ID and variable values. The system renders the template body before sending: ```json { "to_e164": "+233551234567", "template_id": 1, "template_variables": { "customer_first_name": "Jane", "merchant_name": "Arova" } } ``` You must provide either `message` or `template_id` — not both. If you provide `template_id`, `template_variables` is required. ### Request fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `to_e164` | string | yes | Recipient phone number in E.164 format starting with + | | `message` | string | conditional | Raw message text. Required if `template_id` is not provided. Max 480 characters. | | `template_id` | integer | conditional | ID of an active SMS template. Required if `message` is not provided. | | `template_variables` | object | conditional | Variable values used with `template_id`. Required when `template_id` is provided. | | `idempotency_key` | UUID | no | UUID v4 for safe retries. Prevents duplicate sends. | ## Response fields When you send a message, the API returns a dispatch log record: | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique dispatch identifier. Store this to check status later. | | `event_type` | string | Always `"API_MESSAGE"` for API-sent messages. | | `to_e164` | string | The recipient phone number. | | `status` | string | Message status. See status lifecycle below. | | `provider` | string | The SMS provider that handled the message (e.g. `"hubtel"`, `"arkesel"`). | | `sender_id` | string | The sender ID that was used. Empty string uses the system default. | | `estimated_segments` | integer | Number of SMS segments consumed. | | `error_code` | string or null | Provider error code if the send failed. | | `error_message` | string or null | Human-readable error description if the send failed. | | `created_at` | datetime | When the dispatch was created. | | `sent_at` | datetime or null | When the provider accepted the message. | | `delivered_at` | datetime or null | When delivery was confirmed by the network. | ## Message status lifecycle Messages move through these statuses: ``` QUEUED ──→ SENT ──→ DELIVERED │ └──→ FAILED ``` | Status | Description | |--------|-------------| | `QUEUED` | The message has been recorded but not yet handed to the provider. | | `SENT` | The SMS provider accepted the message for delivery to the recipient's network. | | `DELIVERED` | The network confirmed delivery to the recipient's device (requires DLR/reconciliation). | | `FAILED` | The provider rejected the message. Check `error_code` and `error_message` for details. | ## Listing messages To retrieve all messages sent through the API: ``` GET /api/v1/messages/ ``` Returns a paginated list of dispatch logs ordered by creation date (newest first). ## Checking a specific message To check the status of a single message: ``` GET /api/v1/messages/{id}/ ``` Returns the full dispatch log record, including the current status and delivery timestamps. Use this to poll for delivery confirmation. ## Phone numbers Phone numbers must be in E.164 format. This means a plus sign (+) followed by the country code and phone number. The API enforces this — requests without a leading + are rejected. ## Idempotency To safely retry requests without sending duplicate messages, provide an idempotency key. Generate a UUID v4 on your end and pass it as the `idempotency_key` parameter. If you send the same key again, the API returns the original dispatch record instead of sending a new message. This is useful when network errors cause timeouts. You can retry with the same key knowing the recipient will only receive one message. ## Quotas SMS messages count against your subscription plan's SMS quota. Each message consumes one segment from your monthly allowance. Messages longer than 160 characters are split into multiple segments and consume more quota. You can check your remaining quota from the dashboard. ## Best practices * Store the message ID returned by the API. Use it to check delivery status later by calling `GET /api/v1/messages/{id}/`. * Generate a UUID v4 as your idempotency key before each request. Use it when retrying to prevent duplicate sends. * Use templates for frequently sent message types. They enforce variable consistency and catch errors early. * Keep messages short. Messages over 160 characters are split into multiple segments and cost more. * Respect your customers. Only send messages they have opted in to receive.

API Console

Try the API live from your browser. Paste your API key, select an endpoint, and send a request. Use a test key for exploration - test keys start with arv_test_.

Open full Swagger UI

200

        

Customers

Create, read, update, and delete loyalty customers. Each customer is linked to a branch and has a wallet balance.

GET POST PUT PATCH DELETE
/api/v1/customers/

Request Fields

Field Type Required Description
id UUID Read only None
phone_number Char Required E.164 format
display_name Char Optional None
birth_date Date Optional None
consent_status Boolean Optional None
lifetime_spend Decimal Read only None
last_visit_at DateTime Read only None
is_active Boolean Optional None
custom_data JSON Optional None
branch_id UUID Optional None
wallet_balance Computed Read only None
tier_name Computed Read only None
created_at DateTime Read only None
updated_at DateTime Read only None

Example Request

curl -X GET "https://arovadigital.com/api/v1/customers/" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
  "phone_number": "+233551234567",
  "display_name": "Jane Doe",
  "branch_id": "3a1b2c3d-...",
  "birth_date": "1990-06-15"
}'
import requests

headers = {
    "Authorization": "Bearer <YOUR_API_KEY>",
    "Content-Type": "application/json",
}
data = {
  "phone_number": "+233551234567",
  "display_name": "Jane Doe",
  "branch_id": "3a1b2c3d-...",
  "birth_date": "1990-06-15"
}
response = requests.get(
    "https://arovadigital.com/api/v1/customers/",
    headers=headers,
    json=data,
)
print(response.json())
fetch("https://arovadigital.com/api/v1/customers/", {
  method: "GET",
  headers: {
    "Authorization": "Bearer <YOUR_API_KEY>",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "phone_number": "+233551234567",
  "display_name": "Jane Doe",
  "branch_id": "3a1b2c3d-...",
  "birth_date": "1990-06-15"
}),
})
  .then(res => res.json())
  .then(data => console.log(data));

Example Response

{
  "id": "a1b2c3d4-...",
  "phone_number": "+233551234567",
  "display_name": "Jane Doe",
  "wallet_balance": 250,
  "tier_name": "Gold",
  "branch_id": "3a1b2c3d-...",
  "created_at": "2026-01-15T10:30:00Z"
}

Transactions

Record customer transactions and award loyalty points. Idempotent — use the idempotency_key to safely retry.

GET POST
/api/v1/transactions/

Request Fields

Field Type Required Description
id UUID Read only None
tenant_customer_id UUID Required None
branch_id UUID Required None
amount Decimal Required None
currency_code Char Optional None
idempotency_key UUID Optional None
metadata JSON Optional None
points_earned Decimal Read only None
txn_timestamp DateTime Read only None

Example Request

curl -X GET "https://arovadigital.com/api/v1/transactions/" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
  "tenant_customer_id": "a1b2c3d4-...",
  "branch_id": "3a1b2c3d-...",
  "amount": "150.00",
  "currency_code": "GHS",
  "idempotency_key": "e5f6g7h8-..."
}'
import requests

headers = {
    "Authorization": "Bearer <YOUR_API_KEY>",
    "Content-Type": "application/json",
}
data = {
  "tenant_customer_id": "a1b2c3d4-...",
  "branch_id": "3a1b2c3d-...",
  "amount": "150.00",
  "currency_code": "GHS",
  "idempotency_key": "e5f6g7h8-..."
}
response = requests.get(
    "https://arovadigital.com/api/v1/transactions/",
    headers=headers,
    json=data,
)
print(response.json())
fetch("https://arovadigital.com/api/v1/transactions/", {
  method: "GET",
  headers: {
    "Authorization": "Bearer <YOUR_API_KEY>",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "tenant_customer_id": "a1b2c3d4-...",
  "branch_id": "3a1b2c3d-...",
  "amount": "150.00",
  "currency_code": "GHS",
  "idempotency_key": "e5f6g7h8-..."
}),
})
  .then(res => res.json())
  .then(data => console.log(data));

Example Response

{
  "id": "i9j0k1l2-...",
  "points_earned": "75.0000",
  "txn_timestamp": "2026-01-15T10:30:00Z"
}

Redemptions

Submit, approve, and reject customer redemption requests. Supports OTP and owner approval workflows.

GET POST PUT PATCH
/api/v1/redemptions/

Request Fields

Field Type Required Description
id UUID Read only None
tenant_customer_id UUID Required None
branch_id UUID Required None
requested_points Decimal Required None
requested_value Decimal Optional None
reason Char Optional None
status Choice Read only None
requires_otp Boolean Read only None
requires_owner_approval Boolean Read only None
created_at DateTime Read only None
resolved_at DateTime Read only None

Example Request

curl -X GET "https://arovadigital.com/api/v1/redemptions/" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
  "tenant_customer_id": "a1b2c3d4-...",
  "branch_id": "3a1b2c3d-...",
  "requested_points": 500,
  "requested_value": "50.00",
  "reason": "Birthday reward"
}'
import requests

headers = {
    "Authorization": "Bearer <YOUR_API_KEY>",
    "Content-Type": "application/json",
}
data = {
  "tenant_customer_id": "a1b2c3d4-...",
  "branch_id": "3a1b2c3d-...",
  "requested_points": 500,
  "requested_value": "50.00",
  "reason": "Birthday reward"
}
response = requests.get(
    "https://arovadigital.com/api/v1/redemptions/",
    headers=headers,
    json=data,
)
print(response.json())
fetch("https://arovadigital.com/api/v1/redemptions/", {
  method: "GET",
  headers: {
    "Authorization": "Bearer <YOUR_API_KEY>",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "tenant_customer_id": "a1b2c3d4-...",
  "branch_id": "3a1b2c3d-...",
  "requested_points": 500,
  "requested_value": "50.00",
  "reason": "Birthday reward"
}),
})
  .then(res => res.json())
  .then(data => console.log(data));

Example Response

{
  "id": "m3n4o5p6-...",
  "status": "pending",
  "requires_otp": true,
  "requires_owner_approval": false,
  "created_at": "2026-01-15T10:30:00Z"
}

Campaigns

Manage loyalty campaigns. Each campaign can have multiple rules that define when and how points are earned.

GET POST PUT PATCH DELETE
/api/v1/campaigns/

Request Fields

Field Type Required Description
id UUID Read only None
name Char Required None
description Char Optional None
starts_at DateTime Optional None
ends_at DateTime Optional None
priority Integer Optional None
is_active Boolean Optional None
rules ListSerializer Read only None
created_at DateTime Read only None

Example Request

curl -X GET "https://arovadigital.com/api/v1/campaigns/" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Summer Bonus",
  "description": "Double points on all purchases",
  "starts_at": "2026-06-01T00:00:00Z",
  "ends_at": "2026-08-31T23:59:59Z",
  "is_active": true
}'
import requests

headers = {
    "Authorization": "Bearer <YOUR_API_KEY>",
    "Content-Type": "application/json",
}
data = {
  "name": "Summer Bonus",
  "description": "Double points on all purchases",
  "starts_at": "2026-06-01T00:00:00Z",
  "ends_at": "2026-08-31T23:59:59Z",
  "is_active": true
}
response = requests.get(
    "https://arovadigital.com/api/v1/campaigns/",
    headers=headers,
    json=data,
)
print(response.json())
fetch("https://arovadigital.com/api/v1/campaigns/", {
  method: "GET",
  headers: {
    "Authorization": "Bearer <YOUR_API_KEY>",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "name": "Summer Bonus",
  "description": "Double points on all purchases",
  "starts_at": "2026-06-01T00:00:00Z",
  "ends_at": "2026-08-31T23:59:59Z",
  "is_active": true
}),
})
  .then(res => res.json())
  .then(data => console.log(data));

Example Response

{
  "id": "q7r8s9t0-...",
  "name": "Summer Bonus",
  "is_active": true,
  "rules": [],
  "created_at": "2026-01-15T10:30:00Z"
}

Tiers

Configure loyalty tiers. Each tier defines the point multiplier and benefits for customers who reach it.

GET POST PUT PATCH DELETE
/api/v1/tiers/

Request Fields

Field Type Required Description
id UUID Read only None
name Char Required None
min_points Decimal Optional None
point_multiplier Decimal Optional None
benefits_summary Char Optional None
is_active Boolean Optional None
created_at DateTime Read only None

Example Request

curl -X GET "https://arovadigital.com/api/v1/tiers/" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Gold",
  "min_points": 1000,
  "point_multiplier": "2.0",
  "benefits_summary": "2x points, free delivery",
  "is_active": true
}'
import requests

headers = {
    "Authorization": "Bearer <YOUR_API_KEY>",
    "Content-Type": "application/json",
}
data = {
  "name": "Gold",
  "min_points": 1000,
  "point_multiplier": "2.0",
  "benefits_summary": "2x points, free delivery",
  "is_active": true
}
response = requests.get(
    "https://arovadigital.com/api/v1/tiers/",
    headers=headers,
    json=data,
)
print(response.json())
fetch("https://arovadigital.com/api/v1/tiers/", {
  method: "GET",
  headers: {
    "Authorization": "Bearer <YOUR_API_KEY>",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "name": "Gold",
  "min_points": 1000,
  "point_multiplier": "2.0",
  "benefits_summary": "2x points, free delivery",
  "is_active": true
}),
})
  .then(res => res.json())
  .then(data => console.log(data));

Example Response

{
  "id": "u1v2w3x4-...",
  "name": "Gold",
  "min_points": 1000,
  "point_multiplier": "2.0",
  "is_active": true,
  "created_at": "2026-01-15T10:30:00Z"
}

Rules

Define loyalty rules for campaigns. Rules use a trigger/condition/reward pattern. This is covered in more detail below.

GET POST PUT PATCH DELETE
/api/v1/rules/

Request Fields

Field Type Required Description
id UUID Read only None
campaign PrimaryKeyRelated Optional None
name Char Required None
trigger_type Char Optional None
condition_json JSON Optional None
reward_json JSON Optional None
is_active Boolean Optional None
created_at DateTime Read only None

Example Request

curl -X GET "https://arovadigital.com/api/v1/rules/" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
  "campaign": "q7r8s9t0-...",
  "name": "Double points on weekends",
  "trigger_type": "SPEND",
  "condition_json": {
    "days_of_week": [
      5,
      6
    ],
    "min_spend": 20
  },
  "reward_json": {
    "multiplier": 2.0
  },
  "is_active": true
}'
import requests

headers = {
    "Authorization": "Bearer <YOUR_API_KEY>",
    "Content-Type": "application/json",
}
data = {
  "campaign": "q7r8s9t0-...",
  "name": "Double points on weekends",
  "trigger_type": "SPEND",
  "condition_json": {
    "days_of_week": [
      5,
      6
    ],
    "min_spend": 20
  },
  "reward_json": {
    "multiplier": 2.0
  },
  "is_active": true
}
response = requests.get(
    "https://arovadigital.com/api/v1/rules/",
    headers=headers,
    json=data,
)
print(response.json())
fetch("https://arovadigital.com/api/v1/rules/", {
  method: "GET",
  headers: {
    "Authorization": "Bearer <YOUR_API_KEY>",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "campaign": "q7r8s9t0-...",
  "name": "Double points on weekends",
  "trigger_type": "SPEND",
  "condition_json": {
    "days_of_week": [
      5,
      6
    ],
    "min_spend": 20
  },
  "reward_json": {
    "multiplier": 2.0
  },
  "is_active": true
}),
})
  .then(res => res.json())
  .then(data => console.log(data));

Example Response

{
  "id": "y5z6a7b8-...",
  "campaign": "q7r8s9t0-...",
  "name": "Double points on weekends",
  "trigger_type": "purchase.completed",
  "is_active": true,
  "created_at": "2026-01-15T10:30:00Z"
}

Branches

List and retrieve business branches. Read-only — manage branches from the dashboard.

GET
/api/v1/branches/

Request Fields

Field Type Required Description
id UUID Read only None
name Char Required None
location_label Char Optional None
timezone Char Optional None
currency_code Char Optional None
is_active Boolean Optional None
created_at DateTime Read only None

Example Response

[
  {
    "id": "c9d0e1f2-...",
    "name": "Accra Main",
    "location_label": "Kotoka Airport, Accra",
    "timezone": "Africa/Accra",
    "currency_code": "GHS",
    "is_active": true,
    "created_at": "2026-01-15T10:30:00Z"
  }
]

Send SMS

Send an SMS message to a phone number. You can provide the message body directly, or use a template. Idempotent — provide an idempotency key to safely retry without duplicates.

POST
/api/v1/messages/

Request Fields

Field Type Required Description
id UUID Read only None
event_type Char Read only None
to_e164 Char Read only None
status Choice Read only None
provider Char Read only None
sender_id Char Read only None
estimated_segments Integer Read only None
error_code Char Read only None
error_message Char Read only None
created_at DateTime Read only None
sent_at DateTime Read only None
delivered_at DateTime Read only None

Example Request

curl -X POST "https://arovadigital.com/api/v1/messages/" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
  "to_e164": "+233551234567",
  "message": "Your order #1234 is ready for pickup.",
  "idempotency_key": "550e8400-e29b-41d4-a716-446655440000"
}'
import requests

headers = {
    "Authorization": "Bearer <YOUR_API_KEY>",
    "Content-Type": "application/json",
}
data = {
  "to_e164": "+233551234567",
  "message": "Your order #1234 is ready for pickup.",
  "idempotency_key": "550e8400-e29b-41d4-a716-446655440000"
}
response = requests.post(
    "https://arovadigital.com/api/v1/messages/",
    headers=headers,
    json=data,
)
print(response.json())
fetch("https://arovadigital.com/api/v1/messages/", {
  method: "POST",
  headers: {
    "Authorization": "Bearer <YOUR_API_KEY>",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "to_e164": "+233551234567",
  "message": "Your order #1234 is ready for pickup.",
  "idempotency_key": "550e8400-e29b-41d4-a716-446655440000"
}),
})
  .then(res => res.json())
  .then(data => console.log(data));

Example Response

{
  "id": "d1e2f3a4-...",
  "event_type": "API_MESSAGE",
  "to_e164": "+233551234567",
  "status": "SENT",
  "provider": "hubtel",
  "sender_id": "",
  "estimated_segments": 1,
  "error_code": null,
  "error_message": null,
  "created_at": "2026-01-15T10:30:00Z",
  "sent_at": "2026-01-15T10:30:01Z",
  "delivered_at": null
}

Send SMS with Template

Send an SMS using a template. The system renders the template body with the provided variable values before dispatching. You must provide template_id and template_variables instead of a raw message.

POST
/api/v1/messages/

Request Fields

Field Type Required Description
id UUID Read only None
event_type Char Read only None
to_e164 Char Read only None
status Choice Read only None
provider Char Read only None
sender_id Char Read only None
estimated_segments Integer Read only None
error_code Char Read only None
error_message Char Read only None
created_at DateTime Read only None
sent_at DateTime Read only None
delivered_at DateTime Read only None

Example Request

curl -X POST "https://arovadigital.com/api/v1/messages/" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
  "to_e164": "+233551234567",
  "template_id": 1,
  "template_variables": {
    "customer_first_name": "Jane",
    "merchant_name": "Arova"
  },
  "idempotency_key": "550e8400-e29b-41d4-a716-446655440000"
}'
import requests

headers = {
    "Authorization": "Bearer <YOUR_API_KEY>",
    "Content-Type": "application/json",
}
data = {
  "to_e164": "+233551234567",
  "template_id": 1,
  "template_variables": {
    "customer_first_name": "Jane",
    "merchant_name": "Arova"
  },
  "idempotency_key": "550e8400-e29b-41d4-a716-446655440000"
}
response = requests.post(
    "https://arovadigital.com/api/v1/messages/",
    headers=headers,
    json=data,
)
print(response.json())
fetch("https://arovadigital.com/api/v1/messages/", {
  method: "POST",
  headers: {
    "Authorization": "Bearer <YOUR_API_KEY>",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "to_e164": "+233551234567",
  "template_id": 1,
  "template_variables": {
    "customer_first_name": "Jane",
    "merchant_name": "Arova"
  },
  "idempotency_key": "550e8400-e29b-41d4-a716-446655440000"
}),
})
  .then(res => res.json())
  .then(data => console.log(data));

Example Response

{
  "id": "d1e2f3a4-...",
  "event_type": "API_MESSAGE",
  "to_e164": "+233551234567",
  "status": "SENT",
  "provider": "hubtel",
  "sender_id": "",
  "estimated_segments": 1,
  "created_at": "2026-01-15T10:30:00Z",
  "sent_at": "2026-01-15T10:30:01Z"
}

List Messages

Retrieve all SMS messages sent through the API. Results are paginated and ordered by creation date (newest first).

GET
/api/v1/messages/

Request Fields

Field Type Required Description
id UUID Read only None
event_type Char Read only None
to_e164 Char Read only None
status Choice Read only None
provider Char Read only None
sender_id Char Read only None
estimated_segments Integer Read only None
error_code Char Read only None
error_message Char Read only None
created_at DateTime Read only None
sent_at DateTime Read only None
delivered_at DateTime Read only None

Example Response

[
  {
    "id": "d1e2f3a4-...",
    "event_type": "API_MESSAGE",
    "to_e164": "+233551234567",
    "status": "DELIVERED",
    "provider": "hubtel",
    "sender_id": "LoveDesigns",
    "estimated_segments": 1,
    "created_at": "2026-01-15T10:30:00Z",
    "sent_at": "2026-01-15T10:30:01Z",
    "delivered_at": "2026-01-15T10:30:15Z"
  }
]

Get Message

Retrieve a single SMS dispatch log by its ID. Use this to check the delivery status of a previously sent message.

GET
/api/v1/messages/{id}/

Request Fields

Field Type Required Description
id UUID Read only None
event_type Char Read only None
to_e164 Char Read only None
status Choice Read only None
provider Char Read only None
sender_id Char Read only None
estimated_segments Integer Read only None
error_code Char Read only None
error_message Char Read only None
created_at DateTime Read only None
sent_at DateTime Read only None
delivered_at DateTime Read only None

Example Response

{
  "id": "d1e2f3a4-...",
  "event_type": "API_MESSAGE",
  "to_e164": "+233551234567",
  "status": "DELIVERED",
  "provider": "hubtel",
  "sender_id": "LoveDesigns",
  "estimated_segments": 1,
  "error_code": null,
  "error_message": null,
  "created_at": "2026-01-15T10:30:00Z",
  "sent_at": "2026-01-15T10:30:01Z",
  "delivered_at": "2026-01-15T10:30:15Z"
}

Templates (SMS)

Create, read, update, and delete reusable SMS templates. Templates support variable substitution using <%variable_key%> syntax. Use them with the messages endpoint to send personalized SMS without building the message body every time.

GET POST PUT PATCH DELETE
/api/v1/templates/

Request Fields

Field Type Required Description
id Integer Read only None
name Char Required None
description Char Optional None
body Char Required None
required_variable_keys JSON Optional None
is_active Boolean Optional None
created_at DateTime Read only None
updated_at DateTime Read only None

Example Request

curl -X GET "https://arovadigital.com/api/v1/templates/" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Welcome SMS",
  "description": "",
  "body": "Hi <%customer_first_name%>, welcome to <%merchant_name%>!",
  "required_variable_keys": [
    "customer_first_name"
  ],
  "is_active": true
}'
import requests

headers = {
    "Authorization": "Bearer <YOUR_API_KEY>",
    "Content-Type": "application/json",
}
data = {
  "name": "Welcome SMS",
  "description": "",
  "body": "Hi <%customer_first_name%>, welcome to <%merchant_name%>!",
  "required_variable_keys": [
    "customer_first_name"
  ],
  "is_active": true
}
response = requests.get(
    "https://arovadigital.com/api/v1/templates/",
    headers=headers,
    json=data,
)
print(response.json())
fetch("https://arovadigital.com/api/v1/templates/", {
  method: "GET",
  headers: {
    "Authorization": "Bearer <YOUR_API_KEY>",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "name": "Welcome SMS",
  "description": "",
  "body": "Hi <%customer_first_name%>, welcome to <%merchant_name%>!",
  "required_variable_keys": [
    "customer_first_name"
  ],
  "is_active": true
}),
})
  .then(res => res.json())
  .then(data => console.log(data));

Example Response

{
  "id": "t1e2m3p4-...",
  "name": "Welcome SMS",
  "description": "",
  "body": "Hi <%customer_first_name%>, welcome to <%merchant_name%>!",
  "required_variable_keys": [
    "customer_first_name"
  ],
  "is_active": true,
  "created_at": "2026-01-15T10:30:00Z",
  "updated_at": "2026-01-15T10:30:00Z"
}

Template Variables

List all available template variables for your merchant. Each variable includes its key, token format, group label, and kind. Use these variables in your template bodies with <%key%> syntax.

GET
/api/v1/templates/variables/

Example Response

[
  {
    "key": "customer_first_name",
    "token": "<%customer_first_name%>",
    "group": "Customer",
    "kind": "standard"
  },
  {
    "key": "points_balance",
    "token": "<%points_balance%>",
    "group": "Rewards",
    "kind": "standard"
  },
  {
    "key": "custom_favorite_store",
    "token": "<%custom_favorite_store%>",
    "group": "Custom Fields",
    "kind": "custom_field"
  }
]
Arova

Loyalty, CRM, approvals, and consumer wallets for growing merchants.

© 2026 Arova Digital. All rights reserved.

Product

Features Pricing Developer Docs Blog

Company

Privacy Policy Terms of Service Merchant Agreement

Contact

9th Floor Emporium
Movenpick Ambassador Hotel
Accra, Ghana

+233 55 186 2403 | +233 55 950 7927

hello@arovadigital.com

Arova Icon

Install Arova App

Add Arova to your home screen for quick loyalty access!