AAstroDeveloper API v1 Get API access
Phase 2 · Sandbox open

Astro Developer API

Give each of your users their own crypto wallet, get notified the moment they receive crypto, sell crypto for naira, swap coins, send to outside wallets and sell gift cards — through one REST API.

Wallets per user

Create users with your own reference and give them deposit addresses on supported coins and networks.

Signed webhooks

Receive a deposit.received event, signed with your secret, when one of your users is paid.

Live rates

Crypto USD prices and gift card rates per $1 in naira and cedis.

Live base URLhttps://api.astroafrica.site/v1/partner
Sandbox base URLSent to you with your test key

Trading is open in the sandbox. Selling, swaps, withdrawals and gift card sales work with test keys now. They are switched on for live accounts at launch, after your business is approved.

Getting access

  1. Request a sandbox account. Email [email protected] with your business name and a technical contact. You get a test key (ak_test_…) and the sandbox address.
  2. Build against the sandbox. Sandbox wallets are not real and never hold real funds. Use sandbox deposits to send yourself pretend deposits and test your webhook handling end to end.
  3. Complete the business check (KYB). Company registration (the CAC number in Nigeria) and an identity check for each director: BVN, ID selfie and address.
  4. Go live. Once your business is approved, you receive a live key (ak_live_…) for the live base URL.

Quickstart

Three calls take you from a new user to an address they can be paid on.

# 1. Create a user with your own reference
curl -X POST "$ASTRO_BASE/users" \
  -H "Authorization: Bearer $ASTRO_KEY" -H "Content-Type: application/json" \
  -d '{"reference":"cust_8812","first_name":"Ada","last_name":"Okafor"}'

# 2. Ask for a USDT (TRC20) deposit address
curl -X POST "$ASTRO_BASE/users/ref:cust_8812/addresses" \
  -H "Authorization: Bearer $ASTRO_KEY" -H "Content-Type: application/json" \
  -d '{"currency":"usdt","network":"trc20"}'

# 3. Read it until status is "ready"
curl "$ASTRO_BASE/users/ref:cust_8812/addresses/usdt" \
  -H "Authorization: Bearer $ASTRO_KEY"
const base = process.env.ASTRO_BASE;
const headers = { Authorization: `Bearer ${process.env.ASTRO_KEY}`, "Content-Type": "application/json" };
const call = async (method, path, body) => {
  const res = await fetch(base + path, { method, headers, body: body && JSON.stringify(body) });
  const json = await res.json();
  if (!res.ok) throw new Error(`${json.error.code}: ${json.error.message}`);
  return json.data;
};

await call("POST", "/users", { reference: "cust_8812", first_name: "Ada", last_name: "Okafor" });
await call("POST", "/users/ref:cust_8812/addresses", { currency: "usdt", network: "trc20" });

let addr;
do {
  await new Promise((r) => setTimeout(r, 3000));
  [addr] = await call("GET", "/users/ref:cust_8812/addresses/usdt");
} while (addr?.status !== "ready");
console.log(addr.address);
import os, time, requests

BASE = os.environ["ASTRO_BASE"]
S = requests.Session()
S.headers["Authorization"] = f"Bearer {os.environ['ASTRO_KEY']}"

def call(method, path, body=None):
    r = S.request(method, BASE + path, json=body, timeout=20)
    data = r.json()
    if not r.ok:
        raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
    return data["data"]

call("POST", "/users", {"reference": "cust_8812", "first_name": "Ada", "last_name": "Okafor"})
call("POST", "/users/ref:cust_8812/addresses", {"currency": "usdt", "network": "trc20"})

while True:
    time.sleep(3)
    addr = call("GET", "/users/ref:cust_8812/addresses/usdt")[0]
    if addr["status"] == "ready":
        print(addr["address"]); break

Authentication

Send your key as a bearer token on every request.

