AAstroDeveloper API v1 Get API access
Phase 1 · Sandbox open

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.

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

Phase 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

  1. Request a sandbox account. Email support@myastroafrica.com 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.
  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.
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": "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"
  }
}
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.

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.

EventSent when
deposit.receivedCrypto arrives at one of your users' addresses.
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": "usdt",
      "amount": "50.0",
      "network": "trc20",
      "txid": "0x…",
      "status": "accepted"
    }
  }
}
BehaviourDetails
RetriesWithout a 2xx, we retry after 30 seconds, 2 minutes, 10 minutes and 45 minutes: 5 attempts in about an hour.
DuplicatesEach deposit makes one event, but you may receive it more than once. Use the event id to ignore repeats.
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");

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.
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
not_found
No such user in your account, or no such endpoint.
409user_existsThat reference is already set up. Use GET /users/ref:….
409user_not_readyThe user's wallet isn't set up yet. Call POST /users again with the same reference.
429rate_limitedToo many requests. Wait for the time in the RateLimit headers.
502wallet_provider_errorA temporary problem on our side. Retry with backoff.
500server_errorSomething went wrong. Retry, and contact us if it continues.

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

Coming next

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

FeatureWhat it adds
User verification PlannedSend a user's BVN and ID selfie through the API. Required before selling or withdrawing.
Sell, swap and withdraw PlannedCrypto to naira or cedis with a quote first, crypto-to-crypto swaps, and sends to outside wallets.
Gift card sales PlannedSubmit a card for a user and receive giftcard.approved or giftcard.declined.
Address ready event Planneduser.address_ready, so you no longer need to poll new addresses.