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.

Get a key

“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.

The key is shown once.

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

StatusErrorMeans
401no_keyNo key was sent at all
401bad_keyMalformed, unknown, or the wrong secret
401key_revokedThat key was revoked from your panel
403account_pendingApproved not yet — the key exists but the account is not live
403account_suspendedAccess paused; your balance is untouched
403ip_not_allowedAn IP allow-list is set and you are not on it
403https_requiredThe 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.

StatusErrorMeans
400invalid_requestA parameter is missing or wrong; message says which
400unknown_serviceNo such service id — the valid ones are listed in the message
400idempotency_key_requiredMissing or not 8–80 characters
400cannot_priceThat service has no price configured right now
402insufficient_fundsTop up and retry — nothing was charged
404not_foundNo such order of yours, or no such endpoint
429rate_limitedSlow down
500server_errorOur fault. If it happened during an order, you were not charged.
Asking for an order that belongs to somebody else returns 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.

TierDiscountReached 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.

Never hard-code our prices.

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
Generate the key before the first attempt, not per attempt.

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
    }
  ]
}
FieldMeaning
idWhat to send as service when ordering
requiresEvery parameter this service needs, and what it expects
your_price_per_unitYour price after discount. Multiply by quantity to estimate.
min_order_costWhat 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

ParameterRequiredNotes
serviceyesAn id from /services
quantityyesWhole number, within the service's min and max, and a multiple of its step where it has one
linkyesWhat to deliver to. The shape depends on the service — see the catalogue.
idempotency_keyyes8–80 chars, yours, reused across retries
packageboosts onlyA 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:

  1. The order is created unpaid, carrying your idempotency key. A duplicate is caught here, before any money moves.
  2. Your balance is checked and debited in a single atomic operation — so concurrent orders can never together spend more than you have.
  3. 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.

Boosts are checked against live supplier stock.

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 idQuantitylink expectsExtra
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

StatusMeans
processingAccepted and queued. This is what a new order returns.
pendingWaiting on something — usually delivery capacity.
completedDelivered. completed_at is set.
failedCould 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.

Something here unclear or missing? Tell us — documentation that does not answer your question is a bug in the documentation.