Authorization: Bearer ak_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  • Keys are shown once, when they are created. Astro stores only a fingerprint and cannot recover a key; a lost key is revoked and replaced.
  • Test keys work only on the sandbox and live keys only on live. A key used in the wrong place returns wrong_environment.
  • You can hold several keys (for example, one per server). A revoked key stops working immediately.
  • Keys can be restricted to a list of your server IP addresses.
  • Keep keys on your servers. Never ship a key in a mobile app, a web page or a public repository.

Requests and responses

Send JSON with Content-Type: application/json (bodies up to 50 KB). Successful responses are wrapped in data. Errors are wrapped in error, with a stable code to program against and a human-readable message.

{ "data": { "id": 401, "reference": "cust_8812" } }

{ "error": { "code": "user_not_found", "message": "User not found." } }

Every response includes X-Astro-Mode: test or live, showing which kind of key made the call. Amounts are always strings, so no precision is lost.

Account

GET/me

Confirms your key works and returns your partner account.

FieldDescription
statuspending, active or suspended.
modeThe kind of key used: test or live.
environmentsandbox or live.
sandbox_walletstrue when sandbox deposits are available (sandbox only).
webhook_urlWhere events are sent, or null.
{
  "data": {
    "id": 12,
    "name": "Acme Pay",
    "country": "NG",
    "status": "active",
    "mode": "live",
    "environment": "live",
    "webhook_url": "https://acme.example/astro/webhooks"
  }
}

Users

A user is one of your customers. You identify each user with your own reference, and Astro returns an id. Wherever a path takes :id, you can pass the Astro id or ref:<your reference>.

POST/users

Creates a user and sets up their wallet.

FieldTypeDescription
reference requiredstringYour ID for this user: 1–64 letters, digits, . - or _. Unique in your account.
first_name requiredstringAt least 2 characters.
last_name requiredstringAt least 2 characters.
emailstringThe user's email, if you have it.

Safe to retry. If the wallet can't be set up right away you get 502 wallet_provider_error; send the same request again with the same reference to finish. A reference that is already fully set up returns 409 user_exists.

curl -X POST "$ASTRO_BASE/users" \
  -H "Authorization: Bearer $ASTRO_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "reference": "cust_8812",
    "first_name": "Ada",
    "last_name": "Okafor",
    "email": "[email protected]"
  }'
{
  "data": {
    "id": 401,
    "reference": "cust_8812",
    "email": "[email protected]",
    "first_name": "Ada",
    "last_name": "Okafor",
    "status": "active",
    "kyc_status": "none",
    "created_at": "2026-11-09T19:02:11.000Z"
  }
}
GET/users/:id

Returns one user in the same shape as above. GET /users/ref:cust_8812 also works.

curl "$ASTRO_BASE/users/ref:cust_8812" \
  -H "Authorization: Bearer $ASTRO_KEY"
GET/users

Lists your users, newest first.

QueryDescription
pagePage number, from 1.
per_page1–100. Default 50.
{
  "data": {
    "users": [ { "id": 401, "reference": "cust_8812", "status": "active" } ],
    "page": 1,
    "per_page": 50,
    "total": 1
  }
}

Deposit addresses

POST/users/:id/addresses

Creates the user's deposit address for a coin on a network. If the user already has an address on that network, it is returned instead.

FieldTypeDescription
currency requiredstringCoin code, for example usdt or btc.
networkstringRequired for coins with more than one network. Valid names come from GET /rates.

A new address often starts as pending with address: null. Read it every few seconds until status is ready, then show it to your user.

curl -X POST "$ASTRO_BASE/users/ref:cust_8812/addresses" \
  -H "Authorization: Bearer $ASTRO_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "currency": "usdt", "network": "trc20" }'
{
  "data": {
    "id": "a1b2c3",
    "currency": "usdt",
    "network": "trc20",
    "address": null,
    "destination_tag": null,
    "status": "pending"
  }
}
GET/users/:id/addresses/:currency

