AIBulk Developer / Wholesale API
Base URL: https://api.aibulk.uk/wholesale/v1. The machine-readable contract is https://api.aibulk.uk/openapi.json; the online version of this document is at aibulk.uk/en/developers/api, with an overview and code samples at aibulk.uk/en/developers.
Conventions
Every request carries the API key generated in the account center:
Authorization: Bearer awk_xxxxxxxx...
- Amounts are decimal strings with at most two decimals (for example
"22.00"); never use floats. - Default rate limit: 300 requests per minute. Responses carry
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset; a 429 also carriesRetry-After. - Every response carries
X-Request-Id. Error bodies includerequestIdwhere possible; include it when reporting a problem. - Branch on the stable
errorcode, not on the human-readablemessage. Some conflicts also returndetails; 5xx responses are markedretryable: true. The full list is under "Error codes" below. - Codes issued through the API have no expiry date.
payment=intentorders and top-ups expire after about 15 minutes without payment. USDC top-ups are credited 1:1 to the USDT balance.
Payment compliance
- Since August 2026 Binance has been blocking transactions with 17 sanctioned platforms (including HTX, BitPapa, EXMO, Rapira, Aifory Pro, ABCeX, WhiteBird, Shelbit, Aban Tether and A7). Wallets with direct or indirect fund flows to these entities may be frozen or have funds held for compliance review; see the official Binance announcement.
- Only pay top-ups and single orders from wallets with no fund flows to those entities or their associated addresses; any freeze, hold or loss caused by such addresses is borne by the payer.
- This platform has no association of any kind with sanctioned entities.
Levels and pricing
User levels Lv1 to Lv5 are raised automatically by "cumulative spending + current balance" and never go down. The web, the REST API and MCP all settle at the product's price for the user's level; price in GET /products is the caller's current unit price.
Recommended ordering flow
GET /productsfor products, your current unit price and fulfilment status. Order by the stableid, never byslug.- Call
GET /products/:id/availability?qty=Nbefore large orders. - Send your own unique
externalOrderIdwithPOST /orders; on network errors, timeouts or 5xx, retry with the same value and the same product and quantity. - If a silent price change is unacceptable, pass the latest quote (
pricefromGET /productsorunitPricefromavailability) asexpectedUnitPrice; a change returns409 price_changedwith the current price. payment=balancedebits immediately and platform codes are usually returned synchronously; direct-code products may first returnpaid, so query the original order or wait for the webhook.payment=intentreturns a checkout URL; after payment, use the same query flow.- Deliver according to
deliveryType:redemptionsends the code plusredeemUrl;direct_codesends the actual gift card code withredeemUrl=nulland no platform redemption.
0.01 USDT live order test
The test product API order test (no real benefits) has the ID prod_api_order_test. It is hidden from the shop and from product lists; use the ID directly.
- It is a real charge in production, 0.01 USDT per unit at every level; it is not a free sandbox.
- REST API only,
payment=balanceonly,qty=1only, and at least 0.01 USDT balance is required; it cannot be bought on the web, through MCP or with a single online payment. - It verifies balance debit, code delivery, order queries,
externalOrderIdidempotency and any configuredorder.deliveredwebhook. Standard API rate limits apply. - The returned codes are test codes with no real benefits and cannot be resold as subscriptions. Submitting one on the redemption site returns
400 test_code_only; it is not for testing real subscription activation. - A test code can be submitted to
POST /redemptions(anyredeemInfo,{}recommended): it completes in the same transaction (status=fulfilled, a fixeddeliverabletext) and emitsredemption.fulfilled. This lets you exercise "order → server-side redemption → polling / webhook" end to end for 0.01 USDT; resubmitting the same test code returns409 already_redeemed. - Retrying the same order must reuse
externalOrderIdand the same parameters, which never charges twice; a different ID creates a new order and charges again.
curl https://api.aibulk.uk/wholesale/v1/orders \
-H "Authorization: Bearer $AIBULK_API_KEY" \
-H "Content-Type: application/json" \
-d '{"externalOrderId":"integration-test-001","productId":"prod_api_order_test","qty":1,"payment":"balance","expectedUnitPrice":"0.01"}'
You can first call GET /products/prod_api_order_test/availability?qty=1 to check purchasability. A quantity other than 1 returns 400 api_test_quantity; an unsupported channel or payment method returns 400 api_test_balance_only.
Direct-code gift cards
deliveryType on products and orders distinguishes delivery: redemption uses platform codes plus a redemption URL; direct_code delivers the gift card code itself, redeemUrl is null, and the platform redemption site is not involved.
US App Store gift cards, Roblox USD gift cards and Robux codes are direct_code. After a balance payment or a completed single payment, the order may temporarily return status: "paid" with codes: [] while codes are being obtained. Query the original order ID or wait for the order.delivered webhook; the status becomes delivered only after the full quantity is delivered, and codes[] then holds the actual card codes. Do not create a new order or change the idempotency ID while waiting. Order downloads likewise contain only actual card codes. US Apple cards work only with US-region Apple Accounts.
Group productGroupId |
Example value | faceCurrency |
region |
Official redemption page |
|---|---|---|---|---|
apple-gift-card-us |
"25.00" |
USD |
US |
https://apps.apple.com/redeem |
roblox-gift-card-us |
"25.00" |
USD |
US |
https://www.roblox.com/redeem |
robux |
"800" (Robux amount) |
ROBUX |
GLOBAL |
https://www.roblox.com/redeem |
Multi-value gift cards (Apple Gift Card / Roblox / Robux)
The catalog keeps the original list of individual SKUs, and every row can still be ordered by id. New fields let you merge several values for display:
| Field | Example | Meaning |
|---|---|---|
productGroupId |
apple-gift-card-us |
Display group; cannot be used as the order productId |
denomination |
"25.00" / "800" |
Actual face value, a decimal string in faceCurrency units |
faceCurrency |
USD / ROBUX |
Unit of the face value, distinct from the payment currency |
region |
US / GLOBAL |
Card region |
price |
"24.99" |
The current account's actual unit purchase price |
stockMode |
limited |
Finite stock; the quantity must be checked separately and stock is null |
For ordinary products the four group/value fields are null. SKUs that share a productGroupId can form a value picker; show only options with inStock && available, sorted by numeric value. When buying, pass the selected row's id, not the page slug, and do not pass denomination instead of choosing a SKU.
For limited products, availability?qty=N reads the latest purchasability and subtracts this platform's pending orders; ordering re-validates before debiting or creating a payment link and prevents concurrent requests on this platform from double-claiming the remainder. The availability query itself reserves nothing, so stock can still run out before or after payment. 409 insufficient_stock, stock_unavailable or fulfillment_unavailable mean nothing was debited and no payment link was created; reduce the quantity or check again later. On a network timeout or an unclear result, look up the order by the original externalOrderId first.
Example: select the $25 value and buy one card from the balance. API_KEY comes from your own secure configuration; externalOrderId must be persisted by your system and reused on timeout retries.
const base = "https://api.aibulk.uk/wholesale/v1";
const headers = { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" };
async function api(path, init = {}) {
const response = await fetch(base + path, { ...init, headers });
const data = await response.json();
if (!response.ok) throw Object.assign(new Error(data.message), { status: response.status, data });
return data;
}
const { products } = await api("/products");
const selected = products.find(p => p.productGroupId === "apple-gift-card-us"
&& p.denomination === "25.00" && p.faceCurrency === "USD" && p.region === "US"
&& p.inStock && p.available);
if (!selected) throw new Error("This denomination is unavailable");
const quote = await api(`/products/${encodeURIComponent(selected.id)}/availability?qty=1`);
if (!quote.available) throw new Error("This quantity is unavailable");
const result = await api("/orders", {
method: "POST",
body: JSON.stringify({ externalOrderId, productId: selected.id, qty: 1,
payment: "balance", expectedUnitPrice: quote.unitPrice })
});
// Persist result.orderId. Reuse it for GET /orders/:id and webhook reconciliation.
// payment=intent instead returns checkoutUrl; inspect the same order after payment.
Direct-code orders additionally expose, in the create response, the order list and the order detail:
fulfillmentStatus |
Client behaviour |
|---|---|
awaiting_payment |
Waiting for payment to complete |
processing |
Query the original order every 10–30 seconds, or wait for the notification |
needs_attention |
Show "under manual review", slow down polling, contact support with the original order number; do not buy again |
delivered |
Deliver codes[] from the detail to the buyer |
expired |
Expired unpaid; if a payment was started with an unclear result, check the original order first |
refunded |
The order has a recorded refund status |
fulfilledQuantity is the number of card codes obtained so far; a partially completed order still returns no codes, and everything is delivered together once the whole order completes. needs_attention also returns errorCode: "delivery_needs_attention" and resolution: "manual_review". It means support is reviewing the remaining delivery and any refund; it does not mean a refund has been paid. Orders with codes already obtained, or with an unclear result, are not refunded in full automatically. Judge the payment status paid separately from the fulfilment status. After delivery, give the buyer the matching official redemption page from the table above.
The exception notification is order.delivery_attention, with the same signature, retry and eventId deduplication rules as order.delivered; it is sent once when an order first enters manual review. Notifications may arrive out of order; query the original order on arrival to confirm the latest state.
{
"eventId": "order:example-order:order.delivery_attention",
"event": "order.delivery_attention",
"timestamp": 1787500000,
"orderId": "example-order",
"externalOrderId": "your-order-123",
"productId": "selected-sku-id",
"qty": 2,
"deliveryType": "direct_code",
"fulfillmentStatus": "needs_attention",
"fulfilledQuantity": 1,
"errorCode": "delivery_needs_attention",
"resolution": "manual_review",
"codes": [],
"redeemUrl": null
}
Endpoints
GET /products — products and quotes
Returns products, the current user's unit price, the stock mode and the fulfilment status:
{
"products": [{
"id": "prod_chatgpt_plus",
"name": "ChatGPT Plus (1个月)",
"price": "21.56",
"currency": "USDT",
"stockMode": "on_demand",
"availabilityStatus": "available",
"deliveryMode": "automatic"
}]
}
Common fields:
id: stable product ID, used for ordering and availability checks.slug: display/SEO identifier that may change; not recommended as a basis for programmatic ordering.price: the unit price for the level of the user behind the API key.stockMode=inventory: limited by the platform's own inventory;stockis cached for at most 60 seconds and shown with a cap.stockMode=on_demand: fulfilled per redemption request,stock=null.stockMode=limited: finite stock whose quantity is not published,stock=null; run the availability check for the desired quantity, which is validated again at order time.deliveryMode=automaticmeans automatic fulfilment after redemption;manual/manual_fallbackmeans manual handling may be needed.availabilityStatus=degradedmeans the automatic route is unavailable and a manual fallback applies; it does not promise instant automatic delivery.redeemFields/redeemFieldsEn: what the end user must provide at redemption (identical to the redemption site's check response;[]fordirect_codegift cards). Downstreams redeeming on behalf of customers should collect this before purchasing;keyis the key name inredeemInfo.
GET /products/:id returns a single product in the same shape (bypassing the list cache); unknown or delisted products return 404 not_found. For example, Claude Pro:
{
"id": "prod_claude_pro",
"price": "22.00",
"deliveryType": "redemption",
"redeemFields": [
{ "key": "account_id", "label": "Claude Organization ID", "type": "text", "required": true }
]
}
GET /products/:id/availability?qty=50 — purchasability and quote
{
"productId": "prod_chatgpt_plus",
"qty": 50,
"available": true,
"unitPrice": "21.56",
"stockMode": "on_demand",
"availabilityStatus": "available",
"deliveryMode": "automatic",
"checkedAt": 1787500000
}
The result reflects this platform's current stock and routing configuration; it is not an SLA guarantee from upstream services.
GET /balance — balance
{ "balance": "1250.00", "currency": "USDT" }
POST /topup — idempotent top-up
New integrations must send externalTopupId, or put it in the Idempotency-Key header. Timeout retries must reuse the same value:
{ "externalTopupId": "topup-20260824-001", "amount": "500.00", "currency": "USDT" }
{
"orderId": "...",
"externalTopupId": "topup-20260824-001",
"amount": "500.00",
"status": "pending",
"checkoutUrl": "https://checkout.tariapay.com/...",
"reused": false
}
Reusing an idempotency key with a different amount or currency returns 409 idempotency_conflict. A top-up expires after about 15 minutes without payment; once credited, GET /balance is the source of truth, and you can also subscribe to topup.credited. USDC top-ups are credited 1:1 to the USDT balance.
Each account may start at most 20 top-up requests per hour (idempotent replays count; the account center, the REST API and MCP share the quota) and may have at most 5 unpaid top-ups at a time; beyond that the API returns 429 too_many_pending_topups: pay or let them expire before creating a new one.
During the compatibility window, legacy clients that send only amount / currency can still create a top-up, but the response carries HTTP Warning: 299 and idempotencyWarning; such requests cannot be retried safely. Add the idempotency key as soon as possible; a future major version will make it mandatory.
POST /orders — idempotent bulk order
{
"externalOrderId": "your-order-123",
"productId": "prod_chatgpt_plus",
"qty": 50,
"payment": "balance",
"expectedUnitPrice": "21.56"
}
Balance mode returns immediately:
{
"orderId": "...",
"externalOrderId": "your-order-123",
"amount": "1078.00",
"status": "delivered",
"codes": ["AB4K-9XQ2-..."],
"redeemUrl": "https://redeemhub.uk",
"reused": false
}
Single-payment mode returns checkoutUrl (the compatibility field paymentUrl may also appear); the order is usually pending at this point and no code is returned:
{
"orderId": "...",
"externalOrderId": "your-order-123",
"amount": "1078.00",
"status": "pending",
"checkoutUrl": "https://checkout.tariapay.com/...",
"paymentUrl": "https://checkout.tariapay.com/...",
"redeemUrl": "https://redeemhub.uk",
"reused": false
}
Give checkoutUrl only to the payer; never log or publish it.
Once the user has paid and Tariapay has confirmed it, the platform starts delivery; direct-code orders may stay paid, so also check fulfillmentStatus. Integrators should query GET /orders/:id or receive the order.delivered webhook, and ship downstream only after confirming all codes are usable. An order not confirmed within the payment window becomes expired; to restart an unpaid order, use a new externalOrderId.
Insufficient balance returns 402 insufficient_balance; stock, idempotency conflicts and price changes use 409 with the matching error codes.
GET /orders / GET /orders/:id — order queries
The list supports externalOrderId, limit=1..100 and cursor, and returns:
{ "orders": [], "nextCursor": null, "hasMore": false }
Pass the previous page's nextCursor through unchanged for the next page; do not parse the cursor. An invalid or expired cursor returns 400 invalid_cursor. List rows are summaries without codes or paymentUrl; the detail carries codes when status=delivered. Deliver both the code and redeemUrl for redemption products; direct_code products deliver the actual card code with redeemUrl=null.
GET /orders/:id example (a balance-paid platform-code order):
{
"id": "…",
"external_order_id": "your-order-123",
"channel": "wholesale_api",
"product_id": "prod_claude_pro",
"order_number": "…",
"qty": 2,
"unit_price": "22.00",
"amount": "44.00",
"currency": "USDT",
"status": "delivered",
"deliveryType": "redemption",
"created_at": 1787500000,
"paid_at": 1787500000,
"product_name": "Claude Pro (1个月)",
"redeemUrl": "https://redeemhub.uk",
"paymentUrl": null,
"codes": ["AB4K-9XQ2-XXXX-XXXX", "CD7M-2PQ8-XXXX-XXXX"]
}
Direct-code orders also carry fulfillmentStatus, fulfilledQuantity, errorCode and resolution (see above). paymentUrl is returned only on pending single-payment orders.
Order statuses:
pending: awaiting payment or payment confirmation; no code inintentmode at this point.paid: payment confirmed; for direct-code orders also checkfulfillmentStatus, whereneeds_attentionmeans manual handling.delivered: codes issued and ready to deliver downstream.expired: the payment window passed without confirmation; order again with a newexternalOrderId.refunded: refunded / reversed.
PUT /webhook — configure notifications
Only public HTTPS URLs are accepted; an empty string clears the webhook. A successful configuration returns a one-time secret (reconfiguring rotates it). The platform persists events first, then delivers immediately; the receiver must return 2xx within 5 seconds, otherwise delivery is retried with intervals starting at 30 seconds, doubling each time and capped at 6 hours, up to 10 attempts. Redirects are not followed and a 3xx counts as a failed delivery, so register the final URL (including details such as a trailing slash). X-AIBulk-Timestamp is the event creation time and can be used to reject stale replays.
Delivery body example:
{
"eventId": "evt_...",
"event": "order.delivered",
"timestamp": 1787500000,
"orderId": "...",
"externalOrderId": "your-order-123",
"codes": ["..."],
"redeemUrl": "https://redeemhub.uk"
}
Request headers:
X-AIBulk-Signature: sha256=<HMAC-SHA256(secret, rawBody)>X-AIBulk-Event-IdX-AIBulk-Delivery-IdX-AIBulk-Timestamp
Receivers should be idempotent on eventId and return 2xx after successful processing.
POST /webhook/test: creates and delivers a persisted test event.GET /webhook/deliveries?limit=50&cursor=...: delivery history.POST /webhook/deliveries/:id/retry: manually retry a failed or cancelled delivery.
Current events: order.delivered, order.delivery_attention, topup.credited, redemption.fulfilled, redemption.failed and webhook.test.
order.delivered means all customer codes are ready. For redemption products it does not mean the end user has completed the subscription redemption; redemption progress is checked on the redemption site at redeemUrl, or tracked by the downstream through the redemption endpoints / redemption.* events below. direct_code products deliver actual gift card codes and need no platform redemption.
POST /redemptions — server-side redemption on behalf of customers
For downstreams that buy codes and then redeem for end users themselves (for example, the buyer pays and submits a Claude Organization ID, and the downstream system redeems directly). It is the same validation and state machine as the redemption site's submit; the only differences:
- Authenticated with the same Bearer API key and rate-limited per key (60 submissions per minute; queries share the 300 per minute limit), not per egress IP. Server-side downstreams sharing an egress IP with other tenants do not crowd each other out.
- Only codes issued by orders of the user who owns this key are accepted; any other code returns
404 invalid_codewithout distinguishing "does not exist" from "not yours".direct_codegift card orders need no redemption and their codes are outside this endpoint's scope.
The redeemInfo keys come from the product's redeemFields[].key:
{ "code": "AB4K-9XQ2-XXXX-XXXX", "redeemInfo": { "account_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" } }
{ "redemptionId": "6f3c…", "status": "pending", "code": "AB4K-9XQ2-XXXX-XXXX", "orderId": "…" }
Fulfilment is asynchronous: afterwards poll GET /redemptions/:redemptionId, or receive redemption.fulfilled / redemption.failed. Error codes match the redemption site:
| HTTP | error |
Meaning |
|---|---|---|
| 400 | missing_field |
A required field from redeemFields is empty |
| 400 | invalid_account_id |
The Claude Organization ID is not a valid UUID |
| 400 | invalid_session_json / invalid_telegram_username |
The product's details are malformed |
| 400 | test_code_only |
Codes of the test product cannot be redeemed |
| 404 | invalid_code |
The code is invalid, voided, or not from this account's orders |
| 409 | already_redeemed |
Already processing or completed; cannot be resubmitted |
| 409 | retry_conflict |
The previous failed attempt still holds supplier resources; check progress / retry later |
| 410 | code_expired |
The code has expired |
| 503 | session_validation_unavailable |
The ChatGPT session pre-check is temporarily unavailable; retryable |
GET /redemptions/:redemptionId, GET /redemptions?code=… — redemption progress
{
"redemptionId": "6f3c…",
"code": "AB4K-9XQ2-XXXX-XXXX",
"orderId": "…",
"externalOrderId": "your-order-123",
"productId": "prod_claude_pro",
"status": "fulfilled",
"retryable": false,
"failureCode": null,
"deliverable": "充值完成:ab***@example.com",
"createdAt": 1787500000,
"fulfilledAt": 1787500600
}
status:not_redeemed(the code has not been submitted,redemptionId=null; only appears on?code=queries) →pending→processing→fulfilled|failed.- When
failed,retryable=trueand the same code can bePOST /redemptionsagain with correctedredeemInfo;failureCodeisfulfillment_failed(automatic fulfilment did not complete) orrejected_by_operator(rejected by manual review). deliverableis returned only whenfulfilledand is a masked completion note (for example a partially masked account); it never contains raw data other than card codes.- While processing, the server refreshes from upstream at most once every 60 seconds per redemption, so faster polling gains nothing; poll every 30–60 seconds, or rely on webhooks with polling as the fallback.
- Querying a
redemptionIdthat does not belong to this account returns404 not_found.
redemption.fulfilled / redemption.failed use the same signature, retry and eventId deduplication rules as order.delivered; each redemption emits once every time it reaches a terminal state (a resubmission after failure starts a new attempt and emits again, with a different eventId, when it reaches a terminal state). They are sent only to accounts whose codes came from wholesale API orders, never for retail web orders or gift card orders. The payload contains neither the deliverable nor the submitted details:
{
"eventId": "redemption:6f3c…:0:redemption.failed",
"event": "redemption.failed",
"timestamp": 1787500000,
"orderId": "…",
"externalOrderId": "your-order-123",
"productId": "prod_claude_pro",
"redemptionId": "6f3c…",
"code": "AB4K-9XQ2-XXXX-XXXX",
"status": "failed",
"failureCode": "fulfillment_failed",
"retryable": true,
"fulfilledAt": null
}
For redemption.fulfilled, status is fulfilled, failureCode is null, retryable is false and fulfilledAt is the completion time. Events may arrive out of order or later than a polling result; on receipt, treat GET /redemptions/:redemptionId as the source of truth.
GET /transactions — balance ledger
Supports limit, cursor and type=topup|purchase|refund|adjust:
{
"transactions": [{ "type": "purchase", "amount": "-1078.00", "balanceAfter": "172.00", "order_id": "..." }],
"nextCursor": null,
"hasMore": false
}
Error codes
| HTTP | error |
Where | Handling |
|---|---|---|---|
| 401 | unauthorized |
All endpoints | Key missing or invalid; regenerate it in the account center |
| 400 | invalid_input |
All endpoints | Invalid parameters; see message |
| 400 | invalid_cursor |
List endpoints | The cursor is invalid; start again from the first page |
| 404 | not_found |
Products, orders, redemptions | The ID does not exist or does not belong to this account |
| 402 | insufficient_balance |
POST /orders in balance mode |
Top up or reduce the quantity |
| 409 | out_of_stock / insufficient_stock / stock_unavailable / fulfillment_unavailable |
POST /orders |
Nothing was debited; reduce the quantity or retry later |
| 409 | price_changed |
POST /orders |
Reconfirm with details.actualUnitPrice, then order again |
| 409 | duplicate_order |
POST /orders |
Concurrent collision; look up the order by externalOrderId |
| 409 | idempotency_conflict |
POST /orders, POST /topup |
The idempotency key was used with different parameters; use a new business ID |
| 400 | api_test_quantity / api_test_balance_only |
Test product | The test product supports only REST, balance payment and qty=1 |
| 404 | invalid_code |
/redemptions |
The code is invalid, voided or not from this account |
| 400 | missing_field / invalid_account_id / invalid_session_json / invalid_telegram_username |
POST /redemptions |
Fix the details according to redeemFields |
| 409 | already_redeemed / retry_conflict |
POST /redemptions |
Switch to polling; for retry_conflict, resubmit later |
| 410 | code_expired |
POST /redemptions |
The code has expired (API codes have no expiry, so this is rare) |
| 409 | webhook_not_configured |
POST /webhook/test |
Configure the webhook first |
| 429 | rate_limited |
All endpoints | Back off according to Retry-After |
| 429 | too_many_pending_topups |
POST /topup |
5 unpaid top-ups already exist; pay or wait about 15 minutes for them to expire |
| 502 / 504 | payment_provider_error / payment_provider_timeout |
intent orders, top-ups |
No order was left behind; retry with the same idempotency key |
| 503 | session_validation_unavailable |
POST /redemptions |
The ChatGPT session pre-check is temporarily unavailable; retryable |
| 500 | internal_error |
All endpoints | retryable: true; contact support with the requestId |
Retry guidance
- Order creation timeout, disconnect or 5xx: retry with the same
externalOrderId, the sameproductIdand the sameqty, or recover the original order withGET /orders?externalOrderId=.... duplicate_order: usually a concurrent retry collision; queryGET /orders?externalOrderId=....idempotency_conflict: the same idempotency ID was used with a different product, quantity or amount; stop retrying and use a new business ID.price_changed: show or confirm the latestdetails.actualUnitPrice, then proceed with a new business decision.rate_limited: back off according toRetry-After.retry_conflicton server-side redemption: do not buy a new code; queryGET /redemptions?code=…after 30–60 seconds and resubmit oncestatus=failedandretryable=true.
Redemption site delivery notes
The following applies to platform codes with deliveryType=redemption; send both the code and redeemUrl to the end user. direct_code gift cards deliver the actual card code and skip these platform redemption steps:
- The end user opens
redeemUrl. - Enters or pastes the code.
- Submits the redemption details the product requires, as prompted on the page.
- For automatically fulfilled products, processing may take a few minutes after redemption; the end user can check the status on the redemption site.
The redemption site is a neutral-brand domain suited to resale scenarios. Never deliver only the code without redeemUrl.
If the downstream collects the details itself and redeems on behalf of end users, use POST /redemptions and GET /redemptions/:redemptionId above instead of the redemption site's public endpoints (those are rate-limited per client IP and will be throttled quickly when a server egress IP is shared with other tenants).
MCP
https://api.aibulk.uk/mcp uses the same Bearer key. Tools include list_products, check_availability, get_balance, create_topup, purchase_codes, get_order, list_orders and list_transactions.
Grok redemption account
Super Grok and Grok Heavy redemptions require the full Session JSON, or the UUID from its session.userId. Sign in to Grok and open Grok Session to get it; do not use sessionId, xUserId or an email address as the top-up account.
When using the redemption API / MCP, call check first and use the returned redeemFields. The grok_user_id field accepts a UUID string, or the full JSON string containing status: "authenticated" and session.userId. The page shows the recognised userId; confirm the target account before submitting.
The input format is validated on both client and server, and the server keeps only the normalised userId. A malformed input returns 400 before the code is consumed, and can be retried after correction. This check does not prove the account's live login state or top-up eligibility; a successful submission means the redemption request was accepted, not that the subscription has been applied.