Code examples
Complete, minimal clients for PHP/Laravel, Node.js and Python. Each one:
- reads every value from configuration (4.3)
- sends
X-Api-Keyon every call and caches the bearer token for 14 minutes - logs in again and retries once on
401 - sends amounts as integer cents and rejects anything else before calling InnBucks
- treats
00and000as success and reads the message from either field - normalises the code status and treats
ClaimedandPaidas paid - builds the deep link for the configured environment
- keeps the
X-Trace-Idof the last response
| Language | HTTP client | Section |
|---|---|---|
| PHP / Laravel | Laravel Http facade, Cache, a queued job |
9.1 |
| JavaScript | Node.js 18+ built-in fetch |
9.2 |
| Python | requests |
9.3 |
Every client was run against a mock InnBucks server that checks the paths, both headers, token
refresh after a 401, the exact field names and integer amounts (tools/test_examples.py).
9.1 PHP / Laravel
config/services.php:
'innbucks' => [
'base_url' => env('INNBUCKS_BASE_URL'),
'api_key' => env('INNBUCKS_API_KEY'),
'username' => env('INNBUCKS_USERNAME'),
'password' => env('INNBUCKS_PASSWORD'),
'environment' => env('INNBUCKS_ENV', 'test'),
'token_ttl' => (int) env('INNBUCKS_TOKEN_TTL', 840),
],
app/Exceptions/InnBucksException.php:
<?php
namespace App\Exceptions;
use RuntimeException;
class InnBucksException extends RuntimeException
{
public function __construct(
string $message,
public readonly array $response = [],
public readonly ?string $traceId = null,
public readonly ?int $httpStatus = null,
) {
parent::__construct($message);
}
}
app/Services/InnBucks.php:
<?php
namespace App\Services;
use App\Exceptions\InnBucksException;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
class InnBucks
{
private const TOKEN_CACHE_KEY = 'innbucks.access_token';
public ?string $lastTraceId = null;
// ---- authentication -------------------------------------------------
/** A cached bearer token; logs in when there is none (or $fresh is true). */
public function token(bool $fresh = false): string
{
if ($fresh) {
Cache::forget(self::TOKEN_CACHE_KEY);
}
return Cache::remember(self::TOKEN_CACHE_KEY, config('services.innbucks.token_ttl', 840), function () {
$response = $this->http()->post('/auth/third-party', [
'username' => config('services.innbucks.username'),
'password' => config('services.innbucks.password'),
]);
$body = $this->decode($response);
if (! self::ok($body) || empty($body['accessToken'])) {
throw new InnBucksException('InnBucks login failed: '.self::message($body), $body, $this->lastTraceId, $response->status());
}
return $body['accessToken'];
});
}
// ---- codes: payments and withdrawals --------------------------------
/** Generate a PAYMENT or WITHDRAWAL code. $amountCents: 450 = 4.50. */
public function generateCode(int $amountCents, string $currency, string $reference,
string $narration, string $type = 'PAYMENT'): array
{
return $this->send('POST', '/api/code/generate', [
'reference' => $reference,
'narration' => $narration,
'currency' => self::currency($currency),
'amount' => self::cents($amountCents),
'type' => in_array($type, ['PAYMENT', 'WITHDRAWAL'], true) ? $type
: throw new \InvalidArgumentException('type must be PAYMENT or WITHDRAWAL'),
]);
}
/** Current status of a code. Call at most once every 30 seconds per code. */
public function inquireCode(string $code, ?string $reference = null): array
{
return $this->send('POST', '/api/code/inquiry', array_filter([
'reference' => $reference,
'code' => $code,
], fn ($v) => $v !== null));
}
// ---- agent services -------------------------------------------------
public function linkedAccount(string $msisdn, string $currency = 'USD'): array
{
return $this->send('GET', '/api/v1/account/msisdn/'.rawurlencode($msisdn).'/details?currency='.self::currency($currency));
}
public function deposit(string $destinationAccount, int $amountCents, string $currency,
string $reference, string $narration, ?string $type = null): array
{
return $this->send('POST', '/api/transaction/deposit', array_filter([
'reference' => $reference,
'currency' => self::currency($currency),
'amount' => self::cents($amountCents),
'narration' => $narration,
'destinationAccount' => $destinationAccount,
'type' => $type,
], fn ($v) => $v !== null));
}
public function depositInquiry(string $accountNumber, string $originalReference, string $inquiryReference): array
{
return $this->send('POST', '/bank/api/transaction/inquiry', [
'accountNumber' => $accountNumber,
'participantReference' => $inquiryReference,
'originalParticipantReference' => $originalReference,
]);
}
/** Reverse a DEPOSIT. Amount and account must match the original exactly. */
public function reverseDeposit(string $destinationAccount, int $amountCents, string $currency,
string $originalReference, string $reversalReference): array
{
return $this->send('POST', '/api/transaction/reversal/v2', [
'amount' => self::cents($amountCents),
'currency' => self::currency($currency),
'type' => 'CREDIT',
'destinationAccount' => $destinationAccount,
'participantReference' => $reversalReference,
'originalParticipantReference' => $originalReference,
]);
}
/** Credit a cash customer's change to their wallet: US$5.00 (500 cents) or less. */
public function bankChange(string $msisdn, int $amountCents, string $currency,
string $reference, string $narration): array
{
if (strtoupper($currency) === 'USD' && $amountCents > 500) {
throw new \InvalidArgumentException('Bank change is limited to 500 cents (US$5.00).');
}
return $this->send('POST', '/api/transaction/bankChange', [
'reference' => $reference,
'currency' => self::currency($currency),
'amount' => self::cents($amountCents),
'narration' => $narration,
'destinationMsisdn' => $msisdn,
]);
}
public function utilityPayment(string $provider, string $product, string $destinationAccount,
int $amountCents, string $currency, string $reference,
string $narration = '', array $additionalData = []): array
{
return $this->send('POST', '/api/utility/provider/payment', [
'provider' => $provider,
'providerProduct' => $product,
'amount' => self::cents($amountCents),
'currency' => self::currency($currency),
'reference' => $reference,
'narration' => $narration,
'destinationAccount' => $destinationAccount,
'additionalData' => (object) $additionalData,
]);
}
/** $start / $end as 'Y-m-d H:i:s', within one calendar month. */
public function statement(string $accountNumber, string $currency, string $start, string $end): array
{
return $this->send('POST', '/api/account/fullStatement', [
'accountNumber' => $accountNumber,
'currency' => self::currency($currency),
'startDateTime' => $start,
'endDateTime' => $end,
]);
}
// ---- helpers --------------------------------------------------------
public static function ok(array $body): bool
{
return in_array($body['responseCode'] ?? null, ['00', '000'], true);
}
public static function message(array $body): string
{
return (string) ($body['responseMsg'] ?? $body['responseDescription'] ?? 'no message');
}
/** new | claimed | paid | expired | timed_out | null */
public static function codeStatus(array $body): ?string
{
$s = strtolower(preg_replace('/[\s_-]+/', '', (string) ($body['status'] ?? '')));
return ['new' => 'new', 'claimed' => 'claimed', 'paid' => 'paid',
'expired' => 'expired', 'timedout' => 'timed_out'][$s] ?? null;
}
public static function isPaid(array $body): bool
{
return self::ok($body) && in_array(self::codeStatus($body), ['claimed', 'paid'], true);
}
public static function isFinal(array $body): bool
{
return self::ok($body) && in_array(self::codeStatus($body), ['claimed', 'paid', 'expired', 'timed_out'], true);
}
public static function deepLink(string $code): string
{
$scheme = config('services.innbucks.environment') === 'production'
? 'com.innbucks.customer'
: 'zw.co.innbucksnova.test';
return $scheme.'://purchase?paymentToken='.rawurlencode($code);
}
/** A data: URI for an <img> tag, whether or not qrCode already carries the prefix. */
public static function qrDataUri(array $body): ?string
{
$qr = $body['qrCode'] ?? null;
return $qr === null ? null : (str_starts_with($qr, 'data:') ? $qr : 'data:image/png;base64,'.$qr);
}
private static function cents(int $amount): int
{
if ($amount <= 0) {
throw new \InvalidArgumentException('Amounts are positive integers in cents: 450 = 4.50.');
}
return $amount;
}
private static function currency(string $currency): string
{
$currency = strtoupper($currency);
return in_array($currency, ['USD', 'ZWG'], true) ? $currency
: throw new \InvalidArgumentException('currency must be USD or ZWG');
}
private function http(): PendingRequest
{
return Http::baseUrl(rtrim(config('services.innbucks.base_url'), '/'))
->withHeaders(['X-Api-Key' => config('services.innbucks.api_key')])
->acceptJson()
->asJson()
->timeout(30);
}
private function send(string $method, string $path, array $body = []): array
{
$call = fn (string $token) => $this->http()->withToken($token)
->send($method, $path, $method === 'GET' ? [] : ['json' => $body]);
$response = $call($this->token());
if ($response->status() === 401) { // token expired: log in again, retry once
$response = $call($this->token(fresh: true));
}
$decoded = $this->decode($response);
if ($response->failed()) {
throw new InnBucksException("InnBucks HTTP {$response->status()}: ".self::message($decoded),
$decoded, $this->lastTraceId, $response->status());
}
return $decoded;
}
private function decode(Response $response): array
{
$this->lastTraceId = $response->header('X-Trace-Id') ?: null;
return $response->json() ?? [];
}
}
app/Jobs/PollInnBucksCode.php: checks one code every 30 seconds until it is final:
<?php
namespace App\Jobs;
use App\Models\Order;
use App\Services\InnBucks;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Throwable;
class PollInnBucksCode implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 1;
public function __construct(public Order $order, public int $attempt = 1) {}
public function handle(InnBucks $innbucks): void
{
try {
$res = $innbucks->inquireCode($this->order->innbucks_code);
} catch (Throwable $e) {
report($e);
$res = []; // try again at the next check
}
if (InnBucks::isPaid($res)) {
$this->order->markPaid(authNumber: $res['authNumber'] ?? null);
return;
}
if (InnBucks::isFinal($res)) { // Expired or Timed Out
$this->order->markInnBucksCodeExpired();
return;
}
if ($this->attempt >= 22) { // ~11 minutes: the code has expired by now
$this->order->markPaymentUnknown(); // review it; never call it failed
return;
}
self::dispatch($this->order, $this->attempt + 1)->delay(now()->addSeconds(30));
}
}
Usage in a controller:
use App\Jobs\PollInnBucksCode;
use App\Services\InnBucks;
$res = app(InnBucks::class)->generateCode(
amountCents: $order->total_cents, // 450 = US$4.50
currency: 'USD',
reference: $order->reference,
narration: 'Order '.$order->reference,
);
if (! InnBucks::ok($res)) {
return back()->withErrors(['payment' => InnBucks::message($res)]);
}
$order->update(['innbucks_code' => $res['code'], 'innbucks_auth_number' => $res['authNumber'] ?? null]);
PollInnBucksCode::dispatch($order)->delay(now()->addSeconds(30));
return view('checkout.innbucks', [
'code' => $res['code'],
'qr' => InnBucks::qrDataUri($res),
'deepLink' => InnBucks::deepLink($res['code']),
'expires' => now()->addMinutes(10),
]);
markPaid, markInnBucksCodeExpired and markPaymentUnknown stand for your own order logic.
Make markPaid idempotent: a second call must not fulfil the order twice.
9.2 Node.js
innbucks.js (Node.js 18+, no dependencies, ES module):
// InnBucks Merchant API client. Node.js 18+ (built-in fetch). Amounts are integer cents.
const cfg = {
baseUrl: (process.env.INNBUCKS_BASE_URL || '').replace(/\/+$/, ''),
apiKey: process.env.INNBUCKS_API_KEY,
username: process.env.INNBUCKS_USERNAME,
password: process.env.INNBUCKS_PASSWORD,
environment: process.env.INNBUCKS_ENV || 'test',
tokenTtlMs: Number(process.env.INNBUCKS_TOKEN_TTL || 840) * 1000,
};
let token = null;
let tokenAt = 0;
export let lastTraceId = null;
export class InnBucksError extends Error {
constructor(message, { response = {}, traceId = null, httpStatus = null } = {}) {
super(message);
Object.assign(this, { response, traceId, httpStatus });
}
}
async function request(method, path, body, bearer) {
const headers = { 'X-Api-Key': cfg.apiKey, Accept: 'application/json' };
if (bearer) headers.Authorization = `Bearer ${bearer}`;
if (body !== undefined) headers['Content-Type'] = 'application/json';
const res = await fetch(cfg.baseUrl + path, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
signal: AbortSignal.timeout(30_000),
});
lastTraceId = res.headers.get('x-trace-id');
const text = await res.text();
let json = {};
try { json = text ? JSON.parse(text) : {}; } catch { json = { raw: text }; }
return { status: res.status, json };
}
export async function login(fresh = false) {
if (!fresh && token && Date.now() - tokenAt < cfg.tokenTtlMs) return token;
const { status, json } = await request('POST', '/auth/third-party',
{ username: cfg.username, password: cfg.password });
if (!ok(json) || !json.accessToken) {
throw new InnBucksError(`InnBucks login failed: ${message(json)}`,
{ response: json, traceId: lastTraceId, httpStatus: status });
}
token = json.accessToken;
tokenAt = Date.now();
return token;
}
async function send(method, path, body) {
let res = await request(method, path, body, await login());
if (res.status === 401) res = await request(method, path, body, await login(true)); // retry once
if (res.status >= 400) {
throw new InnBucksError(`InnBucks HTTP ${res.status}: ${message(res.json)}`,
{ response: res.json, traceId: lastTraceId, httpStatus: res.status });
}
return res.json;
}
function cents(amount) {
if (!Number.isInteger(amount) || amount <= 0) {
throw new TypeError('Amounts are positive integers in cents: 450 = 4.50');
}
return amount;
}
function currency(c) {
const up = String(c).toUpperCase();
if (!['USD', 'ZWG'].includes(up)) throw new TypeError('currency must be USD or ZWG');
return up;
}
// ---- codes ---------------------------------------------------------------
export function generateCode({ amount, currency: cur = 'USD', reference, narration, type = 'PAYMENT' }) {
if (!['PAYMENT', 'WITHDRAWAL'].includes(type)) throw new TypeError('type must be PAYMENT or WITHDRAWAL');
return send('POST', '/api/code/generate',
{ reference, narration, currency: currency(cur), amount: cents(amount), type });
}
export function inquireCode(code, reference) {
return send('POST', '/api/code/inquiry', reference ? { reference, code } : { code });
}
// ---- agent services ----------------------------------------------------
export function linkedAccount(msisdn, cur = 'USD') {
return send('GET', `/api/v1/account/msisdn/${encodeURIComponent(msisdn)}/details?currency=${currency(cur)}`);
}
export function deposit({ destinationAccount, amount, currency: cur, reference, narration, type }) {
const body = { reference, currency: currency(cur), amount: cents(amount), narration, destinationAccount };
if (type) body.type = type;
return send('POST', '/api/transaction/deposit', body);
}
export function depositInquiry({ accountNumber, originalReference, inquiryReference }) {
return send('POST', '/bank/api/transaction/inquiry', {
accountNumber,
participantReference: inquiryReference,
originalParticipantReference: originalReference,
});
}
export function reverseDeposit({ destinationAccount, amount, currency: cur, originalReference, reversalReference }) {
return send('POST', '/api/transaction/reversal/v2', {
amount: cents(amount),
currency: currency(cur),
type: 'CREDIT',
destinationAccount,
participantReference: reversalReference,
originalParticipantReference: originalReference,
});
}
export function bankChange({ msisdn, amount, currency: cur = 'USD', reference, narration }) {
if (currency(cur) === 'USD' && cents(amount) > 500) throw new RangeError('Bank change is limited to 500 cents (US$5.00)');
return send('POST', '/api/transaction/bankChange',
{ reference, currency: currency(cur), amount, narration, destinationMsisdn: msisdn });
}
export function utilityPayment({ provider, product, destinationAccount, amount, currency: cur, reference, narration = '', additionalData = {} }) {
return send('POST', '/api/utility/provider/payment', {
provider, providerProduct: product, amount: cents(amount), currency: currency(cur),
reference, narration, destinationAccount, additionalData,
});
}
export function statement({ accountNumber, currency: cur, start, end }) {
return send('POST', '/api/account/fullStatement',
{ accountNumber, currency: currency(cur), startDateTime: start, endDateTime: end });
}
// ---- helpers ---------------------------------------------------------------
export function ok(body) {
return ['00', '000'].includes(body?.responseCode);
}
export function message(body) {
return body?.responseMsg ?? body?.responseDescription ?? 'no message';
}
/** 'new' | 'claimed' | 'paid' | 'expired' | 'timed_out' | null */
export function codeStatus(body) {
const s = String(body?.status ?? '').toLowerCase().replace(/[\s_-]+/g, '');
return { new: 'new', claimed: 'claimed', paid: 'paid', expired: 'expired', timedout: 'timed_out' }[s] ?? null;
}
export const isPaid = (body) => ok(body) && ['claimed', 'paid'].includes(codeStatus(body));
export const isFinal = (body) => ok(body) && ['claimed', 'paid', 'expired', 'timed_out'].includes(codeStatus(body));
export function deepLink(code) {
const scheme = cfg.environment === 'production' ? 'com.innbucks.customer' : 'zw.co.innbucksnova.test';
return `${scheme}://purchase?paymentToken=${encodeURIComponent(code)}`;
}
export function qrDataUri(body) {
const qr = body?.qrCode;
if (!qr) return null;
return qr.startsWith('data:') ? qr : `data:image/png;base64,${qr}`;
}
/** Check a code every intervalMs (30 s by default) until final, or give up after maxMs. */
export async function waitForCode(code, { intervalMs = 30_000, maxMs = 11 * 60_000 } = {}) {
const started = Date.now();
for (;;) {
await new Promise((r) => setTimeout(r, intervalMs));
let body = {};
try { body = await inquireCode(code); } catch { /* try again at the next check */ }
if (isFinal(body)) return body;
if (Date.now() - started >= maxMs) return { ...body, status: body.status ?? null, unknown: true };
}
}
Usage:
import { generateCode, ok, message, deepLink, qrDataUri, waitForCode, isPaid } from './innbucks.js';
const res = await generateCode({ amount: 450, reference: 'ORD-10045', narration: 'Order ORD-10045' });
if (!ok(res)) throw new Error(message(res));
// save res.code on the order, then show: res.code, qrDataUri(res), deepLink(res.code)
const final = await waitForCode(res.code); // in a worker, not in the web request
console.log(isPaid(final) ? 'paid' : final.unknown ? 'unknown - review' : 'expired');
9.3 Python
innbucks.py (Python 3.9+, pip install requests):
"""InnBucks Merchant API client. Amounts are integer cents: 450 == 4.50."""
import os
import re
import time
from urllib.parse import quote
import requests
BASE_URL = os.environ.get("INNBUCKS_BASE_URL", "").rstrip("/")
API_KEY = os.environ.get("INNBUCKS_API_KEY")
USERNAME = os.environ.get("INNBUCKS_USERNAME")
PASSWORD = os.environ.get("INNBUCKS_PASSWORD")
ENVIRONMENT = os.environ.get("INNBUCKS_ENV", "test")
TOKEN_TTL = int(os.environ.get("INNBUCKS_TOKEN_TTL", "840"))
_session = requests.Session()
_token = None
_token_at = 0.0
last_trace_id = None
class InnBucksError(Exception):
def __init__(self, message, response=None, trace_id=None, http_status=None):
super().__init__(message)
self.response = response or {}
self.trace_id = trace_id
self.http_status = http_status
def _request(method, path, body=None, bearer=None):
global last_trace_id
headers = {"X-Api-Key": API_KEY, "Accept": "application/json"}
if bearer:
headers["Authorization"] = "Bearer " + bearer
res = _session.request(method, BASE_URL + path, json=body, headers=headers, timeout=30)
last_trace_id = res.headers.get("X-Trace-Id")
try:
data = res.json() if res.content else {}
except ValueError:
data = {"raw": res.text}
return res.status_code, data
def login(fresh=False):
global _token, _token_at
if not fresh and _token and time.time() - _token_at < TOKEN_TTL:
return _token
status, data = _request("POST", "/auth/third-party", {"username": USERNAME, "password": PASSWORD})
if not ok(data) or not data.get("accessToken"):
raise InnBucksError("InnBucks login failed: " + message(data), data, last_trace_id, status)
_token, _token_at = data["accessToken"], time.time()
return _token
def _send(method, path, body=None):
status, data = _request(method, path, body, login())
if status == 401: # token expired: log in again, retry once
status, data = _request(method, path, body, login(fresh=True))
if status >= 400:
raise InnBucksError("InnBucks HTTP %d: %s" % (status, message(data)), data, last_trace_id, status)
return data
def _cents(amount):
if not isinstance(amount, int) or isinstance(amount, bool) or amount <= 0:
raise ValueError("Amounts are positive integers in cents: 450 == 4.50")
return amount
def _currency(currency):
if currency.upper() not in ("USD", "ZWG"):
raise ValueError("currency must be USD or ZWG")
return currency.upper()
# ---- codes ----------------------------------------------------------------
def generate_code(amount, currency, reference, narration, type="PAYMENT"):
if type not in ("PAYMENT", "WITHDRAWAL"):
raise ValueError("type must be PAYMENT or WITHDRAWAL")
return _send("POST", "/api/code/generate", {
"reference": reference, "narration": narration,
"currency": _currency(currency), "amount": _cents(amount), "type": type})
def inquire_code(code, reference=None):
body = {"reference": reference, "code": code} if reference else {"code": code}
return _send("POST", "/api/code/inquiry", body)
# ---- agent services -------------------------------------------------------
def linked_account(msisdn, currency="USD"):
return _send("GET", "/api/v1/account/msisdn/%s/details?currency=%s" % (quote(msisdn), _currency(currency)))
def deposit(destination_account, amount, currency, reference, narration, type=None):
body = {"reference": reference, "currency": _currency(currency), "amount": _cents(amount),
"narration": narration, "destinationAccount": destination_account}
if type:
body["type"] = type
return _send("POST", "/api/transaction/deposit", body)
def deposit_inquiry(account_number, original_reference, inquiry_reference):
return _send("POST", "/bank/api/transaction/inquiry", {
"accountNumber": account_number,
"participantReference": inquiry_reference,
"originalParticipantReference": original_reference})
def reverse_deposit(destination_account, amount, currency, original_reference, reversal_reference):
return _send("POST", "/api/transaction/reversal/v2", {
"amount": _cents(amount), "currency": _currency(currency), "type": "CREDIT",
"destinationAccount": destination_account,
"participantReference": reversal_reference,
"originalParticipantReference": original_reference})
def bank_change(msisdn, amount, currency, reference, narration):
if _currency(currency) == "USD" and _cents(amount) > 500:
raise ValueError("Bank change is limited to 500 cents (US$5.00)")
return _send("POST", "/api/transaction/bankChange", {
"reference": reference, "currency": _currency(currency), "amount": _cents(amount),
"narration": narration, "destinationMsisdn": msisdn})
def utility_payment(provider, product, destination_account, amount, currency, reference,
narration="", additional_data=None):
return _send("POST", "/api/utility/provider/payment", {
"provider": provider, "providerProduct": product, "amount": _cents(amount),
"currency": _currency(currency), "reference": reference, "narration": narration,
"destinationAccount": destination_account, "additionalData": additional_data or {}})
def statement(account_number, currency, start, end):
"""start / end as 'YYYY-MM-DD HH:MM:SS', within one calendar month."""
return _send("POST", "/api/account/fullStatement", {
"accountNumber": account_number, "currency": _currency(currency),
"startDateTime": start, "endDateTime": end})
# ---- helpers --------------------------------------------------------------
def ok(body):
return (body or {}).get("responseCode") in ("00", "000")
def message(body):
body = body or {}
return str(body.get("responseMsg") or body.get("responseDescription") or "no message")
def code_status(body):
"""'new' | 'claimed' | 'paid' | 'expired' | 'timed_out' | None"""
s = re.sub(r"[\s_-]+", "", str((body or {}).get("status") or "")).lower()
return {"new": "new", "claimed": "claimed", "paid": "paid",
"expired": "expired", "timedout": "timed_out"}.get(s)
def is_paid(body):
return ok(body) and code_status(body) in ("claimed", "paid")
def is_final(body):
return ok(body) and code_status(body) in ("claimed", "paid", "expired", "timed_out")
def deep_link(code):
scheme = "com.innbucks.customer" if ENVIRONMENT == "production" else "zw.co.innbucksnova.test"
return "%s://purchase?paymentToken=%s" % (scheme, quote(str(code)))
def qr_data_uri(body):
qr = (body or {}).get("qrCode")
if not qr:
return None
return qr if qr.startswith("data:") else "data:image/png;base64," + qr
def wait_for_code(code, interval=30, max_seconds=660):
"""Check a code every `interval` seconds until final; after `max_seconds` return it marked unknown."""
started = time.time()
while True:
time.sleep(interval)
try:
body = inquire_code(code)
except (InnBucksError, requests.RequestException):
body = {} # try again at the next check
if is_final(body):
return body
if time.time() - started >= max_seconds:
return dict(body, unknown=True)
Usage:
import innbucks
res = innbucks.generate_code(450, "USD", "ORD-10045", "Order ORD-10045")
if not innbucks.ok(res):
raise RuntimeError(innbucks.message(res))
# save res["code"] on the order, then show res["code"], innbucks.qr_data_uri(res),
# innbucks.deep_link(res["code"])
final = innbucks.wait_for_code(res["code"]) # in a worker (Celery, RQ), not the web request
print("paid" if innbucks.is_paid(final) else "unknown" if final.get("unknown") else "expired")
This page is generated from section 9 of the README in README.md. Spotted something wrong? Open an issue.