External API reference
YallaCoins Merchant API
Place wholesale recharges programmatically. This reference covers every endpoint available to API-key integrations.
Base URL
https://merchant.yallacoins.team
Auth
Authorization: Api-Key
Format
JSON (UTF-8)
Available endpoints
-
GET
/api/v1/products/List allocated products -
POST
/api/v1/products/{id}/validate-target/Validate a target account -
GET
/api/v1/wallet/Wallet balances -
POST
/api/v1/orders/Create an order -
GET
/api/v1/orders/List orders -
GET
/api/v1/orders/{order_id}/Get order status
Authentication
Every request requires an API key in the Authorization header. Create keys in your dashboard under API Keys. Your merchant account must be operational (verified and not suspended).
| Header | Required | Description |
|---|---|---|
| Authorization | required | Api-Key YOUR_API_KEY |
| Content-Type | required | application/json for POST bodies |
curl "https://merchant.yallacoins.team/api/v1/products/" \
-H "Authorization: Api-Key YOUR_API_KEY" \
-H "Content-Type: application/json"
Key security
API conventions
- All paths require a trailing slash.
- Request and response bodies are JSON.
- Monetary values are USD decimal strings — do not parse them as floating-point.
- Timestamps are UTC ISO 8601, e.g. 2026-07-18T12:30:00Z.
- List endpoints paginate with up to 20 results per page by default.
Error envelope
{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable message.",
"details": {}
}
}
Handle errors by code, not by message text. details carries endpoint-specific information when applicable. For validation errors, it may identify affected fields but can be empty when no field-level information is available.
Paginated list shape
{
"count": 1,
"next": null,
"previous": null,
"results": []
}
/api/v1/products/
List available products
Returns the products available to your merchant account, including the current price, permitted quantity range, and remaining approved budget for each product.
curl "https://merchant.yallacoins.team/api/v1/products/" \
-H "Authorization: Api-Key YOUR_API_KEY"
import requests
resp = requests.get(
"https://merchant.yallacoins.team/api/v1/products/",
headers={"Authorization": "Api-Key YOUR_API_KEY"},
)
products = resp.json()
const res = await fetch("https://merchant.yallacoins.team/api/v1/products/", {
headers: { Authorization: "Api-Key YOUR_API_KEY" },
});
const products = await res.json();
Response — 200 OK
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"name": "Bigo",
"product_type": "quantity",
"unit_price": "0.01285714",
"currency": "USD",
"min_quantity": 1,
"max_quantity": 1000000
}
]
}
| Field | Type | Description |
|---|---|---|
| id | integer | Product ID. Use as product_id when validating a target or creating an order. |
| name | string | Product display name. |
| product_type | string | Product category. |
| unit_price | string | Price for one unit, in USD. |
| currency | string | Currency code. Currently USD. |
| min_quantity | integer | Minimum quantity allowed in one order. |
| max_quantity | integer | Maximum quantity allowed in one order. |
/api/v1/products/{product_id}/validate-target/
Validate a target account
Advisory check that a target account exists on the product's platform. Does not create an order or reserve funds.
| Field | Type | Required | Description |
|---|---|---|---|
| target_account | string | required | Recipient account ID on the platform |
curl -X POST "https://merchant.yallacoins.team/api/v1/products/1/validate-target/" \
-H "Authorization: Api-Key YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"target_account":"108594930"}'
resp = requests.post(
"https://merchant.yallacoins.team/api/v1/products/1/validate-target/",
headers={
"Authorization": "Api-Key YOUR_API_KEY",
"Content-Type": "application/json",
},
json={"target_account": "108594930"},
)
await fetch("https://merchant.yallacoins.team/api/v1/products/1/validate-target/", {
method: "POST",
headers: {
Authorization: "Api-Key YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ target_account: "108594930" }),
});
Valid — 200 OK
{
"valid": true,
"display_name": "Example User"
}
Invalid — 200 OK
{
"valid": false,
"reason": "INVALID_TARGET"
}
| Field | Type | Description |
|---|---|---|
| valid | boolean | Whether the target passed the validation check. |
| display_name | string | Display name returned for a valid target. May be empty when the platform does not provide one. |
| reason | string | Reason code for an invalid target. Currently always INVALID_TARGET. |
| HTTP | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR | target_account is missing or malformed, or the product is not allocated to your account. |
| 404 | RESOURCE_NOT_FOUND | Unknown product_id, or the product is not eligible for merchant orders. |
| 503 | VALIDATION_UNAVAILABLE | The validation service could not return a result. |
/api/v1/wallet/
Retrieve wallet balance
Returns your prepaid wallet balances in USD.
curl "https://merchant.yallacoins.team/api/v1/wallet/" \
-H "Authorization: Api-Key YOUR_API_KEY"
Response — 200 OK
{
"currency": "USD",
"balance": "1000.00",
"reserved_balance": "17.70",
"available_balance": "982.30"
}
| Field | Type | Description |
|---|---|---|
| currency | string | Currency code. Currently USD. |
| balance | string | Total wallet balance, in USD. |
| reserved_balance | string | Wallet funds currently held for in-flight orders, in USD. |
| available_balance | string | Wallet funds currently available for new orders, in USD. |
/api/v1/orders/
Create an order
Creates an order for your merchant account. Generate and store a unique client_order_id for each business operation before sending the request.
| Field | Type | Required | Description |
|---|---|---|---|
| product_id | integer | required | Product ID from GET /products/ |
| target_account | string | required | Recipient account ID on the product's platform. Maximum length: 100 characters. |
| quantity | integer | required | Amount to send; must be within product min/max |
| client_order_id | string | required | The string form of the primary key or another unique reference from your own stored business operation. It must be unique within your merchant account. Maximum length: 100 characters. |
curl -X POST "https://merchant.yallacoins.team/api/v1/orders/" \
-H "Authorization: Api-Key YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"product_id": 1,
"target_account": "108594930",
"quantity": 1000,
"client_order_id": "ORDER-2026-0042"
}'
resp = requests.post(
"https://merchant.yallacoins.team/api/v1/orders/",
headers={
"Authorization": "Api-Key YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"product_id": 1,
"target_account": "108594930",
"quantity": 1000,
"client_order_id": "ORDER-2026-0042",
},
timeout=30,
)
order = resp.json()
const res = await fetch("https://merchant.yallacoins.team/api/v1/orders/", {
method: "POST",
headers: {
Authorization: "Api-Key YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
product_id: 1,
target_account: "108594930",
quantity: 1000,
client_order_id: "ORDER-2026-0042",
}),
});
const order = await res.json();
Response — 201 Created (or 200 OK on identical retry)
{
"order_id": "MER-260718-A1B2C3D4E5F6",
"client_order_id": "ORDER-2026-0042",
"status": "pending",
"product_id": 1,
"product_name": "Bigo",
"target_account": "108594930",
"username": null,
"quantity": 1000,
"unit_price": "0.01770000",
"total_price": "17.70",
"created_at": "2026-07-18T12:30:00Z",
"completed_at": null,
"failure": null
}
For an identical retry, 200 OK returns the existing order in its current status.
| HTTP | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR | Missing or invalid fields, product not available, quantity outside the permitted range, or insufficient wallet balance. |
| 404 | RESOURCE_NOT_FOUND | Unknown product_id. |
| 409 | CLIENT_ORDER_ID_REUSED | client_order_id reused with different order details — see Client order IDs below. |
| 503 | TEMPORARILY_UNAVAILABLE | Order creation is temporarily unavailable. No order was created and no funds were reserved. Wait for Retry-After, then repeat the identical request. |
/api/v1/orders/
List orders
Returns your orders, newest first. Supports status, product, date range, and client_order_id filters.
| Parameter | Type | Description |
|---|---|---|
| status | string | pending | processing | completed | failed |
| product_id | integer | Exact product ID |
| created_from | date | YYYY-MM-DD (UTC, inclusive start) |
| created_to | date | YYYY-MM-DD (UTC, inclusive end) |
| client_order_id | string | Exact match, case-insensitive |
| page | integer | Page number (20 per page) |
curl "https://merchant.yallacoins.team/api/v1/orders/?status=processing&created_from=2026-07-01&created_to=2026-07-31&page=1" \
-H "Authorization: Api-Key YOUR_API_KEY"
Response — 200 OK
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"order_id": "MER-260718-A1B2C3D4E5F6",
"client_order_id": "ORDER-2026-0042",
"status": "completed",
"product_id": 1,
"product_name": "Bigo",
"target_account": "108594930",
"username": "Ahmad Alkhteb",
"quantity": 1000,
"unit_price": "0.01285714",
"total_price": "12.86",
"created_at": "2026-07-18T12:30:00Z",
"completed_at": "2026-07-18T12:31:10Z",
"failure": null
}
]
}
/api/v1/orders/{order_id}/
Retrieve an order
Fetch a single order by its system order_id (for example MER-260718-A1B2C3D4E5F6).
curl "https://merchant.yallacoins.team/api/v1/orders/MER-260718-A1B2C3D4E5F6/" \
-H "Authorization: Api-Key YOUR_API_KEY"
Returns the same order representation as order creation. When status is failed, failure contains a code and message:
"failure": {
"code": "INVALID_TARGET",
"message": "The target account does not exist on this platform."
}
Failure codes: INVALID_TARGET, TEMPORARILY_UNAVAILABLE, ORDER_FAILED
Order fields
| Field | Type | Description |
|---|---|---|
| order_id | string | System-generated identifier for the order. Use it to retrieve the order. |
| client_order_id | string | Your reference for this business operation. It is unique within your merchant account and can be used to find the order. |
| status | string | Current processing status of the order. |
| product_id | integer | Product ID used for the order. |
| product_name | string | Current display name of the product associated with this order. |
| target_account | string | Recipient account ID on the product's platform. |
| username | string or null | Supplier-derived recipient identity returned by the completed charge response. Output-only; null when unavailable. |
| quantity | integer | Quantity ordered. |
| unit_price | string | Price per unit, in USD. |
| total_price | string | Total price of the order, in USD. |
| created_at | string | Order creation time in UTC ISO 8601 format. |
| completed_at | string or null | Time the order completed successfully, in UTC ISO 8601 format. null until the order is completed. |
| failure | object or null | Failure information when status is failed; otherwise null. Contains a machine-readable code and a human-readable message. code is one of INVALID_TARGET, TEMPORARILY_UNAVAILABLE, or ORDER_FAILED. |
Client order IDs and retries
Before sending the create request, create and store your own order or operation record. Use that record's primary key or another unique reference, such as an invoice number or UUID, as client_order_id. It identifies this business operation in your system and must be unique within your merchant account. We enforce that uniqueness for your account, preventing duplicate orders when you repeat a create request after an uncertain result. You can also use it to look up the order and its current status. Matching for client_order_id and target_account is case-insensitive. The spelling from the first accepted request is preserved in the returned order.
- ●New unique client_order_id → 201 Created with a new order.
- ●Same client_order_id with the same product_id, quantity, and target_account → 200 OK with the existing order.
- ●Same client_order_id with a changed product_id, quantity, or target_account → 409 CLIENT_ORDER_ID_REUSED.
{
"error": {
"code": "CLIENT_ORDER_ID_REUSED",
"message": "This client_order_id has already been used for a different order.",
"details": {
"order_id": "MER-260718-A1B2C3D4E5F6",
"status": "completed"
}
}
}
Use details.order_id to retrieve the original order. details.status is its current status.
After a network timeout
- Do not invent a new client_order_id for the same business operation.
- Repeat the identical create request with the same client_order_id.
- Or look up the order with GET /api/v1/orders/?client_order_id=YOUR_REFERENCE.
Replaying a request for an order that is already failed returns that existing failed order; it does not submit the order again.
Order lifecycle
Orders are processed asynchronously. Signed webhooks (see below) notify you when an order reaches completed or failed — no polling is required in the normal flow. GET /api/v1/orders/{order_id}/ remains available for reconciliation, manual checks, or as a fallback.
pending
Accepted and awaiting execution
processing
Being processed
completed
Completed successfully
failed
Could not be completed
Polling example
import time
import requests
TERMINAL = {"completed", "failed"}
def wait_for_order(order_id, max_wait=120):
for _ in range(max_wait // 2):
order = requests.get(
f"https://merchant.yallacoins.team/api/v1/orders/{order_id}/",
headers={"Authorization": "Api-Key YOUR_API_KEY"},
).json()
if order["status"] in TERMINAL:
return order
time.sleep(2)
return order
Error codes
All errors use the envelope above. Handle by code, not by message text.
| HTTP | Code | Meaning |
|---|---|---|
| 400 | MALFORMED_JSON | Body is not valid JSON |
| 400 | VALIDATION_ERROR | Missing or invalid fields |
| 401 | INVALID_API_KEY | Missing, invalid, or expired key |
| 403 | FORBIDDEN | Not permitted |
| 403 | MERCHANT_NOT_OPERATIONAL | Account not operational |
| 404 | RESOURCE_NOT_FOUND | Unknown resource for this account |
| 405 | METHOD_NOT_ALLOWED | Wrong HTTP method |
| 406 | NOT_ACCEPTABLE | Request JSON responses |
| 409 | CLIENT_ORDER_ID_REUSED | client_order_id reused with different order details — see details.order_id |
| 415 | UNSUPPORTED_MEDIA_TYPE | Content-Type must be application/json |
| 503 | TEMPORARILY_UNAVAILABLE | Order creation is temporarily unavailable. Wait for Retry-After, then repeat the identical request. |
| 503 | VALIDATION_UNAVAILABLE | Target validation is inconclusive. |
| 500 | INTERNAL_ERROR | Unexpected server error. Retry later; for a create request, reuse the same client_order_id. |
Best practices
Recommended integration flow
- Fetch products — GET /api/v1/products/ to get available product IDs, pricing, and quantity ranges.
- Validate the target — POST /api/v1/products/{product_id}/validate-target/ before placing an order. Validation can reduce failures caused by invalid or nonexistent targets, but it does not guarantee that the later order will succeed.
- Create your order record and set client_order_id — store the business operation in your database first, then use its primary key or another unique reference (invoice number or UUID) as client_order_id.
- Create the order — POST /api/v1/orders/ with the validated product_id, target_account, quantity, and your client_order_id.
- Verify the webhook — receive the signed order.completed or order.failed webhook and apply the full terminal state. Use GET /api/v1/orders/{order_id}/ only if you need to reconcile or check manually.
- 1Retry with the same client_order_id — if a create request ends with a timeout, repeat the identical request. Do not generate a new reference for the same business operation.
- 2Validate targets before ordering — validation can reduce failures caused by invalid or nonexistent recipients, but it does not guarantee that the order will succeed.
- 3Rely on webhooks, poll only as fallback — GET /api/v1/orders/{order_id}/ is for reconciliation and manual checks, not the primary update path.
- 4Store order_id — from every successful create for reconciliation and support.
- 5Keep money as strings — unit_price and total_price are decimal strings in USD.
- 6Protect your API keys — server-side only; rotate if leaked.
Webhooks
Signed webhooks are the primary terminal-status notification. When an order reaches completed or failed, YallaCoins.Teams sends a signed HTTPS POST to your configured endpoint. The payload contains the full order state — no follow-up GET is required in the normal flow.
GET /api/v1/orders/{order_id}/ remains available for reconciliation, manual checks, and recovery when your endpoint failed to process a webhook.
Delivery semantics
- Delivery is at least once — duplicates are possible. Use event_id (from the signed JSON body) as your idempotency key.
- Up to 7 delivery attempts over approximately 4 hours. The first send is immediate; later retries are scheduled automatically.
- Retryable: connection/DNS errors, timeouts, HTTP 408, 429, and 5xx.
- Permanent (no retry): redirects (3xx), all other 4xx responses (including 401 and 403), invalid configuration, disabled webhook.
- Return any HTTP 2xx status to acknowledge receipt. The response body is ignored.
- Redirects are not followed — respond 2xx directly from the configured URL.
- Rotating the signing secret immediately invalidates the previous secret.
- Disabling webhooks prevents new webhook deliveries. An existing delivery attempted while webhooks are disabled is stopped. Failed events are not replayed automatically.
- No manual replay exists in the MVP. Use the Orders API for reconciliation.
Webhook setup
- Deploy an HTTPS endpoint — on the default port (443). Private/loopback IP addresses and localhost are rejected.
- Generate a signing secret — in the Webhooks settings page. The secret is shown once — store it securely outside source control.
- Enable webhooks — after saving your URL and generating a secret, check the enable box and save.
Webhook events
Required headers
HTTP header names are case-insensitive.
| Header | Description |
|---|---|
X-Webhook-Signature | HMAC-SHA256 of the raw body, keyed with your complete signing secret: sha256= |
Content-Type | application/json; charset=utf-8 |
Payload structure
{
"event_id": "evt_a1b2c3d4...",
"event_type": "order.completed",
"occurred_at": "2026-07-27T12:30:00Z",
"data": { ... full order object ... }
}
order.completed example
{
"event_id": "evt_2d5a3f...",
"event_type": "order.completed",
"occurred_at": "2026-07-27T12:30:00Z",
"data": {
"order_id": "MER-260727-A1B2C3D4E5F6",
"client_order_id": "ORDER-2026-0042",
"status": "completed",
"product_id": 1,
"product_name": "Bigo",
"target_account": "108594930",
"username": "Ahmad Alkhteb",
"quantity": 1000,
"unit_price": "0.01770000",
"total_price": "17.70",
"created_at": "2026-07-27T12:29:50Z",
"completed_at": "2026-07-27T12:30:00Z",
"failure": null
}
}
order.failed example
{
"event_id": "evt_f6e5d4...",
"event_type": "order.failed",
"occurred_at": "2026-07-27T12:31:00Z",
"data": {
"order_id": "MER-260727-F6E5D4C3B2A1",
"client_order_id": "ORDER-2026-0043",
"status": "failed",
"product_id": 1,
"product_name": "Bigo",
"target_account": "108594930",
"username": null,
"quantity": 1000,
"unit_price": "0.01770000",
"total_price": "17.70",
"created_at": "2026-07-27T12:30:50Z",
"completed_at": null,
"failure": {
"code": "INVALID_TARGET",
"message": "The target account does not exist on this platform."
}
}
}
failure.code is one of INVALID_TARGET, TEMPORARILY_UNAVAILABLE, or ORDER_FAILED — see Errors below.
Verifying webhooks
- Read the exact raw request body bytes — before any JSON parsing.
- Read the X-Webhook-Signature header.
- Require the value to start with sha256=.
- Compute HMAC-SHA256 over the raw body using your complete signing secret as the key.
- Encode the digest as lowercase hexadecimal.
- Prefix it with sha256= and compare using a constant-time comparison.
- Parse JSON only after verification succeeds.
- Deduplicate using event_id from the signed JSON body.
- Apply the event and return any 2xx status quickly.
Signing key
import hashlib
import hmac
import json
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
SIGNATURE_HEADER = "X-Webhook-Signature"
WEBHOOK_SECRET = "whsec_..." # load from settings/env — never hardcode
def verify_webhook(secret: str, raw_body: bytes, signature: str) -> bool:
if not signature or not signature.startswith("sha256="):
return False
expected = "sha256=" + hmac.new(
secret.encode("utf-8"),
raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
@csrf_exempt
def handle_webhook(request):
raw_body = request.body
signature = request.headers.get(SIGNATURE_HEADER, "")
if not verify_webhook(WEBHOOK_SECRET, raw_body, signature):
return HttpResponse(status=401)
event = json.loads(raw_body)
event_id = event["event_id"]
event_type = event["event_type"]
# Deduplicate event_id before applying the business state.
# Process order.completed or order.failed.
return HttpResponse(status=204)
csrf_exempt is required — Django's CSRF protection otherwise rejects this external POST before verification runs.
import crypto from "node:crypto";
import express from "express";
const app = express();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
function verifyWebhook(secret, rawBody, signature) {
if (typeof signature !== "string" || !signature.startsWith("sha256=")) {
return false;
}
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const expectedBuffer = Buffer.from(expected, "utf8");
const signatureBuffer = Buffer.from(signature, "utf8");
if (expectedBuffer.length !== signatureBuffer.length) {
return false;
}
return crypto.timingSafeEqual(expectedBuffer, signatureBuffer);
}
app.post(
"/webhooks/yallacoins",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.get("X-Webhook-Signature") || "";
if (!verifyWebhook(WEBHOOK_SECRET, req.body, signature)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
// Deduplicate event.event_id.
// Process event.event_type.
return res.sendStatus(204);
},
);
Mount express.raw() on this route before any JSON body parser — verification needs the untouched raw bytes.
function verifyWebhook(
string $secret,
string $rawBody,
?string $signature
): bool {
if ($signature === null || !str_starts_with($signature, 'sha256=')) {
return false;
}
$expected = 'sha256=' . hash_hmac(
'sha256',
$rawBody,
$secret
);
return hash_equals($expected, $signature);
}
// Register this route in routes/api.php (stateless, no CSRF middleware)
// rather than routes/web.php — the web group's CSRF check otherwise
// rejects this external POST before verification runs.
$rawBody = $request->getContent();
$signature = $request->header('X-Webhook-Signature');
if (!verifyWebhook(
config('services.yallacoins.webhook_secret'),
$rawBody,
$signature,
)) {
return response('', 401);
}
$event = json_decode($rawBody, true, flags: JSON_THROW_ON_ERROR);
// Deduplicate $event['event_id'].
// Process $event['event_type'].
return response('', 204);
Do not Base64-decode the secret or the signature — both are used as plain strings.
Delivery is at-least-once
- The same event may be delivered more than once. Use event_id as your idempotency key.
- Store processed event IDs with a uniqueness constraint.
- When an already-processed event_id arrives again, return 2xx without applying the business change a second time.
- This is an explicit MVP trade-off: the signature has no timestamp, so there is no replay-window protection. Deduplication is the only defense against a replayed valid request.