Returns all of the user's addresses for one coin, as a list in the shape above.

Tell your users to send only the matching coin on the matching network. Crypto sent on the wrong network, or as a different coin, can't be recovered. When an address has a destination_tag, it must be included.

{
  "data": [
    {
      "id": "a1b2c3",
      "currency": "usdt",
      "network": "trc20",
      "address": "TQ5x…9vKp",
      "destination_tag": null,
      "status": "ready"
    }
  ]
}

Balances

GET/users/:id/balances

Returns the user's crypto balances. locked is the part held for an action in progress.

{
  "data": [
    { "currency": "usdt", "balance": "125.50", "locked": "0.0" },
    { "currency": "btc", "balance": "0.0012", "locked": "0.0" }
  ]
}

Rates

GET/rates

Supported coins with their networks and USD price, plus every gift card rate that is live right now, per $1 of card value. Cards without a live rate are left out.

sell_ngn is Astro's naira rate for selling each coin, in bands by the sale's dollar value. Use entry_id to sell a gift card.

Rates move during the day. Read them fresh before showing a price to a user. The values in the example are illustrations, not quotes.

{
  "data": {
    "crypto": [
      {
        "currency": "usdt",
        "name": "Tether",
        "usd_price": 1,
        "networks": ["trc20", "bep20", "erc20"],
        "sell_ngn": [
          { "min_usd": 1, "max_usd": null, "ngn_per_usd": 1360 }
        ]
      }
    ],
    "giftcards": [
      {
        "entry_id": 214,
        "card": "Steam",
        "uid": "steam",
        "range": "$50–$100",
        "description": "USA, physical",
        "ngn_per_usd": 1000,
        "ghs_per_usd": null
      }
    ],
    "as_of": "2026-11-09T19:05:00.000Z"
  }
}

Sell to naira

Sell a user's crypto for naira at Astro's rate. The naira goes to your Astro naira balance; you then pay your user in your own app. Naira only. There is no fee: the price is the sell_ngn rate.

POST/users/:id/sell/quote

Body: currency, amount (in the coin). Returns a quote that holds its price for 30 seconds.

POST/users/:id/sell

Body: quote_id, reference (your own id for this sale, unique). Each quote works once. Sending the same reference again returns the same sale, so retries are safe.

The sale is processing until the coin has moved, usually seconds, then completed. You get sell.completed or sell.failed.

{
  "data": {
    "id": 5012,
    "type": "sell",
    "status": "quoted",
    "reference": null,
    "user": { "id": 401, "reference": "cust_8812" },
    "from": { "currency": "usdt", "amount": "10" },
    "to": { "currency": "ngn", "amount": "13600" },
    "fee": "0",
    "rate": { "ngn_per_usd": 1360, "usd_value": "10.00" },
    "expires_at": "2026-11-09T19:20:30.000Z"
  }
}

Swaps

Swap one coin for another inside the user's wallet. Astro's fee is 0.5% of the amount, taken in the coin being swapped. The smallest swap is $1 worth.

POST/users/:id/swap/quote

Body: from_currency, to_currency, amount. The quote shows the fee and what the user receives, and lasts 30 seconds.

POST/users/:id/swap

Body: quote_id, reference. If the price has moved more than 1% against the user since the quote, the swap is refused with price_changed and nothing moves. You get swap.completed.

{
  "data": {
    "id": 5013,
    "type": "swap",
    "status": "completed",
    "reference": "swap_77",
    "from": { "currency": "btc", "amount": "0.005" },
    "to": { "currency": "usdt", "amount": "372.28" },
    "fee": "0.000025",
    "fee_currency": "btc",
    "price": "74830.5"
  }
}

Withdrawals

Send crypto from a user's wallet to an outside address. Astro charges only the network fee, which comes from the user's balance on top of the amount.

GET/withdrawal-fee?currency=usdt&network=trc20&amount=5

