Client libraries
Copy-paste TypeScript and Python clients with retries and pagination, and the OpenAPI file for any language.
There's no package to install yet. These two small clients cover the partner API with retries and pagination done right; copy them into your project. The OpenAPI file at /openapi.json generates clients for any other language.
TypeScript / JavaScript
autopayke-partner.ts
// autopayke-partner.ts: a small typed client for the AutoPayKe partner API.
// No dependencies. Node 18+, Bun, Deno, edge runtimes.
export type Destination =
| { type: "mobile_money"; phone: string; network?: "safaricom" | "airtel"; name?: string }
| { type: "till"; till_number: string }
| { type: "paybill"; paybill_number: string; account_number: string }
| { type: "bank"; bank_code: string; account_number: string; account_name: string };
export type PayoutStatus = "pending" | "processing" | "succeeded" | "failed" | "reversed" | "in_review";
export type Payout = {
id: string;
object: "payout";
livemode: boolean;
status: PayoutStatus;
amount: number;
currency: "KES";
source_token: "USDC";
debited_usdc: string;
fee_usdc: string;
rate: string;
destination: Record<string, unknown>;
recipient_name: string | null;
reference: string | null;
description: string | null;
metadata: Record<string, string>;
failure_code: string | null;
failure_message: string | null;
created: string;
updated: string;
};
export type List<T> = { object: "list"; data: T[]; has_more: boolean };
export class AutoPayKeError extends Error {
constructor(public status: number, public code: string, message: string, public requestId?: string, public details?: unknown) {
super(message);
}
}
const RETRYABLE = new Set(["RATE_LIMITED", "IDEMPOTENCY_IN_PROGRESS", "CAPABILITY_PAUSED", "METHOD_UNAVAILABLE"]);
export class AutoPayKe {
constructor(private key: string, private base = "https://api.autopayke.com") {}
get livemode() {
return this.key.startsWith("apk_live_");
}
private async call<T>(method: string, path: string, body?: unknown, idempotencyKey?: string): Promise<T> {
for (let attempt = 0; ; attempt++) {
let res: Response;
try {
res = await fetch(this.base + path, {
method,
headers: {
Authorization: `Bearer ${this.key}`,
...(body ? { "Content-Type": "application/json" } : {}),
...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
} catch (err) {
if (attempt < 4) { await sleep(2 ** attempt * 500); continue; }
throw err;
}
const json = await res.json();
if (json.ok) return json.data as T;
const retry = RETRYABLE.has(json.code) || res.status >= 500;
// Only retry writes that carry an Idempotency-Key.
if (retry && attempt < 4 && (method === "GET" || idempotencyKey)) {
await sleep(Number(res.headers.get("retry-after") ?? 0) * 1000 || 2 ** attempt * 500);
continue;
}
throw new AutoPayKeError(res.status, json.code, json.error, json.requestId, json.details);
}
}
account() { return this.call<Record<string, unknown>>("GET", "/api/v1/account"); }
balance() { return this.call<{ usdc: string; usdt: string; total_usd: string; wallet_address?: string }>("GET", "/api/v1/balance"); }
banks() { return this.call<List<{ code: string; name: string }>>("GET", "/api/v1/banks"); }
quote(amount: number, destination: Destination) {
return this.call<Record<string, unknown>>("POST", "/api/v1/payouts/quote", { amount, currency: "KES", destination });
}
/** idempotencyKey: create it before the first attempt and store it with your record. */
createPayout(
p: { amount: number; destination: Destination; reference?: string; description?: string; metadata?: Record<string, string> },
idempotencyKey: string,
) {
return this.call<Payout>("POST", "/api/v1/payouts", { currency: "KES", ...p }, idempotencyKey);
}
payout(id: string) { return this.call<Payout>("GET", `/api/v1/payouts/${encodeURIComponent(id)}`); }
async *payouts(filter: { status?: PayoutStatus; reference?: string; created_gte?: string; created_lt?: string } = {}) {
let after: string | undefined;
for (;;) {
const q = new URLSearchParams({ limit: "100", ...filter, ...(after ? { starting_after: after } : {}) } as Record<string, string>);
const page = await this.call<List<Payout>>("GET", `/api/v1/payouts?${q}`);
yield* page.data;
if (!page.has_more || !page.data.length) return;
after = page.data[page.data.length - 1]!.id;
}
}
verifyRecipient(r: { type: "mobile_money"; phone: string; network?: string; name?: string } | { type: "bank"; bank_code: string; account_number: string; name?: string }) {
return this.call<{ resolved: boolean; account_name: string | null; name_match: "matched" | "mismatch" | "unverified" | null }>("POST", "/api/v1/recipients/verify", r);
}
}
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
// Usage
// const ap = new AutoPayKe(process.env.AUTOPAYKE_API_KEY!);
// const payout = await ap.createPayout({ amount: 150000, destination: { type: "mobile_money", phone: "+254700000001" }, reference: "order_1042" }, order.idempotencyKey);Python
autopayke_partner.py
# autopayke_partner.py: a small client for the AutoPayKe partner API. Needs requests.
import time
import requests
RETRYABLE = {"RATE_LIMITED", "IDEMPOTENCY_IN_PROGRESS", "CAPABILITY_PAUSED", "METHOD_UNAVAILABLE"}
class AutoPayKeError(Exception):
def __init__(self, status, code, message, request_id=None, details=None):
super().__init__(f"{code}: {message}")
self.status, self.code, self.request_id, self.details = status, code, request_id, details
class AutoPayKe:
def __init__(self, key: str, base: str = "https://api.autopayke.com"):
self.base = base
self.s = requests.Session()
self.s.headers["Authorization"] = f"Bearer {key}"
def _call(self, method, path, json=None, idempotency_key=None, params=None):
headers = {"Idempotency-Key": idempotency_key} if idempotency_key else {}
for attempt in range(5):
try:
r = self.s.request(method, self.base + path, json=json, params=params, headers=headers, timeout=30)
except requests.RequestException:
if attempt == 4:
raise
time.sleep(2 ** attempt * 0.5)
continue
body = r.json()
if body["ok"]:
return body["data"]
retry = body["code"] in RETRYABLE or r.status_code >= 500
if retry and attempt < 4 and (method == "GET" or idempotency_key):
time.sleep(float(r.headers.get("Retry-After", 0)) or 2 ** attempt * 0.5)
continue
raise AutoPayKeError(r.status_code, body["code"], body["error"], body.get("requestId"), body.get("details"))
def account(self):
return self._call("GET", "/api/v1/account")
def balance(self):
return self._call("GET", "/api/v1/balance")
def quote(self, amount: int, destination: dict):
return self._call("POST", "/api/v1/payouts/quote", {"amount": amount, "currency": "KES", "destination": destination})
def create_payout(self, amount: int, destination: dict, idempotency_key: str, **extra):
return self._call("POST", "/api/v1/payouts", {"amount": amount, "currency": "KES", "destination": destination, **extra}, idempotency_key)
def payout(self, payout_id: str):
return self._call("GET", f"/api/v1/payouts/{payout_id}")
def payouts(self, **filters):
after = None
while True:
page = self._call("GET", "/api/v1/payouts", params={"limit": 100, **filters, **({"starting_after": after} if after else {})})
yield from page["data"]
if not page["has_more"] or not page["data"]:
return
after = page["data"][-1]["id"]
def verify_recipient(self, **recipient):
return self._call("POST", "/api/v1/recipients/verify", recipient)
# ap = AutoPayKe(os.environ["AUTOPAYKE_API_KEY"])
# ap.create_payout(150000, {"type": "mobile_money", "phone": "+254700000001"}, idempotency_key=order.idempotency_key, reference="order_1042")Postman, Insomnia and code generators
Import /openapi.json into Postman or Insomnia, set a bearer token of your test key, and every endpoint is ready to call. For generators (openapi-generator, oapi-codegen, openapi-python-client), the API-key endpoints use the apiKeyAuth scheme and payout creation declares its Idempotency-Key header.