Docs/Getting started/SDKs

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.

Node — npm
npm install @apa/node
# or: pnpm add @apa/node  /  yarn add @apa/node
Python — pip
pip install apa-app
Node 18+ and modern Python
@apa/node ships dual CJS + ESM builds and uses the global fetch (Node 18+). The Python SDK is new — pin a version and check the changelog before upgrading in production.

Initialize

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.

Node
import { Apa } from "@apa/node";

const apa = new Apa(process.env.APA_API_KEY!); // "sk_live_…"
Python
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.

Node — options
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.

Node
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 share
Python
link = 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 share
Managing links
Payment links support disable() (stop accepting payments), enable(), and archive() (an alias of disable()) — there is no generic update. Payments support refund() to record a merchant-sent refund.

Customer-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.

Node
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",
});
Python
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.

Node
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);
Python
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.

Node
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);
Python
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.

Node — Express
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;
  }
});
Python — FastAPI
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)
Sign the raw body
Verify against the exact bytes Apa sent. Frameworks that parse and re-serialize JSON will change the payload and break the signature — capture the raw body first (for example express.raw()).

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.

Node
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);
}
Python
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.

Node
await apa.checkoutSessions.create(
  { amount: "100.00", payout_wallet_id: "pp_123" },
  { idempotencyKey: "order_9001" },
);
Python
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.

Node
// 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 });
Python
# 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"])
Prefer raw HTTP?
The SDKs are a thin, typed layer over the REST API. Every method maps to an endpoint in the API reference, and webhook payloads are documented under Webhooks.