Zentra
Version 1.0.0

Reseller API

Sell what Zentra Digital Shop sells, from your own bot or site.

Orders are paid from your Zentra wallet at shop prices and delivered instantly — the same way a purchase in the Telegram bot is. You resell at whatever price you choose.

Base URL
https://api.zentradigital.shop

Quickstart

  1. Open @ZentraShopBot in Telegram.
  2. Top up your wallet — Telebirr, Bank of Abyssinia, USDT or Binance Pay.
  3. Tap API Link → Create API key. It is shown once.
  4. Send it on every request as a bearer token.
# 1. Check your key and balance
curl https://api.zentradigital.shop/v1/me \
  -H "Authorization: Bearer zen_live_YOUR_KEY"

# 2. See what is for sale (no key needed)
curl https://api.zentradigital.shop/v1/products

# 3. Buy something
curl -X POST https://api.zentradigital.shop/v1/orders \
  -H "Authorization: Bearer zen_live_YOUR_KEY" \
  -H "X-Idempotency-Key: my-first-order" \
  -H "Content-Type: application/json" \
  -d '{"product_id": 1, "quantity": 1}'

Authentication

Your key is shown exactly once, when you create it. Only a fingerprint is stored, so it can never be recovered — only replaced. Regenerating immediately kills the previous key.

Every authenticated request carries the key as a bearer token. Anyone holding it can spend your wallet, so keep it server-side — never in a browser, a mobile app, or a public repository.

-H "Authorization: Bearer zen_live_YOUR_KEY"

Money

Every amount is a string, in USDT. Parse it with a decimal type — BigDecimal, decimal.Decimal, or a decimal library. Reading "0.90" as a float and summing it is how two systems come to disagree about what was charged.

JSON's only numeric type is a binary float, which cannot represent most decimal fractions exactly. Amounts are therefore sent as strings so the digits you read are the digits we charged.

Retrying safely

Always send X-Idempotency-Key on POST /v1/orders. Without it, a retry after a timeout is a second order and a second charge.

Pick a value unique to the order you intend to place — a UUID, or your own order id. Retrying with the same value returns the original order with HTTP 200 and does not charge you again. If the first request is still in flight, you get 409 in_progress — wait and ask again.

A key whose order never happened is released, so you can retry with it once the problem is fixed.

Errors

Every failure has the same shape. Branch on error.code, never on the sentence — the wording may change, the code will not.

{
  "error": {
    "code": "insufficient_stock",
    "message": "Only 3 code(s) of Gemini AI Pro 18m are left."
  }
}
unauthorized401No key, a malformed key, or one that is no longer live.
account_suspended403The account behind this key is suspended.
api_disabled503The reseller API is closed right now.
invalid_request400Malformed body, or a quantity outside 1–1000.
order_failed402Your wallet cannot cover it, or the supplier refused and you were refunded.
out_of_stock409Nothing left of that product.
insufficient_stock409Fewer units left than you asked for.
not_available409The supplier has withdrawn it for now.
in_progress409An identical idempotency key is still being placed.
not_found404No such product or order — or the order is not yours.
not_delivered409That order has nothing to export yet.
rate_limited429Too many requests this minute.
daily_cap_reached429This key's daily spend cap is used up.
unresolved202The supplier call died in transit. You were debited and a person is reconciling it — do not retry.
unresolved is the one status that is neither success nor failure: your wallet was debited but the supplier's answer never arrived. Do not retry — check GET /v1/orders, and open a ticket in the bot if it does not settle.

Limits

Requests60 a minute, per key

Exceeding it returns 429 rate_limited. Back off and retry.

Daily spend500 USDT a day, per key

Returns 429 daily_cap_reached. Ask support if you need it raised.

Quantity1 to 1000 per order

Stock is checked before anything is charged.

Endpoints

GET/v1/me

Your account and balance

Confirms the key works and reports the wallet it spends from. Poll this before a large order rather than discovering a shortfall mid-batch.

