Reseller API documentation
Everything needed to place orders from your own shop or bot. If you have not applied yet, start at the reseller page.
“Copy for an LLM” puts a short instruction and your base URL on top of the full reference, so it can be pasted straight into ChatGPT or Claude to get a working client back.
Base URL
https://discordad.com/api/v1/reseller
Every endpoint below is relative to that. HTTPS is required and enforced — a request over plain HTTP is
refused with 403 https_required rather than redirected, because a redirect would already have
put your key on the wire in clear.
All responses are JSON and all of them carry an ok boolean. Nothing ever returns HTML.
Authentication
Create a key from your panel. Send it as a bearer token:
Authorization: Bearer rk_live_a1b2c3d4e5f6a7b8_9f8e...
X-API-Key: rk_live_... works too, if a bearer header is awkward in your stack.
We store only a SHA-256 hash of it, so we genuinely cannot show it to you again or recover it for you — not even by asking support. Lose it and you revoke it and make another, which takes a few seconds.
Session cookies are ignored on these endpoints entirely. Being logged in to the website in the same browser gives you nothing here; the key is the only thing that authenticates a request.
Authentication failures
| Status | Error | Means |
|---|---|---|
| 401 | no_key | No key was sent at all |
| 401 | bad_key | Malformed, unknown, or the wrong secret |
| 401 | key_revoked | That key was revoked from your panel |
| 403 | account_pending | Approved not yet — the key exists but the account is not live |
| 403 | account_suspended | Access paused; your balance is untouched |
| 403 | ip_not_allowed | An IP allow-list is set and you are not on it |
| 403 | https_required | The request was not over TLS |
An unknown key id and a wrong secret give exactly the same answer, deliberately: telling them apart would let somebody discover which key ids exist.
Rate limits
Three ceilings apply, and you will normally never meet any of them:
- 60 requests a minute per IP before authentication — this is what stops key guessing.
- Your account limit after authentication, 120 a minute by default. Ask if you need more; it is a per-account setting.
- 30 order placements a minute, on top of the above.
Exceeding one returns 429 with rate_limited. Standard
RateLimit-* headers come back on every response, so you can back off before being told to.
Errors
Every failure has the same shape:
{
"ok": false,
"error": "insufficient_funds",
"message": "Not enough balance. This order costs $4.50 and your balance is $1.20.",
"required": 4.50,
"balance": 1.20
}
Branch on error, which is stable. message is written for a person and may be
reworded at any time, so do not match on it.
| Status | Error | Means |
|---|---|---|
| 400 | invalid_request | A parameter is missing or wrong; message says which |
| 400 | unknown_service | No such service id — the valid ones are listed in the message |
| 400 | idempotency_key_required | Missing or not 8–80 characters |
| 400 | cannot_price | That service has no price configured right now |
| 402 | insufficient_funds | Top up and retry — nothing was charged |
| 404 | not_found | No such order of yours, or no such endpoint |
| 429 | rate_limited | Slow down |
| 500 | server_error | Our fault. If it happened during an order, you were not charged. |
404, not 403 — a 403 would
confirm the order exists.
Balance and top-ups
Orders are paid for from a prepaid balance. There is no card step on an individual order, which is what makes ordering a single API call.
Top up from your panel by card, PayPal or crypto. You go through the ordinary checkout, and the balance updates within about a minute of the payment confirming. The amount you top up is the amount credited — the minimum is $20 precisely so that no card processing fee applies and the two figures always agree.
Money is held in whole cents throughout. Nothing is ever rounded against you: an order costing a fraction of a cent is rounded to the nearest cent and can never round to zero.
Every movement is recorded and readable at /ledger.
If an order cannot be fulfilled, the money returns to your balance and appears there as a refund.
How pricing works
Your price is our ordinary retail price less your tier discount. Retail is read from the same place the website reads it, so the two cannot drift apart, and the discount is applied to the whole order rather than per unit — this matters on services priced in fractions of a cent, where discounting one unit at a time would round the saving away.
| Tier | Discount | Reached at |
|---|---|---|
| Starter | 10% | Your first order |
| Bronze | 15% | $250 lifetime spend |
| Silver | 20% | $1000 lifetime spend |
| Gold | 25% | $2500 lifetime spend |
Lifetime spend only ever rises. A refund puts the money back but does not un-earn a tier, so your pricing cannot move under you because of a failure that was ours. Your current tier and the gap to the next one are always on /me.
Call /services and read your_price_per_unit. It already has your discount in it,
and it follows both our retail prices and your tier without you doing anything.
Idempotency
idempotency_key is required on every order. It is your own reference — an order
id from your system, or a UUID — between 8 and 80 characters of letters, digits, _ - . :.
Send the same key twice and you get the first order back with "replayed": true, rather than a second order and a second charge. This is what makes retrying safe:
- a request that times out can be repeated without risk
- a crash mid-order can be recovered by simply sending it again
- a queue that delivers twice cannot charge you twice
A fresh key on each retry defeats the entire mechanism — each one looks like a new order and is charged as one. Store it with your own order record and reuse it for every attempt at that order.
Keys are unique per reseller, so yours cannot collide with anyone else's. There is no expiry: a key you used a year ago will still replay that order.
Your account
GET /me
Balance, tier and limits. Cheap; call it whenever.
{
"ok": true,
"business_name": "Your Shop",
"status": "approved",
"balance": 42.60,
"tier": {
"key": "bronze",
"name": "Bronze",
"discount_percent": 15,
"lifetime_spent": 312.40,
"next": { "name": "Silver", "discount_percent": 20, "spend_needed": 687.60 }
},
"rate_limit_per_minute": 120
}
tier.next is null once you are on the top tier.
The catalogue
GET /services
What you can order and what it costs you, with your discount already applied.
{
"ok": true,
"services": [
{
"id": "online-members",
"name": "Online Discord Members",
"unit": "member",
"min_quantity": 10,
"max_quantity": 50000,
"requires": { "link": "Discord server invite (https://discord.gg/...)" },
"retail_price_per_unit": 0.02,
"your_price_per_unit": 0.018,
"your_discount_percent": 10,
"min_order_cost": 0.18
}
]
}
| Field | Meaning |
|---|---|
id | What to send as service when ordering |
requires | Every parameter this service needs, and what it expects |
your_price_per_unit | Your price after discount. Multiply by quantity to estimate. |
min_order_cost | What the smallest possible order of this service costs you |
A service with no price configured is left out of the list entirely rather than shown as unavailable.
Placing an order
POST /orders
| Parameter | Required | Notes |
|---|---|---|
service | yes | An id from /services |
quantity | yes | Whole number, within the service's min and max, and a multiple of its step where it has one |
link | yes | What to deliver to. The shape depends on the service — see the catalogue. |
idempotency_key | yes | 8–80 chars, yours, reused across retries |
package | boosts only | A package id from /services — only in-stock ones are listed |
The price is never read from your request. Send totalPrice, paid or anything else and it is discarded — only the parameters listed above are used, and the cost is worked out here.
The request
curl -X POST https://discordad.com/api/v1/reseller/orders \
-H "Authorization: Bearer $RESELLER_KEY" \
-H "Content-Type: application/json" \
-d '{
"service": "online-members",
"quantity": 500,
"link": "https://discord.gg/example",
"idempotency_key": "shop-order-8842"
}'
Boosts additionally need a package:
curl -X POST https://discordad.com/api/v1/reseller/orders \
-H "Authorization: Bearer $RESELLER_KEY" \
-H "Content-Type: application/json" \
-d '{
"service": "server-boosts",
"quantity": 4,
"package": "1m",
"link": "https://discord.gg/example",
"idempotency_key": "shop-order-8843"
}'
The response
{
"ok": true,
"order": {
"id": "6a99f1c2e0d34a5b6c7d8e9f",
"reference": "k4m2p",
"service": "online-members",
"quantity": 500,
"link": "https://discord.gg/example",
"charged": 9.00,
"status": "processing",
"placed_at": "2026-09-04T11:02:44.518Z",
"completed_at": null
},
"balance": 33.60
}
201 for a new order, 200 with "replayed": true for a repeat of an idempotency key you have used before.
What happens to your money
In this order, deliberately, so that every failure leaves you whole:
- The order is created unpaid, carrying your idempotency key. A duplicate is caught here, before any money moves.
- Your balance is checked and debited in a single atomic operation — so concurrent orders can never together spend more than you have.
- The order is released to fulfilment.
If step 2 fails you are not charged and no order exists. If step 3 fails the money is returned immediately and the response says so. You are never charged for an order that was not placed.
A package that is out of stock is refused with cannot_price and the reason, and nothing is
charged. Packages sell out independently of each other, so read packages from
/services before ordering rather than assuming all four are available.
Quantities are in multiples of 2, from 2 to 30.
Checking one order
GET /orders/:id
Use the id from the create response. Only your own orders are visible; anything else is a 404.
There are no webhooks yet, so poll for status. Once a minute is plenty — these are not instant deliveries, and polling faster will meet the rate limit without telling you anything new.
Listing orders
GET /orders?limit=25
Your orders, newest first. limit is 1–100 and defaults to 25.
Your statement
GET /ledger?limit=25
Every movement on your balance, newest first.
{
"ok": true,
"balance": 33.60,
"entries": [
{ "kind": "order", "amount": -9.00, "balance_after": 33.60,
"order": "6a99f1c2e0d34a5b6c7d8e9f", "note": "online-members",
"at": "2026-09-04T11:02:44.531Z" },
{ "kind": "topup", "amount": 50.00, "balance_after": 42.60,
"order": null, "note": "Top-up · order k4m2p", "at": "2026-09-01T09:14:02.104Z" }
]
}
kind is one of topup, order, refund or
adjustment. Amounts are signed. The entries sum exactly to your balance — that is checked on our
side, and you can check it too.
Service catalogue
Live ids and requirements. Prices are not repeated here on purpose — take them from /services, which knows your tier.
| Service id | Quantity | link expects | Extra |
|---|---|---|---|
real-members |
100–10000 | Discord server invite (https://discord.gg/...) | — |
online-members |
100–50000 | Discord server invite (https://discord.gg/...) | — |
offline-members |
100–50000 | Discord server invite (https://discord.gg/...) | — |
server-boosts |
2–30, step 2 | Discord server invite (https://discord.gg/...) | package |
Order statuses
| Status | Means |
|---|---|
processing | Accepted and queued. This is what a new order returns. |
pending | Waiting on something — usually delivery capacity. |
completed | Delivered. completed_at is set. |
failed | Could not be delivered. The money goes back to your balance and shows in your ledger as a refund. |
Treat any status you do not recognise as still in progress rather than as an error; more may be added.
A worked example
Order 500 online members, then wait for it. Node, no dependencies:
const BASE = 'https://discordad.com/api/v1/reseller';
const KEY = process.env.RESELLER_KEY;
const api = async (path, opts = {}) => {
const res = await fetch(BASE + path, {
...opts,
headers: { 'Authorization': `Bearer ${KEY}`,
'Content-Type': 'application/json', ...(opts.headers || {}) },
});
const body = await res.json();
if (!body.ok) throw Object.assign(new Error(body.message), { code: body.error });
return body;
};
// The key is generated ONCE, with your own order — not per attempt.
// This is what makes the retry below safe.
const myOrderId = 'shop-order-8842';
const { order } = await api('/orders', {
method: 'POST',
body: JSON.stringify({
service: 'online-members',
quantity: 500,
link: 'https://discord.gg/example',
idempotency_key: myOrderId,
}),
});
console.log(`placed ${order.reference}, charged $${order.charged}`);
// Poll until it settles. Once a minute is plenty.
let state = order;
while (state.status !== 'completed' && state.status !== 'failed') {
await new Promise(r => setTimeout(r, 60_000));
({ order: state } = await api(`/orders/${order.id}`));
}
console.log('finished as', state.status);
Handling a low balance
try {
await placeOrder();
} catch (err) {
if (err.code === 'insufficient_funds') {
// Nothing was charged and no order exists. Top up, then send the SAME
// idempotency key again — it is still yours and still unused.
await alertMe('reseller balance too low');
} else {
throw err;
}
}
Keeping your key safe
- Server side only. A key in browser JavaScript or a mobile app is a key you have published — anyone can read it and spend your balance.
- Environment variables, not source. Committing one to a repository is the most common way these leak.
- One key per integration. Then a compromise can be revoked without taking your other systems down with it.
- Rotate by overlapping. Make the new key, deploy it, confirm it works, then revoke the old one. Revoking takes effect on the next request.
- Ask for an IP allow-list if you call us from fixed addresses. A stolen key is then useless from anywhere else.
Revoking a key never touches orders already placed, and never touches your balance. If you believe a key has leaked, revoke it immediately and check your ledger — every movement is there.