Error Handling
Every Migo API returns responses wrapped in a CustomResponse<T> envelope.
The envelope shape is:
interface CustomResponse<T> {
success: boolean;
message?: string;
data?: T;
errors?: { code: string; message: string }[];
}
Note that code is a string (e.g. "7100"), and errors is an array of { code, message } objects. There is no statusCode, error.name, or error.detail field on the wire payload β the name (e.g. USER_NOT_FOUND) is the internal MigoNameErrors key, not part of the response body.
Successβ
{
"success": true,
"message": "ok",
"data": { "...": "..." }
}
Errorβ
{
"success": false,
"message": "Validation failed",
"errors": [
{
"code": "7100",
"message": "User not found in the system."
}
]
}
Status code mappingβ
| HTTP status | When it's returned |
|---|---|
200 OK | Success (including async jobs that return a batch/job id in the body) |
201 Created | Resource created (e.g. statement creation accepted) |
204 No Content | Success, no body |
400 Bad Request | DTO validation failed |
401 Unauthorized | Missing / expired / invalid JWT |
403 Forbidden | Authenticated but permission denied |
404 Not Found | Resource does not exist |
409 Conflict | Resource state conflict |
422 Unprocessable Entity | Semantic validation failed (e.g. card expired) |
500 Internal Server Error | Migo bug β please report with the request timestamp and details |
502 Bad Gateway | Upstream processor unreachable |
503 Service Unavailable | Scheduled maintenance or partial outage |
504 Gateway Timeout | Upstream processor timeout |
Business error code rangesβ
Migo uses two distinct business error code spaces depending on the API you are calling. The Wallet Gateway and CMS Backoffice surface a string code inside the errors[] array; the Middleware uses a separate envelope with an ownCode. The numeric ranges do not overlap and the field names differ.
Wallet Gateway, CMS Backoffice β 7000β8099β
These codes appear as a string in each entry of the errors array of the standard CustomResponse envelope shown above (e.g. "code": "7100").
| Range | Meaning |
|---|---|
7000β7099 | General / validation / terminal-processor |
7100β7199 | Users (incl. OTP, FCM, password, auth-user) |
7200β7299 | Cards (incl. third-party cards, view tokens) |
7300β7399 | Transfers & terminal payments |
7400β7499 | Terminals, businesses & branches |
7500β7599 | Settlements & tax regime |
7600β7699 | Clients, auth, roles, permissions, files & socials |
7700β7799 | Project config & app integrity |
8000β8099 | App / mobile (process, balance, permissions) |
Ranges 7800β7899 and 7900β7999 are currently unused.
Full list in the Error Catalog.
Middleware (Payment Links, payments, tokenization) β ownCodeβ
The Middleware uses a separate envelope (ErrorResponseDto) and carries the business error in a nested error object, where ownCode is a small numeric value (e.g. "2002", "2007", "5000", "5004"). The shape is:
interface ErrorResponseDto {
message?: string;
error?: {
code?: number; // HTTP status associated with the error
ownCode?: string; // Migo business error code
message?: string; // human-readable description
};
}
These do not clash with the 7000β8099 space because they live in a different envelope and field path (error.ownCode, not errors[].code).
| Range | Meaning |
|---|---|
2000β2099 | Transaction creation / business validation (excluded user, amount out of range, etc.) |
5000β5099 | DTO / configuration validation (missing fields, unknown client slug) |
Full list in the Error Catalog β Payment Link / Middleware ownCodes.
When you wrap calls to both surfaces in the same client, branch on the field path:
const code = response?.errors?.[0]?.code ?? response?.error?.ownCode;
Retry semanticsβ
| Error class | Retry? | Strategy |
|---|---|---|
| Network timeout | Yes | Exponential backoff |
500 / 502 / 504 | Yes | Exponential backoff, cap at 3 retries |
503 | Yes | Exponential backoff with long ceiling (up to 5 min) |
400 / 422 | No | Fix the request |
401 | No (refresh instead) | Refresh token, then retry |
403 / 404 / 409 | No | Do not retry |
Client patternβ
async function call<T>(fn: () => Promise<T>, attempt = 0): Promise<T> {
try {
return await fn();
} catch (err) {
const status = err.response?.status;
if (status === 401) {
await refreshToken();
return call(fn, attempt + 1);
}
if ([500, 502, 503, 504].includes(status) && attempt < 3) {
const wait = 2 ** attempt * 500;
await new Promise(r => setTimeout(r, wait));
return call(fn, attempt + 1);
}
throw err;
}
}
Reporting issuesβ
When opening a support ticket include:
- The request timestamp in UTC (Migo correlates it to the internal trace)
- Full request URL, method, and headers (redact the Bearer token)
- Request body (redact PANs and CVVs)
- Full response body