Request
curl \
  -H "Authorization: Bearer zen_live_YOUR_KEY"  https://api.zentradigital.shop/v1/me
Response · 200
{
  "user_id": 1,
  "username": "",
  "balance": "42.50",
  "currency": "USDT",
  "api_key_prefix": "zen_live_A7f3C2d9"
}
Every response
200Your account.
401No key, a malformed key, or a key that is not live. code: unauthorized
403The account is suspended. code: account_suspended
429Too many requests this minute. code: rate_limited
503The reseller API is closed. code: api_disabled
GET/v1/products

List every product

Public — no key needed, so you can build a catalogue before signing up. Sold-out products ARE included, with available: false, so your own storefront can show them as out of stock rather than making them vanish.

Request
curl https://api.zentradigital.shop/v1/products
Response · 200
{
  "products": [
    {
      "id": 1,
      "name": "Gemini AI Pro 18m",
      "description": "",
      "unit_label": "code",
      "price": "0.90",
      "currency": "USDT",
      "stock": 1253,
      "available": true
    }
  ]
}
Every response
200The catalogue.
GET/v1/products/{product_id}

One product

Parameters
product_idintegerpathrequired

Zentra's product id.

Request
curl https://api.zentradigital.shop/v1/products/1
Response · 200
{
  "id": 1,
  "name": "Gemini AI Pro 18m",
  "description": "",
  "unit_label": "code",
  "price": "0.90",
  "currency": "USDT",
  "stock": 1253,
  "available": true
}
Every response
200The product.
404No such product, or it is not on sale. code: not_found
POST/v1/orders

Place an order

Debits your wallet and fills the order immediately. On success the response already contains the delivered goods — there is nothing to poll for.

Always send X-Idempotency-Key. A network timeout without one leaves you unable to tell whether you were charged.

Parameters
X-Idempotency-Keystringheader

Your own unique id for this intended order. Retrying with the same value returns the original order rather than placing a second.

Body
product_idintegerrequired

From GET /v1/products.

quantityinteger

How many units.

Request
curl -X POST https://api.zentradigital.shop/v1/orders \
  -H "Authorization: Bearer zen_live_YOUR_KEY" \
  -H "X-Idempotency-Key: order-1042" \
  -H "Content-Type: application/json" \
  -d '{"product_id":1,"quantity":1}'
Response · 200
{
  "id": 1421,
  "reference": "ZEN-A7F3C2D9",
  "product_id": 1,
  "product_name": "",
  "quantity": 2,
  "total": "1.80",
  "currency": "USDT",
  "status": "pending",
  "created_at": "2026-08-26T19:48:52Z",
  "completed_at": "2026-08-26T19:48:52Z",
  "delivery": [
    {
      "label": "Redeem link",
      "value": ""
    }
  ]
}
Every response
200This idempotency key was already used — the ORIGINAL order, unchanged. You were not charged again.
201Ordered and delivered.
202The supplier call died in transit. Your wallet WAS debited and a human is reconciling it. Do NOT retry — check GET /v1/orders. code: unresolved
400Malformed body or out-of-range quantity. code: invalid_request
401No key, a malformed key, or a key that is not live. code: unauthorized
402Your wallet cannot cover this, or the supplier refused it and you were refunded. code: order_failed
403The account is suspended. code: account_suspended
409Cannot be filled: out_of_stock, insufficient_stock, not_available, not_active — or in_progress if an identical idempotency key is still being placed.
429Rate limited, or this key's daily spend cap is reached. code: rate_limited | daily_cap_reached
503The reseller API is closed. code: api_disabled
GET/v1/orders

Your recent orders

Newest first. delivery is omitted here — fetch a single order to read the goods.

Parameters
limitintegerquery
Request
curl \
  -H "Authorization: Bearer zen_live_YOUR_KEY"  https://api.zentradigital.shop/v1/orders