The network fee and the total that will leave the balance.

POST/users/:id/withdrawals

Body: currency, network, address, amount, reference, and memo for XRP and XLM when the receiving wallet needs one. The address is checked against the network before anything is sent. You get withdrawal.completed (with txid) or withdrawal.failed.

Crypto sent on the wrong network, or to a wrong address, can't be recovered. Show your user the network clearly before they confirm.

{
  "data": {
    "id": 5014,
    "type": "withdraw",
    "status": "processing",
    "reference": "wd_3391",
    "from": { "currency": "usdt", "amount": "5" },
    "to": { "currency": "usdt", "amount": "5" },
    "network": "trc20",
    "address": "TJCn…sxMW",
    "memo": null,
    "network_fee": "1",
    "txid": null
  }
}

Gift cards

Submit a user's gift card for Astro to review. Approved cards are paid in naira to your Astro naira balance at the card's rate on GET /rates, fixed when you submit.

POST/users/:id/giftcards

Body: entry_id (from giftcards on GET /rates), amount (the card's value in US dollars), images (1 to 6 https links to clear photos of the card, and the receipt if there is one), reference, and an optional note.

Our team reviews each card. You get giftcard.approved with the naira paid, or giftcard.declined with the reason. A card can be approved for less than its stated value if part of it is already used. A submitted card can't be cancelled.

{
  "data": {
    "id": 5015,
    "type": "giftcard",
    "status": "processing",
    "reference": "gc_120",
    "from": { "currency": "usd", "amount": "100" },
    "to": { "currency": "ngn", "amount": "100000", "amount_is_estimate": true },
    "card": "Steam",
    "card_range": "$50–$100",
    "ngn_per_usd": 1000
  }
}

Trades and balance

GET/trades/:id
GET/trades?user_id=ref:cust_8812&type=sell&page=1

One trade, or a paged list of sells, swaps, withdrawals, gift card sales and payouts (type is sell, swap, withdraw, giftcard or payout). status is processing, completed or failed. A failed trade has a failure with a code and message.

GET/balance

Your Astro naira balance: sales and approved gift cards add to it. Astro pays it to your company bank account. Each payout appears as a payout trade with the bank transfer reference, and you get a payout.completed webhook.

{ "data": { "currency": "ngn", "balance": "13600.00" } }

Webhooks

Astro sends events as POST requests with a JSON body to an https URL on a public address. Respond with any 2xx within 10 seconds. Redirects are not followed.

EventSent when
deposit.receivedCrypto arrives at one of your users' addresses.
deposit.on_holdA deposit is held for review and is not in the balance yet. If ID is needed, Astro tells you what to collect from your user.
deposit.rejectedA deposit was refused and is not in the balance. Contact Astro support with the deposit id.
user.address_readyA new deposit address is ready to show, so you don't need to poll.
sell.completed, sell.failedA sale finished. Completed sales are in your naira balance.
swap.completedA swap finished.
withdrawal.completed, withdrawal.failedA withdrawal was sent (with txid), or didn't go out and nothing was taken.
giftcard.approved, giftcard.declinedOur team finished reviewing a gift card.
payout.completedAstro paid naira from your balance to your company bank account.
webhook.testYou call POST /webhook/test.
POST /astro/webhooks
Content-Type: application/json
X-Astro-Event: deposit.received
X-Astro-Signature: t=1794250000,v1=5f2c…

{
  "id": "evt_9031",
  "created_at": "2026-11-09T19:10:42.000Z",
  "event": "deposit.received",
  "data": {
    "user": { "id": 401, "reference": "cust_8812" },
    "deposit": {
      "id": "dep_77a1",
      "currency": "btc",
      "amount": "0.02",
      "network": "bitcoin",
      "txid": "8f3e…",
      "status": "accepted",
      "fee": "0.0001",
      "fee_percent": 0.5,
      "credited_amount": "0.0199",
      "fee_status": "collected"
    }
  }
}
Deposit fieldMeaning
amountWhat arrived on the blockchain.
fee, fee_percentAstro's deposit fee. "0" on USDT and USDC.
credited_amountWhat was added to the user's balance. Credit your user with this figure.
fee_statusnone (no fee), collected, or not_collected (we couldn't take it, so the user keeps the full amount).
BehaviourDetails
RetriesWithout a 2xx, we retry after 30 seconds, 2 minutes, 10 minutes and 45 minutes: 5 attempts in about an hour.
DuplicatesEach deposit or trade makes one event, but you may receive it more than once. Use the event id to ignore repeats.
Trade eventsdata.trade is the same object GET /trades/:id returns.
OrderingEvents can arrive out of order. Use created_at.
POST/webhook/test

Sends a webhook.test event to your URL, so you can check your signature code end to end.

{ "data": { "queued": true, "event_id": "evt_9032" } }

Verifying signatures

Every event is signed with your webhook secret (whsec_…, shown once when it is created). The X-Astro-Signature header is t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of t + "." + raw body.

  • Use the raw request body exactly as received, not re-encoded JSON.
  • Compare in constant time.
  • Reject signatures older than 5 minutes.
const crypto = require("crypto");

function verifyAstro(secret, rawBody, header, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec || !parts.v1) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  const got = Buffer.from(parts.v1, "hex");
  return got.length === expected.length && crypto.timingSafeEqual(got, expected);
}

// Express: keep the raw body for this route
app.post("/astro/webhooks", express.raw({ type: "application/json" }), (req, res) => {
  const ok = verifyAstro(process.env.ASTRO_WEBHOOK_SECRET, req.body.toString("utf8"),
    req.get("X-Astro-Signature") || "");
  if (!ok) return res.sendStatus(400);
  const event = JSON.parse(req.body);
  // Skip if event.id was already handled, then do the work.
  res.sendStatus(200);
});
import hashlib, hmac, time

def verify_astro(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    try:
        t = int(parts["t"])
    except (KeyError, ValueError):
        return False
    if abs(time.time() - t) > tolerance or "v1" not in parts:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])
