Official SDKs
Talk to the Apa API from your backend with a typed client instead of raw HTTP. The SDKs wrap the developer payment API, retry transient failures, auto-paginate lists, and verify webhook signatures in constant time — funds still settle straight to your own wallets.
Install
Node.js is the primary, fully released SDK. A Python package is available in preview and mirrors the same surface with snake_case method names.
npm install @apa/node
# or: pnpm add @apa/node / yarn add @apa/nodepip install apa-appInitialize
Pass your secret key — sk_live_… in production; sk_test_… keys are reserved for test mode as it rolls out. It is sent as Authorization: Bearer <key> on every request. Keep secret keys server-side; never expose them in a browser or mobile app.
import { Apa } from "@apa/node";
const apa = new Apa(process.env.APA_API_KEY!); // "sk_live_…"from apa import Apa
apa = Apa("sk_live_…") # or Apa(os.environ["APA_API_KEY"])The Node client also accepts an options object for per-client configuration — the base URL (https://apa.app/v1 by default), request timeout, maxRetries, and a custom fetch.
const apa = new Apa({
apiKey: process.env.APA_API_KEY!,
baseUrl: "https://apa.app/v1", // default
timeout: 60_000, // ms per request (default 60s)
maxRetries: 2, // network errors, 429, 5xx (default 2)
});Create a payment link
Payment links are reusable hosted checkout URLs. The wire format is snake_case, so params and response fields match the raw API (payout_wallet_id, url). Amounts are decimal strings, never floats.
const link = await apa.paymentLinks.create({
payout_wallet_id: "pp_123",
amount: "25.00",
currency: "USD",
description: "Pro plan — monthly",
});
console.log(link.url); // -> hosted checkout URL to sharelink = apa.payment_links.create({
"payout_wallet_id": "pp_123",
"amount": "25.00",
"currency": "USD",
"description": "Pro plan — monthly",
})
print(link["url"]) # -> hosted checkout URL to shareCustomer-entered links and coupons
Payment links can let the customer choose the amount first, then optionally enter an active promotion code before Apa creates the immutable checkout session.
const tipLink = await apa.paymentLinks.create({
payout_wallet_id: "pp_123",
amount_mode: "customer_entered",
min_amount: "5.00",
suggested_amounts: ["10.00", "25.00", "50.00"],
allow_promotion_codes: true,
description: "Tips",
});
const code = await apa.coupons.create({
code: "WELCOME10",
discount_type: "percent",
percent_off: 10,
minimum_amount: "10.00",
});tip_link = apa.payment_links.create({
"payout_wallet_id": "pp_123",
"amount_mode": "customer_entered",
"min_amount": "5.00",
"suggested_amounts": ["10.00", "25.00", "50.00"],
"allow_promotion_codes": True,
"description": "Tips",
})
code = apa.coupons.create({
"code": "WELCOME10",
"discount_type": "percent",
"percent_off": 10,
"minimum_amount": "10.00",
})Create a checkout session
Spin up a one-off hosted checkout for a single order and redirect the customer to the returned checkout_url.
const session = await apa.checkoutSessions.create({
amount: "100.00",
payout_wallet_id: "pp_123",
order_id: "order_9001",
success_url: "https://example.com/thanks",
cancel_url: "https://example.com/cart",
});
console.log(session.checkout_url);session = apa.checkout_sessions.create({
"amount": "100.00",
"payout_wallet_id": "pp_123",
"order_id": "order_9001",
"success_url": "https://example.com/thanks",
"cancel_url": "https://example.com/cart",
})
print(session["checkout_url"])Preview a payment option
Route quotes are session-scoped. Pass the checkout session and the asset/network the customer wants to pay with; Apa returns only the simple result your UI needs — direct or routed, expected output, fee, and expiry.
const quote = await apa.routes.quote({
session: "cs_123",
pay_asset: "ETH",
pay_network: "ethereum",
});
console.log(quote.route, quote.expected_output, quote.apa_fee);quote = apa.routes.quote({
"session": "cs_123",
"pay_asset": "ETH",
"pay_network": "ethereum",
})
print(quote["route"], quote["expected_output"], quote["apa_fee"])Verify a webhook
Apa signs every delivery with an apa-signature header of the form t=<unixSeconds>,v1=<hmac_sha256>. Verify it against the raw request body(not a re-serialized object) using your endpoint's signing secret. constructEvent recomputes the HMAC, compares it in constant time, and rejects timestamps outside a tolerance window (default 300s) to defend against replays.
import { Apa, ApaSignatureVerificationError } from "@apa/node";
const apa = new Apa(process.env.APA_API_KEY!);
const secret = process.env.APA_WEBHOOK_SECRET!; // "whsec_…"
app.post("/webhooks/apa", express.raw({ type: "application/json" }), (req, res) => {
try {
const event = apa.webhooks.constructEvent(
req.body, // Buffer of the raw bytes
req.header("apa-signature"), // signature header
secret,
);
if (event.type === "payment.paid") markOrderPaid(event.data.order_id);
res.sendStatus(200);
} catch (err) {
if (err instanceof ApaSignatureVerificationError) {
return res.status(400).send("Invalid signature");
}
throw err;
}
});from apa import Apa, SignatureVerificationError
apa = Apa(os.environ["APA_API_KEY"])
secret = os.environ["APA_WEBHOOK_SECRET"] # "whsec_…"
@app.post("/webhooks/apa")
async def apa_webhook(request: Request):
body = await request.body() # raw bytes
try:
event = apa.webhooks.construct_event(
body,
request.headers["apa-signature"],
secret,
)
except SignatureVerificationError:
return Response(status_code=400)
if event["type"] == "payment.paid":
mark_order_paid(event["data"]["order_id"])
return Response(status_code=200)Errors
Every non-2xx response throws a typed error. Catch ApaError for everything, or a subclass for specific cases. Node error classes use the Apa prefix — ApaValidationError (400, with err.param), ApaAuthenticationError (401/403), ApaRateLimitError (429), ApaConnectionError (network/timeout), and more. Each error carries status, code, param, and requestId. Python equivalents omit the prefix, for example ValidationError.
import { ApaError, ApaValidationError, ApaAuthenticationError, ApaRateLimitError } from "@apa/node";
try {
await apa.paymentLinks.create({ /* … */ });
} catch (err) {
if (err instanceof ApaValidationError) console.error("Bad field:", err.param, err.message);
else if (err instanceof ApaAuthenticationError) console.error("Check your API key.");
else if (err instanceof ApaRateLimitError) console.error("Slow down.");
else if (err instanceof ApaError) console.error(err.status, err.code, err.requestId);
}from apa import ApaError, ValidationError, AuthenticationError
try:
apa.payment_links.create({...})
except ValidationError as err:
print("Bad field:", err.param, err.message)
except AuthenticationError:
print("Check your API key.")
except ApaError as err:
print(err.status, err.code, err.request_id)Requests are retried automatically on network errors, 429, and 5xx (respecting Retry-After) up to maxRetries times.
Idempotency
Creates accept a per-call idempotency key, sent as the Idempotency-Keyheader. Retrying with the same key safely returns the original result instead of creating a duplicate (keys are retained for at least 24 hours). The SDK's automatic retries already reuse the same request; setting your own key makes application-level retries safe too.
await apa.checkoutSessions.create(
{ amount: "100.00", payout_wallet_id: "pp_123" },
{ idempotencyKey: "order_9001" },
);apa.checkout_sessions.create(
{"amount": "100.00", "payout_wallet_id": "pp_123"},
idempotency_key="order_9001",
)Auto-pagination
Every list() auto-paginates. In Node the result is both awaitable (resolves to the first page) and async-iterable (walks every page via the cursor); Python lists iterate every page for you.
// Iterate every payment across all pages.
for await (const payment of apa.payments.list({ status: "paid" })) {
console.log(payment.id, payment.amount);
}
// Or grab a single page.
const page = await apa.payments.list({ limit: 50, status: "paid" });
console.log(page.data, page.pagination.has_more, page.pagination.cursor);
// Collect up to N items into an array.
const recent = await apa.payments.list().autoPagingToArray({ limit: 200 });# Iterate every payment across all pages.
for payment in apa.payments.list({"status": "paid"}):
print(payment["id"], payment["amount"])
# Or grab a single page.
page = apa.payments.list({"limit": 50, "status": "paid"})
print(page.data, page.pagination["has_more"], page.pagination["cursor"])