Response · 200
{
  "orders": [
    {
      "id": 1421,
      "reference": "ZEN-A7F3C2D9",
      "product_id": 1,
      "product_name": "",
      "quantity": 2,
      "total": "1.80",
      "currency": "USDT",
      "status": "pending",
      "created_at": "2026-08-26T19:48:52Z",
      "completed_at": "2026-08-26T19:48:52Z",
      "delivery": [
        {
          "label": null,
          "value": null
        }
      ]
    }
  ]
}
Every response
200Your orders.
401No key, a malformed key, or a key that is not live. code: unauthorized
403The account is suspended. code: account_suspended
429Too many requests this minute. code: rate_limited
503The reseller API is closed. code: api_disabled
GET/v1/orders/{order_id}

One order, with its delivery

Parameters
order_idintegerpathrequired
Request
curl \
  -H "Authorization: Bearer zen_live_YOUR_KEY"  https://api.zentradigital.shop/v1/orders/13
Response · 200
{
  "id": 1421,
  "reference": "ZEN-A7F3C2D9",
  "product_id": 1,
  "product_name": "",
  "quantity": 2,
  "total": "1.80",
  "currency": "USDT",
  "status": "pending",
  "created_at": "2026-08-26T19:48:52Z",
  "completed_at": "2026-08-26T19:48:52Z",
  "delivery": [
    {
      "label": "Redeem link",
      "value": ""
    }
  ]
}
Every response
200The order.
401No key, a malformed key, or a key that is not live. code: unauthorized
403The account is suspended. code: account_suspended
404No such order, or it is not yours. code: not_found
429Too many requests this minute. code: rate_limited
503The reseller API is closed. code: api_disabled
GET/v1/orders/{order_id}/export

The delivered goods as plain text

One field per line, label: value. Convenient for piping straight into an importer.

Parameters
order_idintegerpathrequired
Request
curl \
  -H "Authorization: Bearer zen_live_YOUR_KEY"  https://api.zentradigital.shop/v1/orders/13/export
Response · 200
Redeem link: https://example.com/redeem/abc
Code: XXXX-YYYY
Every response
200The goods.
401No key, a malformed key, or a key that is not live. code: unauthorized
403The account is suspended. code: account_suspended
404No such order, or it is not yours. code: not_found
409Nothing to export yet. code: not_delivered
429Too many requests this minute. code: rate_limited
503The reseller API is closed. code: api_disabled

Schemas

idintegerrequired

Zentra's product id. Use this in POST /v1/orders — it is NOT the supplier's id.

namestringrequired

Product name.

descriptionstringrequired

The supplier's own description, de-duplicated across their two text fields, or the shop's override where one is set.

unit_labelstringrequired

What one unit is called.

pricestringrequired

Price for ONE unit, in USDT. Decimal as a STRING, never a JSON number. JSON's only numeric type is a binary float, and a reseller reconciling totals against ours must get the same digits we charged.

currencystringrequiredUSDT

Always USDT. Present so a client never has to assume.

stockinteger | nullrequired

Units available, or null when the supplier does not count this product. null is NOT zero.

availablebooleanrequired

Whether this can be ordered right now. False when sold out or withdrawn — the product is still listed so you know it exists.

idintegerrequired

Zentra's order id.

referencestringrequired

Human-readable reference. Quote this to support.

product_idintegerrequired

The product ordered.

product_namestring

Its name at the time of ordering.

quantityintegerrequired

Units ordered.

totalstringrequired

What was charged to your wallet, in USDT. Decimal as a STRING, never a JSON number. JSON's only numeric type is a binary float, and a reseller reconciling totals against ours must get the same digits we charged.

currencystringrequiredUSDT

Always USDT.

statusstringrequiredpending · delivered · refunded · failed

delivered is terminal and successful. refunded means the supplier failed and your wallet was put back. pending means it is still being filled.

created_atstringrequired

RFC 3339 timestamp.

completed_atstring | null

When it finished, or null.

deliveryobject[]

The goods. Present once status is delivered, null before that. Absent entirely from the list endpoint — fetch one order to read it.

Something not behaving?

Open a ticket in the bot with your order reference — ZEN-… — and a person will answer in that chat. Include the error.code if there was one.