function verify_astro(string $secret, string $rawBody, string $header, int $tolerance = 300): bool {
    $parts = [];
    foreach (explode(",", $header) as $p) {
        [$k, $v] = array_pad(explode("=", $p, 2), 2, null);
        $parts[trim($k)] = trim((string) $v);
    }
    $t = (int) ($parts["t"] ?? 0);
    if (!$t || abs(time() - $t) > $tolerance || empty($parts["v1"])) return false;
    $expected = hash_hmac("sha256", $t . "." . $rawBody, $secret);
    return hash_equals($expected, $parts["v1"]);
}

// $rawBody = file_get_contents("php://input");

Sandbox deposits

On the sandbox, every user's wallet is pretend. Addresses look like SANDBOX-USDT-TRC20-… and cannot receive real crypto. Never send real funds to them. To test your deposit handling, send the user a pretend deposit: the balance goes up and you receive a signed deposit.received webhook, exactly as on live.

POST/users/:id/sandbox/deposits

Sandbox only. On live this endpoint returns 404 not_found. Make an address for the coin and network first.

FieldDescription
currencyFor example usdt.
networkThe network of an address the user already has, for example trc20.
amountA string above 0 with at most 8 decimals, for example "25.5".
curl -X POST "$ASTRO_BASE/users/ref:cust_8812/sandbox/deposits" \
  -H "Authorization: Bearer $ASTRO_KEY" \
  -H "Content-Type: application/json" \
  -d '{"currency":"usdt","network":"trc20","amount":"25.5"}'
{
  "data": {
    "id": "sbxdep_cba44de2dfe1909c",
    "currency": "usdt",
    "amount": "25.5",
    "network": "trc20",
    "txid": "sandbox-9f2c…",
    "status": "accepted",
    "fee": "0",
    "fee_percent": 0,
    "credited_amount": "25.5",
    "fee_status": "none",
    "address": "SANDBOX-USDT-TRC20-6c46c20e1812a6ed7dfb"
  }
}

