Skip to content

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.

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