Skip to main content
Danipa
Docs · 007 / 010

Errors & limits

The Danipa Pay error envelope, common 4xx and 5xx codes, idempotency keys, and rate-limit headers.

Every Danipa Pay error response uses the same JSON envelope. This page walks through the shape, the codes you'll see most often, and how to retry safely.

The error envelope

All non-2xx responses return:

{
  "timeStamp": 1730649600000,
  "correlationId": "8f2a3b4c-…",
  "traceId": "00-1a2b3c4d…",
  "statusCode": 422,
  "domain": "fintech",
  "severity": "ERROR",
  "errors": [
    {
      "code": "WALLET_INSUFFICIENT_FUNDS",
      "description": "Wallet wlt_01HG… has 0.00 USD; transfer requested 10.00 USD."
    }
  ]
}

Field guide:

FieldWhat it carries
timeStampServer epoch millis when the response was built
correlationIdStable id for this request's audit trail — surface in your logs
traceIdW3C trace context — paste into your APM to see the full request span
statusCodeSame as the HTTP status
domainWhich Danipa service produced the error (fintech, identity, notification)
severityERROR, WARN, or INFO (most non-2xx are ERROR)
errors[]One or more {code, description} pairs. Codes are stable; descriptions are human-readable and may change

HTTP status codes

CodeMeaningWhen
200OKSuccessful GET request
201CreatedResource successfully created
202AcceptedAsync operation accepted (e.g. remittance)
400Bad RequestInvalid parameters or malformed JSON
401UnauthorizedMissing or invalid API key
403ForbiddenValid API key but insufficient permissions
404Not FoundResource does not exist
409ConflictDuplicate Idempotency-Key with different body
422UnprocessableValid JSON but business rule violation
429Too Many RequestsRate limit exceeded
500Server ErrorUnexpected server error
503Service UnavailableMaintenance or provider outage

400 — Bad request

The request was malformed before any business logic ran (invalid JSON, missing required field, wrong content-type). Fix the client; do not retry the same payload.

401 — Unauthorized

The Authorization header is missing, malformed, or the key is revoked. See Authentication.

403 — Forbidden

The key is valid but missing the required scope. Don't retry — either re-issue the key with broader scopes or call a different endpoint.

404 — Not found

The resource doesn't exist or your account can't see it. Treat the same as a public 404; don't retry.

409 — Conflict (idempotency)

You sent an Idempotency-Key you've used before, but with a different request body. Either re-use the original key with the original payload, or pick a fresh key.

422 — Unprocessable entity

The request was syntactically valid but failed a business rule (insufficient funds, KYC tier too low, recipient ineligible). The errors[].code is the actionable bit. Don't retry blindly.

429 — Rate-limited

You hit a rate limit. The response includes:

Retry-After: 12
X-RateLimit-Limit: 200
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1730649612

Sleep for Retry-After seconds and retry. Use exponential backoff if you keep hitting it.

5xx — Server error

Treat as transient. Retry with exponential backoff (e.g. capped at 60s, doubling each attempt) up to roughly 5 attempts. If you used an Idempotency-Key, the retry is safe — the server returns the original response if it already processed.

Common error codes

The errors[].code is the stable identifier you should branch on. Below are the codes you'll see most often.

Validation errors (400)

CodeDescription
INVALID_AMOUNTAmount is zero, negative, or exceeds limits
INVALID_CURRENCYUnsupported currency code
INVALID_MSISDNPhone number is not valid E.164 format
INVALID_PROVIDERUnsupported payout provider
MISSING_FIELDRequired field not provided

Business errors (422)

CodeDescription
WALLET_INSUFFICIENT_FUNDSWallet balance too low for this transaction
DAILY_LIMIT_EXCEEDEDTransaction would exceed daily sending limit
RECIPIENT_NOT_FOUNDMoMo account not registered for this number
CORRIDOR_UNAVAILABLECurrency pair not currently supported
KYC_REQUIREDHigher KYC tier needed for this amount

