Guides
Practical guides
Five common integration problems: your first integration, idempotent requests, polling versus webhooks, verifying webhook signatures, and handling failures.
Your first integration
Most integrations follow the same shape, whether you're selling airtime from a mobile app or automating electricity payments from an internal tool.
1. Create the account and credentials
Register in the Developer Console and generate a key pair from the API Keys page. Keep your Secret Key server-side. If any part of your integration runs in a browser or mobile app, that part should call your own backend, not Ureh directly, so the Secret Key is never shipped to a client device.
2. Fund the wallet
Every purchase debits your wallet at request time. Fund it from the Developer
Console before sending a live purchase. A purchase against an insufficient
balance is rejected with 402 INSUFFICIENT_BALANCE and nothing is
charged.
3. Make the request
Send the purchase request with a fresh idempotency_key for every
attempt (see Idempotent requests below). A
202 response means it's accepted and queued, not that it has
settled.
4. Track the result
Subscribe to webhooks once, at setup time, and update your own records when a
transaction.completed or transaction.failed event
arrives. Use GET /transactions/status as a fallback for a
transaction you suspect got stuck, not as your primary status source. See
Polling vs. webhooks.
5. Handle the outcome
A completed transaction needs no further action. A failed one is refunded to your wallet automatically. Your application only needs to react to the two terminal states: tell the customer it worked, or that it did not and let them retry.
Idempotent requests
Every purchase endpoint requires an idempotency_key. Network calls
fail in ways that leave you unsure whether the request reached the server: a
timeout, a dropped connection, an automatic retry from your own code. Without an
idempotency key, retrying a purchase you're not sure succeeded risks charging the
customer twice.
Reusing the same idempotency_key on a second attempt returns the
original transaction instead of creating a new one. Generate one key per purchase
attempt (a UUID works well), store it with your own order record before sending
the request, and reuse it if you retry that attempt.
// Generate once, store it alongside your own order record, reuse it on retry
const idempotencyKey = crypto.randomUUID();
await fetch('https://api.ureh.io/v1/airtime/purchase', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.UREH_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
client_id: 'URH-8F2C91A4D6B0E37F1A',
biller: 'MTN',
recipient: '08012345678',
amount: 500,
idempotency_key: idempotencyKey,
}),
});
A different idempotency_key is always a new attempt, even with an
identical payload. The key alone identifies a retry.
Polling vs. webhooks
Both approaches read the same transaction state. They differ in latency, load and how much code you maintain.
| Polling | Webhooks | |
|---|---|---|
| How it works | You call GET /transactions/status on a timer until the
status changes. |
Ureh calls your endpoint once, the moment the status changes. |
| Latency | Bounded by your polling interval. | Near-immediate. |
| Code you maintain | A scheduler and a stopping condition. | One endpoint and a signature check. |
| Best for | A one-off script, or a fallback for a transaction that seems stuck. | Anything running in production. |
The two combine well: webhooks as the primary signal, with an occasional poll against any transaction that has had no webhook after a few minutes, in case a delivery was missed.
Verifying webhook signatures
Every webhook delivery is signed with HMAC-SHA256 over
{timestamp}.{raw_body}, using the secret you received when you
subscribed. Verify it before trusting the payload, using a constant-time
comparison rather than === or == so the check cannot
leak timing information.
Node.js
const crypto = require('crypto');
function verifyUrehSignature(rawBody, timestamp, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
// timingSafeEqual throws if the two buffers differ in length, and the
// header is attacker-controlled — compare lengths first so a malformed
// signature is rejected rather than crashing the handler.
const received = Buffer.from(signature ?? '', 'hex');
const digest = Buffer.from(expected, 'hex');
if (received.length !== digest.length) return false;
return crypto.timingSafeEqual(digest, received);
}
// req.rawBody must be the exact, unparsed request body
const timestamp = req.headers['x-ureh-timestamp'];
const signature = req.headers['x-ureh-signature'];
const valid = verifyUrehSignature(req.rawBody, timestamp, signature, process.env.UREH_WEBHOOK_SECRET);
PHP
function verify_ureh_signature(string $rawBody, string $timestamp, string $signature, string $secret): bool
{
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
return hash_equals($expected, $signature);
}
// $rawBody must come from php://input, not $_POST
$timestamp = $_SERVER['HTTP_X_UREH_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_UREH_SIGNATURE'] ?? '';
$rawBody = file_get_contents('php://input');
$valid = verify_ureh_signature($rawBody, $timestamp, $signature, getenv('UREH_WEBHOOK_SECRET'));
Reject the request if verification fails, and respond quickly either way. Ureh
counts anything other than a prompt 2xx as a failed delivery and
retries it (see Handling failures and retries
below), so slow processing looks the same as a delivery failure.
Handling failures and retries
API errors
Branch on the code field, not the message text, which can change.
A 503 PROVIDER_UNAVAILABLE means the biller could not be served at
that moment. Your wallet was never touched, so retry the same request with the
same idempotency key shortly afterwards. A 422 VALIDATION_FAILED
means fix the payload before retrying. The full list is in the
API Reference.
Failed transactions
A purchase that is accepted but cannot be completed is refunded to your wallet
automatically, with no reconciliation needed on your side. Your application only
needs to react to the transaction.failed event, or a
failed status on a poll, and inform the customer.
Webhook delivery failures
If your endpoint doesn't return a 2xx, Ureh retries the delivery on
a fixed schedule: 1 minute, 5 minutes, 15 minutes, 30 minutes, 1 hour, 6 hours,
then 24 hours. Seven attempts in total before Ureh stops. Design your endpoint
to be safe to receive twice (the event's own reference is a good
deduplication key), since a retried delivery and your own reconciliation poll
can occasionally arrive for the same transaction.