Fees

ActionFee
Creating users and addresses, rates, webhooksFree
USDT and USDC depositsFree
Deposits of other coins (BTC, ETH, SOL and others)0.5% of the deposit, taken when it arrives. Your agreement with Astro may set a different rate.
Selling crypto for nairaNo fee. You get Astro's naira rate (sell_ngn).
Swaps0.5% of the amount swapped
Withdrawals to outside walletsThe network fee only
Gift cardsNo fee. You get the card's naira rate.

The sandbox applies the same fees, so you can test your accounting. Sandbox network fees are made up.

Error codes

HTTPCodeMeaning and fix
400invalid_reference
invalid_name
invalid_email
A field is missing or malformed. The message names it.
400invalid_idUse the Astro id or ref:<reference>.
400invalid_currency
unsupported_currency
invalid_network
Check GET /rates for supported coins and networks.
400no_webhook_urlAsk Astro to set your webhook URL first.
400invalid_amountAmounts must be above 0 with at most 8 decimals (gift cards: $1 to $10,000).
400amount_out_of_range
amount_too_small
Below the smallest sale or swap. Check sell_ngn on GET /rates.
400invalid_address
invalid_memo
The address doesn't fit the network, or the memo is malformed. Nothing was sent.
400invalid_quote
invalid_card
invalid_images
image_unreadable
A trade field is missing or unusable. The message says which.
400card_unavailableThis gift card isn't accepted right now.
400invalid_jsonThe request body is not valid JSON.
401invalid_keyThe key is missing, mistyped or revoked.
401wrong_environmentA test key on live, or a live key on the sandbox.
403partner_not_approvedLive keys work once your business is approved.
403partner_suspendedYour account is paused. Contact Astro.
403ip_not_allowedThe request came from an IP address that isn't on your list.
404user_not_found
quote_not_found
trade_not_found
not_found
Not found in your account, or no such endpoint.
409user_existsThat reference is already set up. Use GET /users/ref:….
409user_busyAnother request for this user is in progress. Retry shortly.
409insufficient_balanceThe user's balance doesn't cover the trade (and the network fee, for withdrawals).
409quote_used
price_changed
Get a new quote.
409duplicate_referenceThat reference belongs to a different trade.
409sell_unavailableSelling this coin for naira is paused for now.
410quote_expiredQuotes last 30 seconds. Get a new one.
409no_addressSandbox only: make an address for that coin and network before sending a pretend deposit.
409user_not_readyThe user's wallet isn't set up yet. Call POST /users again with the same reference.
413body_too_largeThe request body is over 50 KB.
429rate_limitedToo many requests. Wait for the time in the RateLimit headers.
502wallet_provider_error
quote_unavailable
fee_unavailable
A temporary problem on our side. Retry with backoff; for trades, use the same reference.
500server_errorSomething went wrong. Retry, and contact us if it continues.
503unavailable
price_unavailable
Briefly unavailable. Retry with backoff.

Limits

LimitValue
Requests120 per minute per key, with standard RateLimit response headers
Request body50 KB
Allowed IP addressesUp to 20 per account
Webhook response time10 seconds
Test webhooks5 per minute per key
Sandbox deposits20 per minute per key
Trades (quotes, sells, swaps, withdrawals, gift cards)30 per minute per key; one at a time per user
Quotes30 seconds

Coming next

The following are planned and not available yet. This page will be updated as each one is released.

FeatureWhat it adds
Payout requests PlannedAsk for a payout of your naira balance through the API or the dashboard, instead of by email.
Partner dashboard PlannedSign in to manage keys, webhooks, users, trades and payouts yourself, with a company wallet to hold, swap, sell and withdraw your business's own crypto.