Every v1 error response shares the same envelope — modelled after
Stripe's. The error always lives under error, never at the root, so
your client can distinguish error responses from successful ones by
shape alone.
Example
json
{ "error": { "type": "provider_error", "code": "meter_blocked", "message": "This meter has been blocked by the provider and cannot purchase electricity. Please contact BPC.", "param": null, "retryable": false, "doc_url": "https://developers.inbuiltgo.com/developers/reference/errors#meter_blocked", "request_id": "req_d1f1c2a4f6b94c2390b8c6a8f7d9e0e1" }}
Field
Type
Notes
type
string
Broad category — branch on this for general handling (auth vs validation vs provider). See the Error types table below
code
string
Specific, stable identifier — branch on this for surgical handling (meter_blocked vs purchase_amount_invalid)
message
string
Human-readable, localised, safe to show to end users
param
string | null
When the error is tied to a specific request field, this is the snake_case field name (e.g. subscriber_identifier). Useful for inline form-level errors
retryable
boolean
true when the same request is safe to retry as-is. false when the caller must fix something before re-attempting
doc_url
string
Deep-link to the documented code for support tickets
request_id
string
Server-side correlation id (also returned in the X-Request-Id response header). Quote this when contacting support
Always log request_id
Every response — success or error — carries an X-Request-Id header.
When something goes wrong, the fastest path to a resolution is sending
us the request_id. You can also send your own correlation id in the
request X-Request-Id header (≤128 chars) and the server will echo it
back.
Branching strategy
Branch on type for broad handling, fall through to code for
surgical handling, and trust retryable for retry decisions:
Node
js
async function purchase(payload) { const res = await fetch("/public/v1/purchases/confirm", { method: "POST", body: JSON.stringify(payload) }); const json = await res.json(); if (!res.ok) { const { type, code, message, param, retryable, request_id } = json.error; if (type === "validation_error") return showFieldError(param, message); if (type === "authentication_error") return refreshTokenAndRetry(); if (type === "insufficient_funds_error") return promptTopupBalance(); if (retryable) return scheduleRetryWithBackoff(payload, request_id); return showFatalError(message, request_id); } return json;}
Error types
type
HTTP
When
authentication_error
401
Token missing, expired, or invalid
permission_error
403
Authenticated, but the actor lacks the required permission for this action
validation_error
400
Request payload failed schema or business validation. Look at param for the offending field
not_found_error
404
The referenced resource (account, operation, transaction) does not exist
conflict_error
409
The request conflicts with current state (e.g. duplicate operation)
insufficient_funds_error
402
The debited account has insufficient balance
rate_limit_error
429
The caller has been rate-limited. Honour Retry-After
provider_error
422
The downstream VAS provider rejected the request (bad meter, blocked subscriber, amount out of range)
provider_timeout
504
The downstream provider didn't respond in time. State is unknown — see Handling timeouts
provider_unavailable
502
The downstream provider is unreachable
service_unavailable
503
A platform dependency (e.g. sign-in) is temporarily unavailable. Retry with backoff; honour Retry-After
internal_error
500
Unexpected server-side error. Check request_id, contact support
Codes
The full list of code values the platform emits today, grouped by
category. Branch on these in your client logic.
Authentication & permissions
code
type
retryable
Meaning
invalid_credentials
authentication_error
false
Token missing, malformed, expired, or rejected. Refresh the OAuth token and re-attempt
missing_permission
permission_error
false
The actor's role does not include the required permission
forbidden
permission_error
false
The operation isn't allowed for this actor in the current state
meter_not_allowed
permission_error
false
This meter isn't on your organisation's allow-list. param: subscriber_identifier
retries_disabled
permission_error
false
Retries aren't enabled for this transaction — contact support to enable
auth_unavailable
service_unavailable
true
The authorization server is unreachable or failing. Nothing is wrong with your credentials; retry after the Retry-After seconds
Validation
code
type
retryable
Meaning
missing_field
validation_error
false
A required field was absent
invalid_argument
validation_error
false
A field failed schema validation — param names it
invalid_msisdn
validation_error
false
The phone number is not a valid MSISDN. param: beneficiary_phone_number
invalid_meter_number
validation_error
false
The meter number isn't valid for the provider. param: subscriber_identifier
invalid_amount
validation_error
false
Amount is malformed or non-positive
amount_below_minimum
validation_error
false
Amount below the minimum vend. param: amount
amount_above_maximum
validation_error
false
Amount above the maximum vend
amount_below_charges
validation_error
false
Amount is less than the charges to apply
method_not_allowed
validation_error
false
HTTP method is not supported on this endpoint
Resource lookup
code
type
retryable
Meaning
resource_not_found
not_found_error
false
No resource matched the id / reference
meter_not_found
not_found_error
false
The provider doesn't recognise this meter number
transaction_not_found
not_found_error
false
The retry lookup couldn't find a prior failed transaction
Funds & state
code
type
retryable
Meaning
insufficient_balance
insufficient_funds_error
false
The debited account doesn't have enough balance — message includes the gap
duplicate_request
conflict_error
false
The provider rejected the request as a duplicate
transaction_pending
conflict_error
true
A previous transaction on the same subscriber is still pending — wait and retry
Provider-side
These are surfaced from the BPC integration. Most map cleanly from the
provider's response code; a handful disambiguate by message text. The
message field is safe to surface verbatim to end users. It usually
carries the provider's own wording; when the failure is internal to us,
it is a generic "temporarily unavailable" sentence instead.
code
type
retryable
Meaning
meter_blocked
provider_error
false
The provider has blocked this meter from purchases (or marked it not-allowed-to-transact)
meter_unsupported
validation_error
false
The meter type isn't supported (e.g. Ultima Plus meters can't have tokens issued for them)
provider_busy
provider_error
true
Provider returned a transient "cannot process right now" — safe to retry after backoff
provider_error
provider_error
varies
Generic provider-side rejection. retryable: true when the provider hinted the failure was transient
provider_timeout
provider_timeout
true
Upstream timeout — state is unknown, use retry rather than re-confirm
provider_unavailable
provider_unavailable
true
The provider endpoint is unreachable (network failure, endpoint not listening, connection refused)
upstream_error
provider_error
false
A non-auth upstream HTTP call failed
Internal
code
type
retryable
Meaning
internal_error
internal_error
false
Something broke server-side — quote request_id to support
Handling timeouts
A provider_timeout (HTTP 504) means the platform reached the provider
but didn't get a response in time — the underlying transaction state
is unknown. The error will have retryable: true, but the right
recovery is not re-sending /confirm (which would risk a
double-charge). Instead, call
POST /public/v1/purchases/retry
with the same subscriber_identifier + external_reference and the
platform will ask the provider what actually happened.