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"

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

json
{
  "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": []
}
GET /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
idintegerProduct ID. Use as product_id when validating a target or creating an order.
namestringProduct display name.
product_typestringProduct category.
unit_pricestringPrice for one unit, in USD.
currencystringCurrency code. Currently USD.
min_quantityintegerMinimum quantity allowed in one order.
max_quantityintegerMaximum quantity allowed in one order.
POST /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
validbooleanWhether the target passed the validation check.
display_namestringDisplay name returned for a valid target. May be empty when the platform does not provide one.
reasonstringReason code for an invalid target. Currently always INVALID_TARGET.
HTTP Code Cause
400VALIDATION_ERRORtarget_account is missing or malformed, or the product is not allocated to your account.
404RESOURCE_NOT_FOUNDUnknown product_id, or the product is not eligible for merchant orders.
503VALIDATION_UNAVAILABLEThe validation service could not return a result.
Advisory only. A valid result does not guarantee the later order will complete. On 503 VALIDATION_UNAVAILABLE, treat the result as inconclusive — do not assume the target is invalid.
GET /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
currencystringCurrency code. Currently USD.
balancestringTotal wallet balance, in USD.
reserved_balancestringWallet funds currently held for in-flight orders, in USD.
available_balancestringWallet funds currently available for new orders, in USD.
POST /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
400VALIDATION_ERRORMissing or invalid fields, product not available, quantity outside the permitted range, or insufficient wallet balance.
404RESOURCE_NOT_FOUNDUnknown product_id.
409CLIENT_ORDER_ID_REUSEDclient_order_id reused with different order details — see Client order IDs below.
503TEMPORARILY_UNAVAILABLEOrder creation is temporarily unavailable. No order was created and no funds were reserved. Wait for Retry-After, then repeat the identical request.
GET /api/v1/orders/

List orders

Returns your orders, newest first. Supports status, product, date range, and client_order_id filters.

Parameter Type Description
statusstringpending | processing | completed | failed
product_idintegerExact product ID
created_fromdateYYYY-MM-DD (UTC, inclusive start)
created_todateYYYY-MM-DD (UTC, inclusive end)
client_order_idstringExact match, case-insensitive
pageintegerPage 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
    }
  ]
}
GET /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_idstringSystem-generated identifier for the order. Use it to retrieve the order.
client_order_idstringYour reference for this business operation. It is unique within your merchant account and can be used to find the order.
statusstringCurrent processing status of the order.
product_idintegerProduct ID used for the order.
product_namestringCurrent display name of the product associated with this order.
target_accountstringRecipient account ID on the product's platform.
usernamestring or nullSupplier-derived recipient identity returned by the completed charge response. Output-only; null when unavailable.
quantityintegerQuantity ordered.
unit_pricestringPrice per unit, in USD.
total_pricestringTotal price of the order, in USD.
created_atstringOrder creation time in UTC ISO 8601 format.
completed_atstring or nullTime the order completed successfully, in UTC ISO 8601 format. null until the order is completed.
failureobject or nullFailure 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

  1. Do not invent a new client_order_id for the same business operation.
  2. Repeat the identical create request with the same client_order_id.
  3. 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
Use this polling pattern only for reconciliation or manual checks — webhooks are the primary notification mechanism.

Error codes

All errors use the envelope above. Handle by code, not by message text.

HTTP Code Meaning
400MALFORMED_JSONBody is not valid JSON
400VALIDATION_ERRORMissing or invalid fields
401INVALID_API_KEYMissing, invalid, or expired key
403FORBIDDENNot permitted
403MERCHANT_NOT_OPERATIONALAccount not operational
404RESOURCE_NOT_FOUNDUnknown resource for this account
405METHOD_NOT_ALLOWEDWrong HTTP method
406NOT_ACCEPTABLERequest JSON responses
409CLIENT_ORDER_ID_REUSEDclient_order_id reused with different order details — see details.order_id
415UNSUPPORTED_MEDIA_TYPEContent-Type must be application/json
503TEMPORARILY_UNAVAILABLEOrder creation is temporarily unavailable. Wait for Retry-After, then repeat the identical request.
503VALIDATION_UNAVAILABLETarget validation is inconclusive.
500INTERNAL_ERRORUnexpected server error. Retry later; for a create request, reuse the same client_order_id.
A 503 TEMPORARILY_UNAVAILABLE response includes a Retry-After header (5 seconds). No order was created and no funds were reserved. Wait, then repeat the identical create request with the same client_order_id.

Best practices

Recommended integration flow

  1. Fetch products — GET /api/v1/products/ to get available product IDs, pricing, and quantity ranges.
  2. 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.
  3. 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.
  4. Create the order — POST /api/v1/orders/ with the validated product_id, target_account, quantity, and your client_order_id.
  5. 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.
  1. 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.
  2. 2Validate targets before ordering — validation can reduce failures caused by invalid or nonexistent recipients, but it does not guarantee that the order will succeed.
  3. 3Rely on webhooks, poll only as fallback — GET /api/v1/orders/{order_id}/ is for reconciliation and manual checks, not the primary update path.
  4. 4Store order_id — from every successful create for reconciliation and support.
  5. 5Keep money as strings — unit_price and total_price are decimal strings in USD.
  6. 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.

Webhook setup

  1. Deploy an HTTPS endpoint — on the default port (443). Private/loopback IP addresses and localhost are rejected.
  2. Generate a signing secret — in the Webhooks settings page. The secret is shown once — store it securely outside source control.
  3. 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-SignatureHMAC-SHA256 of the raw body, keyed with your complete signing secret: sha256=.
Content-Typeapplication/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

  1. Read the exact raw request body bytes — before any JSON parsing.
  2. Read the X-Webhook-Signature header.
  3. Require the value to start with sha256=.
  4. Compute HMAC-SHA256 over the raw body using your complete signing secret as the key.
  5. Encode the digest as lowercase hexadecimal.
  6. Prefix it with sha256= and compare using a constant-time comparison.
  7. Parse JSON only after verification succeeds.
  8. Deduplicate using event_id from the signed JSON body.
  9. Apply the event and return any 2xx status quickly.
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.