Reseller API
Buy products with your account balance and pull delivered accounts, codes and links — and the state of pending deliveries — into your own system automatically.
https://followturk.com/api/v1Contents
Getting started
- Verify your phone number. Your account must be at least 14 days old.
- In Account → API press "Create my key" and enter the code sent to your phone. The key is shown only once — store it safely. You get an e-mail whenever a key is created.
- Add balance. The API buys only with balance; if your balance is not enough, no order is created.
Tiers and limits
Limits rise automatically with your account's total purchases. See your current tier in GET /account and on Account → API.
| Tier | Condition | Daily spending | Requests / minute |
|---|---|---|---|
| 1 | Total $0 purchases, 14-day-old account | $25 | 30 |
| 2 | Total $100 purchases, 30-day-old account, 1 paid order(s) | $150 | 60 |
| 3 | Total $500 purchases, 60-day-old account, 1 paid order(s) | $500 | 120 |
The daily limit and spent_today count the full amount of every API order placed today: a refunded order still counts and a refund does not reopen the limit. On a product priced below a cent this amount can differ from total by up to half a cent per order.
If you need higher limits, contact our support team. Accounts with a card dispute (chargeback) permanently lose API access.
Authentication
Send the key only in a request header:
curl -H "Authorization: Bearer ftk_YOUR_KEY" https://followturk.com/api/v1/account curl -H "X-API-Key: ftk_YOUR_KEY" https://followturk.com/api/v1/account
Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | / | Balance, daily limit and today's spend |
| GET | / | Categories that have API products |
| GET | / | Products sold through the API (paged) |
| GET | / | One product |
| POST | / | Place an order paid from balance |
| GET | / | Orders you placed through the API (paged, newest first, without delivery details) |
| GET | / | Order and line status, delivered accounts/codes/links |
| POST | / | Give new input to a line that waits for it (e.g. a new invite link) |
| POST | / | Steam: a Steam Guard code for the line's account |
/products and /orders accept the query parameters page and per_page (max 100; default 50 for /products, 20 for /orders). /products also accepts category_id and type (e.g. discord_boost). Each takes a single value; an array (e.g. page[]=1) is refused with 422 invalid_query.
Account
Your balance, tier and limits. tier is 1, 2 or 3; it is "custom" when your limits were set for you separately. Amounts are in USD.
{ "success": true, "data": { "username": "...", "balance": 42.5, "currency": "USD", "tier": 1,
"daily_limit": 25.0, "spent_today": 4.5, "remaining_today": 20.5, "requests_per_minute": 30 } }
Categories
Only categories that have API products are listed. Use id as category_id on /products. This endpoint is not paged.
{ "success": true, "data": [{ "id": 5, "name": "...", "product_count": 12 }] }
Products
{ "id": 26, "name": "...", "type": "discord_boost", "category": { "id": 5, "name": "..." }, "currency": "USD",
"unit_price": 0.282, "min_quantity": 2, "max_quantity": 14, "in_stock": true,
"delivery_mode": "async", "quantity_step": 2, "order_minimum": 0.5,
"required_fields": [{ "name": "invite_link", "type": "string", "required": true, "max_length": 512, "rule": "...", "example": "https://discord.gg/abcd1234" }],
"options": [] }
{ "id": 123, "name": "...", "type": "code", "category": { "id": 7, "name": "..." }, "currency": "USD",
"price": null, "variants": [{ "id": 789, "name": "...", "unit_price": 4.5, "min_quantity": 1, "max_quantity": 50, "in_stock": true }], "in_stock": true,
"delivery_mode": "instant", "quantity_step": 1, "order_minimum": 1, "required_fields": [], "options": [] }
| Field | Description |
|---|---|
id, name, category, currency | Product id, name, category (id, name) and currency |
unit_price | Unit price. A product with variants has price = null and variants instead; a product priced by a choice has null here and pricing instead. It may be null for a product that cannot be bought. |
min_quantity, max_quantity | The smallest and largest quantity allowed on a line |
quantity_step | The quantity must be a multiple of this (Discord boosts: 2, others: 1) |
variants[] | Each variant: id (variant_id on the order), name, unit_price, min_quantity, max_quantity, in_stock |
pricing | The price table of an account product priced by a choice |
in_stock | Whether the product can be bought right now (yes/no) |
delivery_mode | instant: delivered with the order; async: poll the order |
order_minimum | The smallest order amount when the product is bought alone (USD) |
required_fields[] | The input asked in items[].fields: name, type, required, max_length, rule, example. When there is one: pattern and required_when (e.g. {"delivery": "boost"}: the field is required only with this option) |
options[] | The choices given in items[].options |
description | Only in GET /products/{id}: a short description |
Product types and buyer input
Every product carries type, delivery_mode, required_fields and options. The price is the same as on the website. Buyer input, delivery and refunds are too, with three differences: on Steam you send your end customer's customer_ref; an MM2 item is delivered directly as a code; and when a code cannot be handed over, the website keeps the amount and the team delivers the code later, while the API returns the amount to your balance at once. For quantity limits and available options, min_quantity, max_quantity and options in the product reply apply.
| type | What | Delivery | fields |
|---|---|---|---|
account | Account — some products are priced by a choice (options.selection + options.amount) | instant; if only part of the quantity is ready the line is partial and that part is in delivery, it becomes delivered when the rest arrives; if none is ready yet it is processing. When the product's delivery_mode is async the line stays processing until it is ready | — |
code | Digital code | instant | — |
discord_members | Discord members — a one-time link is delivered | a few minutes (async) | — |
discord_boost | Discord server boosts — quantity in multiples of 2 | async | invite_link |
robux | Rbx — through an RBL gamepass | async (within 24 hours) | gamepass_link |
steam_shared_account | Shared Steam library account — per game, quantity 1 | async | customer_ref |
mm2_item | RBL MM2 item — delivered as a code, redeemed at followturk.delivery | async | — |
invite_link: Discord server invite (discord.gg/<code>, discord.com/invite/<code> or the bare code). It must never expire and have no use limit. An invalid or expired invite is refused with 422 field_invalid.gamepass_link: An RBL gamepass link priced at EXACTLY the ordered quantity, on sale, regional pricing off (or the link of a public game that has such a gamepass). A bare username is not accepted. A link that does not fit is refused with 422 field_invalid before any money moves.options.delivery: Only when the product lists it in options: boost (default, needs invite_link) or code (a boost code is delivered; do not send invite_link). An option that is not listed is refused with 422 unknown_option.customer_ref: Steam: your own id for the end customer (1-64 letters, digits, "_" or "-"). Always send the same value for the same person: that person gets one account per game and cannot buy the same game twice (409 already_owned). Upper and lower case differ: Ali and ali are two people.options.selection+options.amount: Account products priced by a choice: selection = skins, be or level (those listed in the product's options), amount = a whole number inside that selection's range (sent as a string). The price table is in the product's pricing field; the exact amount is computed on the order.in_stock: Whether the product can be bought right now (yes/no). No quantity is given.
order_minimum is the smallest order amount when the product is bought alone (0.50 USD for Discord products). Rbx quantity goes up to the product's max_quantity; at most 5 Rbx lines per order.
Place an order
Send a unique Idempotency-Key header with every order (8-64 chars: letters, digits, "-" or "_"). Resending with the same key never creates a second order; it returns the first one, so retrying after a network error is safe.
curl -X POST https://followturk.com/api/v1/orders \
-H "Authorization: Bearer ftk_YOUR_KEY" \
-H "Idempotency-Key: order-20261012-0001" \
-H "Content-Type: application/json" \
-d '{"items":[{"product_id":123,"quantity":2},{"product_id":456,"variant_id":789,"quantity":1}]}'
| Field | Description |
|---|---|
items[].product_id | Product ID (integer, required) |
items[].quantity | Quantity (integer, within the product's min/max) |
items[].variant_id | Required for products with variants, not allowed otherwise |
items[].fields | The buyer input listed in the product's required_fields (object of strings). Any field not on that list is refused. |
items[].options | Choices listed in the product's options (e.g. DC Boost: delivery = boost | code). |
items[].max_unit_price | Optional. If the unit price is above it the order is refused with 409 price_changed and nothing is charged (for products whose price changes often). |
curl -X POST https://followturk.com/api/v1/orders \
-H "Authorization: Bearer ftk_YOUR_KEY" \
-H "Idempotency-Key: boost-20261103-0001" \
-H "Content-Type: application/json" \
-d '{"items":[{"product_id":26,"quantity":4,"fields":{"invite_link":"https://discord.gg/abcd1234"}}]}'
Reply
A new order answers 201. Resent with the same Idempotency-Key the answer is 200 with meta.replayed = true. The fields are explained under Read an order.
HTTP 201
{ "success": true, "data": { "order_number": "FT1A2B3C4D", "status": "completed", "total": 9.5, "currency": "USD", "created_at": "2026-11-05T12:00:00+03:00",
"items": [{ "line": 1, "product_id": 123, "name": "...", "type": "code", "quantity": 2, "unit_price": 4.75, "total": 9.5, "status": "delivered" }] } }
HTTP 200
{ "success": true, "data": { ... }, "meta": { "replayed": true } }
Delivered accounts and codes are not in the create response; fetch them with GET /orders/{order_number}.
Read an order
Delivery details come only in GET /orders/{order_number}. Reading accounts and codes this way counts as taking delivery, just like on the website.
{ "success": true, "data": { "order_number": "FT1A2B3C4D", "status": "completed", "total": 9.5, "currency": "USD", "created_at": "2026-11-05T12:00:00+03:00",
"items": [{ "line": 1, "product_id": 123, "name": "...", "type": "code", "quantity": 2, "unit_price": 4.75, "total": 9.5, "status": "delivered", "delivery": ["AAAA-BBBB", "CCCC-DDDD"] }] } }
total is what really left your balance, the same amount as your transaction record. On a product priced below a cent this amount can change by 1 cent with your balance, so always keep your accounts with the total in the reply. On an order with several lines each line total is rounded on its own and they can add up to a different amount; the order's total is the amount that counts.
Line fields
| Field | Description |
|---|---|
line | Position, starting at 1; used on the /lines/{line} endpoints |
product_id, name, type, quantity | Product id, name, type and quantity |
unit_price, total | Unit price and line total |
status | The line's status (see the table below) |
delivery | What was delivered; only in GET /orders/{order_number} |
refund | When refunded, the amount: refund.amount |
progress | Discord boosts: sending progress |
input_required | What a needs_input line asks for |
customer_ref | Steam: the value you sent |
selection | An account product priced by a choice: type (skins, be, level) and amount |
Line status
async lines move on within minutes; poll the order at a sensible interval (e.g. once a minute).
| status | Meaning |
|---|---|
processing | Queued or being delivered |
needs_input | Delivery needs new input: send the input_required.fields to /lines/{line}/input |
in_review | Our team is checking it; nothing to do on your side |
delivered | Delivered (see delivery) |
partial | Partly delivered: the delivered part is in delivery. The rest either arrives (the line becomes delivered) or its price is refunded (refund.amount) |
failed | Could not be delivered; contact support |
refunded | The whole line was refunded: the amount really went back to your balance |
cancelled | Cancelled. Without refund.amount the amount did not go back to your balance; contact support |
on_hold | The order is on hold for review |
{ "line": 1, "type": "discord_boost", "quantity": 4, "status": "needs_input",
"input_required": { "fields": ["invite_link"], "actions": [], "reason": "invite_invalid", "screening_ack_required": false } }
{ "line": 1, "type": "account", "quantity": 2, "status": "delivered", "delivery": ["login:password", "login:password"] }
{ "line": 1, "type": "account", "quantity": 5, "status": "partial", "delivery": ["login:password", "login:password"] }
{ "line": 1, "type": "code", "quantity": 3, "status": "partial", "refund": { "amount": 2.5 }, "delivery": ["AAAA-BBBB", "CCCC-DDDD"] }
{ "line": 1, "type": "discord_members", "status": "delivered", "delivery": { "link": "https://…" } }
{ "line": 1, "type": "discord_boost", "status": "delivered", "progress": { "delivered": 4, "target": 4 } }
{ "line": 1, "type": "discord_boost", "status": "delivered", "delivery": { "code": "...", "boosts": 4, "period": "1m", "expires_at": "..." } }
{ "line": 1, "type": "robux", "status": "delivered", "delivery": { "robux": 1000 } }
{ "line": 1, "type": "steam_shared_account", "status": "delivered", "customer_ref": "shop-9921", "delivery": { "login": "...", "password": "...", "guard": { "available": true, "method": "mafile" } } }
{ "line": 1, "type": "mm2_item", "status": "delivered", "delivery": { "quantity": 2, "codes": [{ "code": "...", "quantity": 2, "name": "..." }] } }
The shape of delivery follows the line's type: accounts and codes = a list of texts; Discord members = link; Discord boost = code (or only progress); Rbx = robux; Steam = login, password, guard; MM2 = codes. When a delivery was handed over as plain text, delivery is a list of texts on any type (Steam, Discord members and MM2 too); on Rbx and MM2 such texts can also come in a details field. progress appears only once sending has started.
A partial refund (refund.amount) does not hide delivered accounts, codes or links, and the line keeps its delivery status. Delivery details are hidden only when the whole line or the whole order is refunded.
Order status
Order status: processing (work is going on for at least one line), needs_input (at least one line waits for input), completed (nothing more will happen; read the lines, some may be refunded), refunded (the whole amount went back to your balance), cancelled (if the amount did not come back, contact support), failed (no line could be delivered; contact support) or on_hold. You can stop polling on completed, refunded, cancelled and failed. on_hold is temporary: the order can go on after the review, so keep polling.
If the key that placed the order was rotated or deleted: for orders under 7 days old the new key receives the delivery details. For older orders the line status is shown but delivery is not, an MM2 item is not turned into a code, and Steam Guard answers 409 not_available_for_this_key. You reach those deliveries from your account on the website.
Send input
Works only on a line with status = needs_input and takes no money. It is the same action as "fix the link / try again" on the website's order page, with the same limits.
curl -X POST https://followturk.com/api/v1/orders/FT1A2B3C4D/lines/1/input \
-H "Authorization: Bearer ftk_YOUR_KEY" -H "Content-Type: application/json" \
-d '{"fields":{"invite_link":"https://discord.gg/newcode"}}'
-d '{"action":"retry","screening_ack":true}'
-d '{"fields":{"gamepass_link":"https://…/game-pass/123456789/My-Pass"}}'
| Field | Description |
|---|---|
fields | New values for the line's input_required.fields |
action | "retry": try again with the same input (after fixing the server setting) |
screening_ack | DC Boost: confirms you turned off the server's "Apply to Join" (required when screening_ack_required = true) |
The reply returns the updated order without delivery details; meta.line is the line that was handled. DC Boost: 5 attempts per line and 20 per account per hour; Rbx: 15 checks and 3 sends per 10 minutes.
While ordering through the API is closed, this endpoint is closed too (503 orders_disabled or orders_paused).
MM2 items
An MM2 line is delivered directly as codes; no extra call is needed. After the order the line is processing; when the codes are ready it becomes delivered and the codes are in delivery.codes of GET /orders/{order_number} (code, quantity, name). The codes are redeemed at followturk.delivery; hand the code to your end customer.
{ "line": 1, "type": "mm2_item", "quantity": 2, "status": "processing" }
{ "line": 1, "type": "mm2_item", "quantity": 2, "status": "delivered",
"delivery": { "quantity": 2, "codes": [{ "code": "...", "quantity": 2, "name": "..." }] } }
- Reading the order again returns the same codes; it makes no new code and takes no money.
- The code is created the first time you read the order after the line is delivered (GET /orders or GET /orders/{order_number}). If you took the item into the game from your account on the website before that, there is no code for it: delivery.state = withdrawn_on_site.
- If the order cannot be placed in this quantity you get 409 out_of_stock; the reply does not say which line or how many. For a product that is on sale, these refusals for a quantity that does not fit are limited to 5 per hour per account: past that the same attempt gets 429 rate_limited, and the Retry-After header and details.retry_after say how many seconds to wait. An order in a quantity that can be bought is not affected by this limit. A refusal for a product the catalogue shows as in_stock: false is not counted.
- If an item cannot be prepared for a long time the line becomes in_review; our team looks at it and refunds it if needed (refunded). The codes are no longer shown on a refunded line.
Steam Guard
Gives a Steam Guard code for the account of a delivered Steam line; takes no money. The allowance is per line, as on the website: the first code opens a session in which a set number of codes can be taken, then the allowance locks (details.unlock_at). A failed request costs nothing. Your account also has a daily ceiling (delivered Steam lines × the per-line allowance); when it is reached you get 429 guard_daily_limit with details.unlock_at. The body must be empty, {} or []; anything else is refused with 422 invalid_body and costs nothing.
curl -X POST https://followturk.com/api/v1/orders/FT1A2B3C4D/lines/1/steam-guard \
-H "Authorization: Bearer ftk_YOUR_KEY" -H "Content-Type: application/json" -d '{}'
{ "success": true, "data": { "line": 1, "code": "AB12C", "method": "mafile", "expires_in": 30,
"quota": { "used": 1, "max": 7, "remaining": 6, "unlock_at": "..." } } }
The account (login, password and, when needed, ea) is read live on every GET /orders/{order_number}; a changed password shows the new one. If it cannot be read for a moment, delivery = {"state": "temporarily_unavailable"}.
Reply format and error codes
{ "success": true, "data": { ... }, "meta": { "page": 1, "per_page": 50, "total": 120, "total_pages": 3 } }
{ "success": false, "error": { "code": "insufficient_balance", "message": "Insufficient balance.", "details": { "required": 12.5, "balance": 4 } } }
| HTTP | Code | Meaning |
|---|---|---|
400 | key_in_url idempotency_key_required | Key in URL, or Idempotency-Key missing |
401 | unauthorized | Missing or invalid key |
402 | insufficient_balance funds_on_hold | Not enough balance, or the part of it added by card recently cannot be spent through the API yet (details.on_hold, details.hold_hours) — no order was created |
403 | account_restricted phone_not_verified not_eligible api_blocked forbidden | This account cannot use the API, does not meet the requirements yet, its API access was closed, or requests from this address are refused |
404 | not_found product_not_found order_not_found line_not_found | Not found |
404 | wrong_host | The request went to a host other than followturk.com/api/v1 |
405 | method_not_allowed | Only GET and POST are supported |
409 | out_of_stock idempotency_conflict | The product is not available right now or not in this quantity, or the Idempotency-Key was used for a different order |
409 | not_needs_input invites_paused | The line is not waiting for input, or invites are paused on the server |
409 | already_owned price_changed | This customer_ref already owns the game (details.order_number), or the unit price is above max_unit_price |
409 | not_available not_delivered wrong_line_type not_available_for_this_key | The line is refunded, cancelled or on hold, not delivered yet, the endpoint does not fit the line, or the order was placed with an earlier key |
409 | guard_login_first guard_unavailable access_ended | Steam Guard: sign in to Steam with the account first / no code for this account / access has ended |
413 | body_too_large | The request body is too large |
422 | invalid_* product_unavailable below_minimum | Invalid request |
422 | unknown_fields variant_required variant_not_allowed duplicate_item | The body has a field that is not allowed, no variant was chosen, this product has no variants, or the same product/variant (Steam: the same game and customer_ref) is on two lines |
422 | mixed_order | The order mixes product families |
422 | field_required field_invalid unknown_field | Buyer input missing, invalid or not used by this product (details.field, details.reason) |
422 | unknown_option option_invalid screening_ack_required | Invalid option, or an acknowledgement is required |
422 | option_required | A required option is missing (e.g. selection) |
429 | rate_limited daily_limit_reached too_many_failed_attempts | A request limit (MM2: refused attempts for an unavailable quantity are limited too) or the daily spending limit is reached, or too many invalid keys were tried from this address. On the per-minute request limit, the invalid key limit and the MM2 limit the Retry-After header gives the wait |
429 | quota_locked guard_cooldown guard_daily_limit busy | Steam Guard: the line's allowance is used up (details.unlock_at), asked too often, the account's daily Steam Guard ceiling is reached (details.unlock_at), or another request for the same line is running (busy) |
500 | internal_error | Server error. On an order, resend with the same Idempotency-Key and the same body: the order is created once and charged once |
503 | api_disabled orders_disabled orders_paused busy | API or ordering temporarily closed or busy; retry later. An account's orders are processed one at a time: if its turn does not come within 5 seconds the answer is busy, nothing is charged, retry with the same Idempotency-Key. When present, the Retry-After header gives the wait |
API key
Applications and key management live in Account → API.
Log in