Reseller API
Sell AuraStack products from your own shop, app or bot. Browse the catalog, get a quote, buy from your prepaid AuraStack balance, and receive the product credentials in the response.
Build it with your AI agent
Put your sandbox key in your project's environment as AURASTACK_API_KEY, then paste this prompt into Claude Code, Cursor, Codex or any agent that can read a web page. The agent is told never to use a live key unless you say so.
Integrate the AuraStack Reseller API into this project. 1. First read https://aurastack.link/llms.txt and follow the links it gives: the guide (https://aurastack.link/docs/api.md), the OpenAPI contract (https://aurastack.link/api/v1/openapi.json) and the agent skill (https://aurastack.link/skills/aurastack-reseller/SKILL.md). Follow the skill's rules. 2. Use only the sandbox key in the AURASTACK_API_KEY environment variable (it starts with sk_sandbox_). Never use or ask for a live key, and never place a live order, unless I say so. 3. Build: product list, quote before checkout, order creation with one Idempotency-Key per purchase, delivery of the credentials to the buyer, and handling of every order status and error. 4. Add end-to-end tests against the sandbox: call POST /sandbox/reset first, then cover each AuraStack-Sandbox-Outcome value. 5. Never log or print delivery.items or the API key. When you are done, tell me what you built, how to run the tests, and what I must do to go live.
The agent starts from https://aurastack.link/llms.txt, a short map written for agents. It links this guide as plain markdown (/docs/api.md), the OpenAPI contract (/api/v1/openapi.json) and the AuraStack skill. Building by hand? Start with the Quickstart, or try every endpoint in the API reference.
Claude Code: install the skill so the rules load in every session
mkdir -p ~/.claude/skills/aurastack-reseller && \ curl -fsSL https://aurastack.link/skills/aurastack-reseller/SKILL.md \ -o ~/.claude/skills/aurastack-reseller/SKILL.md
Quickstart
- Sign in and open your dashboard. In the Sandbox panel, click Create sandbox key. The key is shown once: store it in a secret manager.
- List products, quote the one you want, then create the order with the quoted unit price. Sandbox orders are never charged.
- Read the credentials from the order response, or poll the order until it is delivered.
- When your integration works, create a live key under Live API Keys, top up your balance, and switch the key.
Request
export AURASTACK_API_KEY=sk_sandbox_... curl https://aurastack.link/api/v1/balance \ -H "Authorization: Bearer $AURASTACK_API_KEY"
Response 200
{
"client_id": "...",
"available_balance": "1000.00",
"reserved_balance": "0.00",
"currency": "EUR",
"sandbox": true
}Base URL: https://aurastack.link/api/v1. Requests and responses are JSON. Amounts are strings with two decimals, in the currency of the response: euros for everything since October 2026 (older orders show USD and vat: null).
Authentication
Send your key as a bearer token on every request: Authorization: Bearer <key>. A missing or unknown key returns 401; do not retry it.
- Key
- Sandbox key
- Prefix
sk_sandbox_- What it does
- Same API, never charged, sandbox balance. One per account; roll it from the dashboard.
- Key
- Live key
- Prefix
sk_live_(older keyssk_aura_)- What it does
- Real orders, charged to your balance. Up to 10 per account.
| Key | Prefix | What it does |
|---|---|---|
| Sandbox key | sk_sandbox_ | Same API, never charged, sandbox balance. One per account; roll it from the dashboard. |
| Live key | sk_live_ (older keys sk_aura_) | Real orders, charged to your balance. Up to 10 per account. |
Sandbox
A full copy of the live API that never costs money. Build your integration and run your end-to-end tests against it, then switch one key to go live.
- URL and endpoints
- Sandbox key
- Same
- Live key
- Same
- Products and quotes
- Sandbox key
- Catalog prices and stock (cached for about a minute: a live quote can differ by a few cents)
- Live key
- Live quote prices and stock
- Validation, VAT, idempotency
- Sandbox key
- Same rules as live
- Live key
- Same rules
- Balance
- Sandbox key
- Sandbox balance: starts at 1000.00 EUR, goes down with each order, refunds add back
- Live key
- Your real balance
- Orders
- Sandbox key
- Stored and readable, statuses change over time like live. Never charged. Credentials look like
SANDBOX-7F2A-... - Live key
- Charged. Real credentials
- Who sees the orders
- Sandbox key
- Your sandbox key only
- Live key
- Your live keys only
- Balance, quote and order responses
- Sandbox key
- Same shape, with
"sandbox": true - Live key
- Same shape, with
"sandbox": false
| Sandbox key | Live key | |
|---|---|---|
| URL and endpoints | Same | Same |
| Products and quotes | Catalog prices and stock (cached for about a minute: a live quote can differ by a few cents) | Live quote prices and stock |
| Validation, VAT, idempotency | Same rules as live | Same rules |
| Balance | Sandbox balance: starts at 1000.00 EUR, goes down with each order, refunds add back | Your real balance |
| Orders | Stored and readable, statuses change over time like live. Never charged. Credentials look like SANDBOX-7F2A-... | Charged. Real credentials |
| Who sees the orders | Your sandbox key only | Your live keys only |
| Balance, quote and order responses | Same shape, with "sandbox": true | Same shape, with "sandbox": false |
Sandbox writes (quotes, orders, resets) are limited to 120 per minute per account, and sandbox orders to 2000 per 24 hours (a reset does not lift it). A quantity outside the product's limits fails like live (500).
Force an outcome
A sandbox order is delivered at once, unless your sandbox balance is too low (then 402, like live). To test the other cases, send the AuraStack-Sandbox-Outcome header when you create the order. It is read on creation only (not on replays), and live keys ignore it.
- Header value
delivered(default)- What you get
- 200, status "delivered" with sandbox credentials
- What your code should do
- Hand the credentials to your customer
- Header value
processing- What you get
- 200, status "processing". About 30 seconds later the order is delivered
- What your code should do
- Poll GET /orders/{id} every 10 to 30 seconds
- Header value
on_hold- What you get
- 502 with the order, status "failed", failure_code "NEEDS_REVIEW". Charged to the sandbox balance
- What your code should do
- Tell your customer it is being checked. Do not refund yet
- Header value
refunded- What you get
- 502 with the order, status "refunded". The sandbox balance gets the amount back
- What your code should do
- Refund or re-order for your customer
- Header value
insufficient_balance- What you get
- 402 payment_error, whatever the balance
- What your code should do
- Top up, then order with a new Idempotency-Key
- Header value
price_changed- What you get
- 409 price_changed with the current unit_price
- What your code should do
- Quote again and confirm with your customer
- Header value
out_of_stock- What you get
- 409 invalid_request_error
- What your code should do
- Offer another product
| Header value | What you get | What your code should do |
|---|---|---|
delivered (default) | 200, status "delivered" with sandbox credentials | Hand the credentials to your customer |
processing | 200, status "processing". About 30 seconds later the order is delivered | Poll GET /orders/{id} every 10 to 30 seconds |
on_hold | 502 with the order, status "failed", failure_code "NEEDS_REVIEW". Charged to the sandbox balance | Tell your customer it is being checked. Do not refund yet |
refunded | 502 with the order, status "refunded". The sandbox balance gets the amount back | Refund or re-order for your customer |
insufficient_balance | 402 payment_error, whatever the balance | Top up, then order with a new Idempotency-Key |
price_changed | 409 price_changed with the current unit_price | Quote again and confirm with your customer |
out_of_stock | 409 invalid_request_error | Offer another product |
End-to-end tests
Put your sandbox key in your CI secrets and run your real code paths against the sandbox (it follows real stock: use a product your catalog shows in stock). Start each run with POST /sandbox/reset: it puts the sandbox balance back to 1000.00 EUR and clears your sandbox orders, so every run starts from the same state. Live keys get 403 on it.
e2e/aurastack.test.js (Node 20+, any test runner)
const API = 'https://aurastack.link/api/v1';
const H = { Authorization: `Bearer ${process.env.AURASTACK_SANDBOX_KEY}`, 'Content-Type': 'application/json' };
const call = (path, init = {}) => fetch(API + path, { ...init, headers: { ...H, ...init.headers } });
const quote = async () => (await call('/quotes', { method: 'POST',
body: JSON.stringify({ product_id: 'prd_IQCRJMV8ww5aozXi', quantity: 1 }) })).json();
const order = (q, headers) => call('/orders', { method: 'POST', headers,
body: JSON.stringify({ product_id: q.product_id, quantity: 1,
expected_unit_price: q.unit_price, billing: { country: 'FR' } }) });
beforeAll(() => call('/sandbox/reset', { method: 'POST' }));
test('a paid order is delivered and charged', async () => {
const res = await order(await quote(), { 'Idempotency-Key': 'e2e-1' });
const placed = await res.json();
expect(placed.status).toBe('delivered');
const balance = await (await call('/balance')).json();
expect(balance.available_balance).toBe((1000 - Number(placed.total_amount)).toFixed(2));
});
test('my shop handles an order on hold', async () => {
const res = await order(await quote(), { 'Idempotency-Key': 'e2e-2', 'AuraStack-Sandbox-Outcome': 'on_hold' });
expect(res.status).toBe(502);
expect((await res.json()).failure_code).toBe('NEEDS_REVIEW');
});Balance
GET/api/v1/balance
Your prepaid balance. available_balance is what orders can spend. reserved_balance is always "0.00": orders are charged immediately.
Products
GET/api/v1/products
The products you can buy, with your price. Query parameters: limit (1 to 100, default 100) and cursor. Pass next_cursor back as cursor to get the next page, until it is null. A page can hold fewer items than limit; keep following the cursor.
Response 200
{
"items": [
{
"id": "prd_8Kq2mZt4Wb7xLn3R",
"name": "Gemini Google One AI Pro 5TB, 18 months",
"description": "...",
"unit_price": "21.50",
"currency": "EUR",
"available_stock": 296,
"minimum_quantity": 1,
"maximum_quantity": 100,
"updated_at": "2026-10-06T11:31:03.131Z"
}
],
"next_cursor": "..."
}unit_price is per unit, in euros, before VAT. available_stock can be null when stock is not tracked. Ignore fields not listed here.
GET/api/v1/products/{id}
One product, same fields. 404 if the product is not sold here. An id that does not exist at all can return 500: check it against the list.
Quotes
POST/api/v1/quotes
The current price for a quantity. Quote right before ordering and send the quoted unit_price with the order.
Request
curl -X POST https://aurastack.link/api/v1/quotes \
-H "Authorization: Bearer $AURASTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "product_id": "prd_8Kq2mZt4Wb7xLn3R", "quantity": 2 }'Response 200
{
"product_id": "prd_8Kq2mZt4Wb7xLn3R",
"quantity": 2,
"unit_price": "21.50",
"total_amount": "43.00",
"currency": "EUR",
"available_stock": 296,
"can_purchase": true,
"quoted_at": "2026-10-06T12:00:00.000Z"
}Amounts are before VAT; VAT is added when you order (see VAT). If can_purchase is false, do not order. A quote does not reserve the price or the stock: the price is confirmed only when you order. Ignore fields not listed here.
Orders
POST/api/v1/orders
Buy a product with your balance. The price is always ours: expected_unit_price only confirms it. If it no longer matches, nothing is charged and you get 409 price_changed with the current unit_price. Send an Idempotency-Key with every order (see Idempotency and retries).
Request
curl -X POST https://aurastack.link/api/v1/orders \
-H "Authorization: Bearer $AURASTACK_API_KEY" \
-H "Idempotency-Key: my-order-1001" \
-H "Content-Type: application/json" \
-d '{
"product_id": "prd_8Kq2mZt4Wb7xLn3R",
"quantity": 2,
"expected_unit_price": "21.50",
"billing": { "country": "FR" }
}'Buying as a company: add "is_company": true, "company_name": "...", "vat_number": "..." to billing, with your real tax number (it is checked).
- Field
product_id- Required
- yes
- Description
- From the catalog.
- Field
quantity- Required
- yes
- Description
- Integer from 1 to 100, within the product's minimum_quantity and maximum_quantity.
- Field
expected_unit_price- Required
- yes
- Description
- The quote's unit_price (euros, before VAT).
- Field
billing.country- Required
- yes
- Description
- Your billing country, 2-letter ISO 3166-1 code such as FR or US.
- Field
billing.is_company- Required
- no
- Description
- JSON boolean true if you buy as a company (the string "true" counts as false).
- Field
billing.company_name- Required
- if company
- Description
- Your company name.
- Field
billing.vat_number- Required
- if company
- Description
- Your tax number. EU VAT numbers start with the billing country's prefix (for example FR12345678901, EL for Greece) and are checked with the EU register (VIES); others are not checked.
| Field | Required | Description |
|---|---|---|
product_id | yes | From the catalog. |
quantity | yes | Integer from 1 to 100, within the product's minimum_quantity and maximum_quantity. |
expected_unit_price | yes | The quote's unit_price (euros, before VAT). |
billing.country | yes | Your billing country, 2-letter ISO 3166-1 code such as FR or US. |
billing.is_company | no | JSON boolean true if you buy as a company (the string "true" counts as false). |
billing.company_name | if company | Your company name. |
billing.vat_number | if company | Your tax number. EU VAT numbers start with the billing country's prefix (for example FR12345678901, EL for Greece) and are checked with the EU register (VIES); others are not checked. |
The billing details are yours: you are our customer. total_amount is what your balance is charged, VAT included, and is the amount to rely on. total_amount minus vat.amount can differ from unit_price times quantity by a cent.
Response 200
{
"id": "api_3f9c2a71d0b84e6a5c1d7e02",
"product_id": "prd_8Kq2mZt4Wb7xLn3R",
"quantity": 2,
"status": "delivered",
"unit_price": "21.50",
"total_amount": "51.60",
"vat": { "rate": 20, "amount": "8.60", "country": "FR" },
"currency": "EUR",
"delivery": { "items": ["XXXX-XXXX-XXXX-XXXX", "YYYY-YYYY-YYYY-YYYY"] },
"failure_code": null,
"failure_message": null,
"created_at": "2026-10-06T12:00:01.000Z",
"delivered_at": "2026-10-06T12:00:03.000Z"
}Most orders are delivered in the response. If status is processing, poll the order (below) every 10 to 30 seconds until it is delivered. Deliver the items to your customer securely and never write them to logs.
GET/api/v1/orders/{id}
One of your orders, same shape as above. Reading a processing order also checks whether it has been delivered since.
GET /api/v1/orders/{id}.VAT
VAT is decided from your billing and, when your billing country is outside the EU, from the country your server calls us from. The order's vat shows what was applied.
- You are
- Billed outside the EU (Monaco counts as France), calling from a server outside the EU
- VAT
- None
- You are
- Billed outside the EU, calling from a server in the EU
- VAT
- 20% (a tax ID from outside the EU cannot be verified; contact us with proof to reclaim it)
- You are
- A company in another EU country with a VAT number confirmed by VIES
- VAT
- None (reverse charge)
- You are
- A company in France
- VAT
- 20%
- You are
- Anyone else billed in the EU
- VAT
- 20% French VAT (vat.country is FR)
| You are | VAT |
|---|---|
| Billed outside the EU (Monaco counts as France), calling from a server outside the EU | None |
| Billed outside the EU, calling from a server in the EU | 20% (a tax ID from outside the EU cannot be verified; contact us with proof to reclaim it) |
| A company in another EU country with a VAT number confirmed by VIES | None (reverse charge) |
| A company in France | 20% |
| Anyone else billed in the EU | 20% French VAT (vat.country is FR) |
If the EU register (VIES) is unavailable when you order, the order goes through with VAT; contact us to reclaim it. A VAT number the register rejects returns 400.
Order statuses
- status
pending_payment- Charged?
- Not yet
- What to do
- Payment did not complete. Retry the same request with the same Idempotency-Key.
- status
payment_captured- Charged?
- Yes
- What to do
- Delivery is starting. Poll the order.
- status
processing- Charged?
- Yes
- What to do
- Delivery in progress. Poll the order.
- status
delivered- Charged?
- Yes
- What to do
- Final. Read delivery.items.
- status
failedwithfailure_code: "INSUFFICIENT_BALANCE"- Charged?
- No
- What to do
- Final. Top up, then order again with a new Idempotency-Key.
- status
failed, any other code- Charged?
- Yes
- What to do
- Our team resolves it: it ends delivered or refunded. Do not refund your customer yet.
- status
refunded- Charged?
- Refunded
- What to do
- Final. The amount is back on your balance.
| status | Charged? | What to do |
|---|---|---|
pending_payment | Not yet | Payment did not complete. Retry the same request with the same Idempotency-Key. |
payment_captured | Yes | Delivery is starting. Poll the order. |
processing | Yes | Delivery in progress. Poll the order. |
delivered | Yes | Final. Read delivery.items. |
failed with failure_code: "INSUFFICIENT_BALANCE" | No | Final. Top up, then order again with a new Idempotency-Key. |
failed, any other code | Yes | Our team resolves it: it ends delivered or refunded. Do not refund your customer yet. |
refunded | Refunded | Final. The amount is back on your balance. |
Idempotency and retries
Send an Idempotency-Key header on every order: one unique key per customer purchase, for example your own order number. If the connection drops or you get a 503, retry the identical request with the same key. You are never charged twice: the retry returns the existing order. Without a key, a retry can create a second order.
Only product_id and quantity are compared: reusing a key with a different product or quantity returns 422 idempotency_error, and with any other change it returns the original order. A key whose order failed for insufficient balance keeps returning that result: use a new key after topping up.
Errors
Errors return { "error": { "message": "...", "type": "..." } }. Branch on the HTTP status and type.
- HTTP
- 400
- type
invalid_request_error- When
- Missing or invalid field, or billing rejected (for example a VAT number VIES does not confirm).
- What to do
- Fix the request.
- HTTP
- 401
- type
authentication_error- When
- Missing or invalid key.
- What to do
- Check the key. Do not retry.
- HTTP
- 402
- type
payment_error- When
- Balance too low. Nothing was charged.
- What to do
- Top up, order again with a new Idempotency-Key.
- HTTP
- 403
- type
invalid_request_error- When
- A live key called POST /sandbox/reset.
- What to do
- Use your sandbox key.
- HTTP
- 404
- type
invalid_request_error- When
- Product not sold here (products, quotes), or an order that is not yours.
- What to do
- Check the id.
- HTTP
- 409
- type
price_changed- When
- The price moved. error.unit_price has the new one. Nothing was charged.
- What to do
- Quote again and confirm with your customer.
- HTTP
- 409
- type
invalid_request_error- When
- Out of stock or unavailable. Nothing was charged.
- What to do
- Offer another product. Do not retry right away.
- HTTP
- 422
- type
idempotency_error- When
- Idempotency-Key reused for a different product or quantity.
- What to do
- Use a new key.
- HTTP
- 429
- type
rate_limit_error- When
- Company orders (is_company with a vat_number), counted per attempt, are limited to 30 per 10 minutes per server IP. Sandbox: 120 writes per minute, 2000 orders per 24 hours. Nothing was charged.
- What to do
- Wait, then retry with the same Idempotency-Key.
- HTTP
- 500
- type
api_errororinternal_error- When
- Unexpected error, or a product id or quantity the product does not accept.
- What to do
- Retry a few times with backoff and the same Idempotency-Key. If it persists, check product_id against the catalog.
- HTTP
- 502
- type
- the order
- When
- The order failed or was refunded. You may have been charged.
- What to do
- Read status and failure_code (see Order statuses).
- HTTP
- 503
- type
api_error- When
- Payment outcome not confirmed.
- What to do
- Retry with the same Idempotency-Key. You will not be charged twice.
| HTTP | type | When | What to do |
|---|---|---|---|
| 400 | invalid_request_error | Missing or invalid field, or billing rejected (for example a VAT number VIES does not confirm). | Fix the request. |
| 401 | authentication_error | Missing or invalid key. | Check the key. Do not retry. |
| 402 | payment_error | Balance too low. Nothing was charged. | Top up, order again with a new Idempotency-Key. |
| 403 | invalid_request_error | A live key called POST /sandbox/reset. | Use your sandbox key. |
| 404 | invalid_request_error | Product not sold here (products, quotes), or an order that is not yours. | Check the id. |
| 409 | price_changed | The price moved. error.unit_price has the new one. Nothing was charged. | Quote again and confirm with your customer. |
| 409 | invalid_request_error | Out of stock or unavailable. Nothing was charged. | Offer another product. Do not retry right away. |
| 422 | idempotency_error | Idempotency-Key reused for a different product or quantity. | Use a new key. |
| 429 | rate_limit_error | Company orders (is_company with a vat_number), counted per attempt, are limited to 30 per 10 minutes per server IP. Sandbox: 120 writes per minute, 2000 orders per 24 hours. Nothing was charged. | Wait, then retry with the same Idempotency-Key. |
| 500 | api_error or internal_error | Unexpected error, or a product id or quantity the product does not accept. | Retry a few times with backoff and the same Idempotency-Key. If it persists, check product_id against the catalog. |
| 502 | the order | The order failed or was refunded. You may have been charged. | Read status and failure_code (see Order statuses). |
| 503 | api_error | Payment outcome not confirmed. | Retry with the same Idempotency-Key. You will not be charged twice. |