Astro Developer API
Give each of your users their own crypto wallet, get notified the moment they receive crypto, and read Astro's live crypto prices and gift card rates — through one REST API.
Create users with your own reference and give them deposit addresses on supported coins and networks.
Receive a deposit.received event, signed with your secret, when one of your users is paid.
Crypto USD prices and gift card rates per $1 in naira and cedis.
https://api.astroafrica.site/v1/partnerSent to you with your test keyPhase 1 is read-and-receive only. No endpoint in this version moves money out of a wallet. Selling, swaps and withdrawals are listed under Coming next.
Getting access
- Request a sandbox account. Email
support@myastroafrica.comwith your business name and a technical contact. You get a test key (ak_test_…) and the sandbox address. - 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.
- Complete the business check (KYB). Company registration (the CAC number in Nigeria) and an identity check for each director: BVN, ID selfie and address.
- 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
Confirms your key works and returns your partner account.
| Field | Description |
|---|---|
status | pending, active or suspended. |
mode | The kind of key used: test or live. |
environment | sandbox or live. |
sandbox_wallets | true when sandbox deposits are available (sandbox only). |
webhook_url | Where 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>.
Creates a user and sets up their wallet.
| Field | Type | Description |
|---|---|---|
reference required | string | Your ID for this user: 1–64 letters, digits, . - or _. Unique in your account. |
first_name required | string | At least 2 characters. |
last_name required | string | At least 2 characters. |
email | string | The 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": "ada@example.com"
}'
{
"data": {
"id": 401,
"reference": "cust_8812",
"email": "ada@example.com",
"first_name": "Ada",
"last_name": "Okafor",
"status": "active",
"kyc_status": "none",
"created_at": "2026-11-09T19:02:11.000Z"
}
}
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"Lists your users, newest first.
| Query | Description |
|---|---|
page | Page number, from 1. |
per_page | 1–100. Default 50. |
{
"data": {
"users": [ { "id": 401, "reference": "cust_8812", "status": "active" } ],
"page": 1,
"per_page": 50,
"total": 1
}
}Deposit 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.
| Field | Type | Description |
|---|---|---|
currency required | string | Coin code, for example usdt or btc. |
network | string | Required 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"
}
}
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
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
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.
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"]
}
],
"giftcards": [
{
"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"
}
}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.
| Event | Sent when |
|---|---|
deposit.received | Crypto arrives at one of your users' addresses. |
webhook.test | You 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 field | Meaning |
|---|---|
amount | What arrived on the blockchain. |
fee, fee_percent | Astro's deposit fee. "0" on USDT and USDC. |
credited_amount | What was added to the user's balance. Credit your user with this figure. |
fee_status | none (no fee), collected, or not_collected (we couldn't take it, so the user keeps the full amount). |
| Behaviour | Details |
|---|---|
| Retries | Without a 2xx, we retry after 30 seconds, 2 minutes, 10 minutes and 45 minutes: 5 attempts in about an hour. |
| Duplicates | Each deposit makes one event, but you may receive it more than once. Use the event id to ignore repeats. |
| Ordering | Events can arrive out of order. Use created_at. |
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.
Sandbox only. On live this endpoint returns 404 not_found. Make an address for the coin and network first.
| Field | Description |
|---|---|
currency | For example usdt. |
network | The network of an address the user already has, for example trc20. |
amount | A 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
| Action | Fee |
|---|---|
| Creating users and addresses, rates, webhooks | Free |
| USDT and USDC deposits | Free |
| 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. |
Fees for selling, swapping and withdrawing will be listed here when those features are released. The sandbox applies the same deposit fee, so you can test your accounting.
Error codes
| HTTP | Code | Meaning and fix |
|---|---|---|
| 400 | invalid_referenceinvalid_nameinvalid_email | A field is missing or malformed. The message names it. |
| 400 | invalid_id | Use the Astro id or ref:<reference>. |
| 400 | invalid_currencyunsupported_currencyinvalid_network | Check GET /rates for supported coins and networks. |
| 400 | no_webhook_url | Ask Astro to set your webhook URL first. |
| 400 | invalid_amount | Sandbox deposit amount must be above 0 with at most 8 decimals. |
| 400 | invalid_json | The request body is not valid JSON. |
| 401 | invalid_key | The key is missing, mistyped or revoked. |
| 401 | wrong_environment | A test key on live, or a live key on the sandbox. |
| 403 | partner_not_approved | Live keys work once your business is approved. |
| 403 | partner_suspended | Your account is paused. Contact Astro. |
| 403 | ip_not_allowed | The request came from an IP address that isn't on your list. |
| 404 | user_not_foundnot_found | No such user in your account, or no such endpoint. |
| 409 | user_exists | That reference is already set up. Use GET /users/ref:…. |
| 409 | user_busy | This user is being set up by another request. Retry in a minute. |
| 409 | no_address | Sandbox only: make an address for that coin and network before sending a pretend deposit. |
| 409 | user_not_ready | The user's wallet isn't set up yet. Call POST /users again with the same reference. |
| 413 | body_too_large | The request body is over 50 KB. |
| 429 | rate_limited | Too many requests. Wait for the time in the RateLimit headers. |
| 502 | wallet_provider_error | A temporary problem on our side. Retry with backoff. |
| 500 | server_error | Something went wrong. Retry, and contact us if it continues. |
| 503 | unavailable | The API is briefly unavailable. Retry with backoff. |
Limits
| Limit | Value |
|---|---|
| Requests | 120 per minute per key, with standard RateLimit response headers |
| Request body | 50 KB |
| Allowed IP addresses | Up to 20 per account |
| Webhook response time | 10 seconds |
| Test webhooks | 5 per minute per key |
| Sandbox deposits | 20 per minute per key |
Coming next
The following are planned and not available yet. This page will be updated as each one is released.
| Feature | What it adds |
|---|---|
| User verification Planned | Send a user's BVN and ID selfie through the API. Required before selling or withdrawing. |
| Sell, swap and withdraw Planned | Crypto to naira with a quote first, crypto-to-crypto swaps, and sends to outside wallets. |
| Gift card sales Planned | Submit a card for a user and receive giftcard.approved or giftcard.declined. |
| Address ready event Planned | user.address_ready, so you no longer need to poll new addresses. |