Provider errors (502/503)

CodeDescription
PROVIDER_TIMEOUTMoMo or bank API did not respond in time
PROVIDER_UNAVAILABLEPayout provider is temporarily down
PROVIDER_REJECTEDProvider rejected the transaction

Retry strategy

Error TypeRetry?Strategy
400 (validation)NoFix the request and resubmit
401/403 (auth)NoCheck API key and permissions
404 (not found)NoVerify the resource ID
409 (conflict)NoUse a different Idempotency-Key (or replay the original body)
429 (rate limit)YesWait for Retry-After header
500 (server)YesRetry with exponential backoff
502/503 (provider)YesRetry after 30-60 seconds

Idempotency keys

For mutating endpoints (POST, PUT, PATCH, DELETE on most resources), pass an Idempotency-Key header — a UUID you generate.

The server stores the (key, request hash, response) triple for 24 hours. During that window:

  • Same key + same body → returns the original response (200 if it succeeded, the original error if it failed). Safe to retry.
  • Same key + different body → 409 Conflict. Use a fresh key.
  • New key → request is processed normally.

cURL

curl https://api.sandbox.danipa.com/ms/v1/transfers \
  -H "Authorization: Bearer $DANIPA_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"amount":"10.00","currency":"USD","recipient":"test_recipient_ghana"}'

Node.js retry helper

import axios from 'axios';
import { randomUUID } from 'node:crypto';

const idempotencyKey = randomUUID();

async function withRetry<T>(fn: () => Promise<T>, attempts = 5): Promise<T> {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (err: unknown) {
      const e = err as { response?: { status?: number; headers?: Record<string, string> } };
      const status = e.response?.status ?? 0;
      if (status < 500 && status !== 429) throw err;
      const retryAfter = Number(e.response?.headers?.['retry-after'] ?? 0);
      const backoff = retryAfter > 0 ? retryAfter * 1000 : Math.min(60000, 2 ** i * 100);
      await new Promise((r) => setTimeout(r, backoff));
    }
  }
  throw new Error('exhausted retries');
}

const res = await withRetry(() =>
  axios.post(
    'https://api.sandbox.danipa.com/ms/v1/transfers',
    { amount: '10.00', currency: 'USD', recipient: 'test_recipient_ghana' },
    {
      headers: {
        Authorization: 'Bearer ' + process.env.DANIPA_API_KEY,
        'Idempotency-Key': idempotencyKey,
      },
    },
  ),
);

Python retry helper

import os, time, uuid, requests

idempotency_key = str(uuid.uuid4())

def with_retry(call, attempts=5):
    for i in range(attempts):
        resp = call()
        if resp.status_code < 500 and resp.status_code != 429:
            return resp
        retry_after = int(resp.headers.get('Retry-After', 0))
        backoff = retry_after if retry_after > 0 else min(60, 2 ** i * 0.1)
        time.sleep(backoff)
    return resp

def post():
    return requests.post(
        'https://api.sandbox.danipa.com/ms/v1/transfers',
        json={'amount': '10.00', 'currency': 'USD', 'recipient': 'test_recipient_ghana'},
        headers={
            'Authorization': f"Bearer {os.environ['DANIPA_API_KEY']}",
            'Idempotency-Key': idempotency_key,
        },
    )

resp = with_retry(post)
resp.raise_for_status()

Rate limits at a glance

SurfaceLimit
Per API key200 requests / minute by default; uplift on request
Per IP (unauthenticated)60 requests / minute
Webhook deliveries to your endpointWe back off if you 5xx; details in the webhooks guide

The actual limits in effect for your key are returned on every response in the X-RateLimit-* headers — read those instead of hard-coding values.

Related

  • Authentication401 and scope-related 403 responses.
  • Get started — uses an idempotency key in the first transfer example.