Errors
Handling errors
The error object, the seven error types, the codes you meet first, and how to handle each.
On this page
When a request fails, the API answers an HTTP status of 400 or more and an error object. Your
code decides what to do from the error's type and code; the message is for people.
The error object
{
"error": {
"type": "invalid_request_error",
"code": "resource_missing",
"message": "No such payment_intent: 'pi_01k7d9x2c5v8b1n4m7q0w3e6rt'",
"doc_url": "https://pay-docs.adpromax.com/errors#resource_missing",
"request_id": "req_01k7d9x3f6g9h2j5k8m1n4p7qs"
}
}| Field | What it holds |
|---|---|
type | One of the seven types below: the broad kind of failure. |
code | The precise reason, stable over time: branch on it. |
decline_code | For a declined card: the bank's reason, such as insufficient_funds. |
param | The parameter at fault, as a path (metadata.order_id), when there is one. |
message | An explanation for a person, in the language of your request (English, French or Arabic, from the Accept-Language header). It may change: never branch on it. |
doc_url | The page that explains the code. |
request_id | The request's ID (req_…), also in the Request-Id header of every answer. |
Error types
type | Status | What happened | What to do |
|---|---|---|---|
invalid_request_error | 400, 404, 409 | The request is wrong: a missing, unknown or invalid parameter, an object or an address that does not exist, an action the object's state does not allow. | Fix the request. Sending it again unchanged fails the same way. |
authentication_error | 401 | No key, or a key that does not work. | Check the key and its mode. |
permission_error | 403 | The key works but may not do this. | See the code: a live key before activation, a test helper with a live key… |
card_error | 402 | A card was declined. | Show your customer a message and let them try another card. |
idempotency_error | 400, 409 | An Idempotency-Key was reused for another request, or its first request still runs. | See Idempotency. |
rate_limit_error | 429 | Too many requests in a short time. | Wait for the Retry-After delay, then retry. See Rate limits. |
api_error | 500, 503 | Something failed on our side, or is unavailable for a moment. | Retry with the same Idempotency-Key; if it persists, write to support with the request_id. |
The codes you meet first
Requests
code | What happened |
|---|---|
parameter_missing | A required parameter is absent; param names it. |
parameter_unknown | A parameter the request does not take; param names it. Check its spelling. |
parameter_invalid | A parameter has a wrong type or value; param names it. A text field that looks like a card number is refused this way too: card numbers never travel through the API. So is a list asked with both starting_after and ending_before. |
resource_missing | No object with this ID, in this mode. If it exists in the other mode, the message says so: you probably used the wrong key. |
unrecognized_request_url | No API route has this method and this address; the message names both. Check the path and the HTTP method. |
api_version_invalid | The X-AdProMaxPay-Version header, or a webhook endpoint's api_version, names a version that does not exist. The message lists the ones that do. |
amount_too_small | The amount is under the method's minimum: 100.00 MAD for cash, 1.00 MAD for a card. |
amount_too_large | A refund exceeds what remains refundable on the payment. |
payment_method_unavailable | The payment method cannot be used for your account in this mode. |
payment_intent_unexpected_state | The payment intent's status does not allow this action, such as confirming a payment that already succeeded. |
payment_intent_busy | Another request is confirming or changing this payment right now. Nothing was changed: send the request again in a moment. |
charge_unexpected_state | The charge's status does not allow this test helper, such as completing a charge that already succeeded. |
refund_unsupported_payment_method | A cash payment cannot be refunded. |
url_not_allowed | A webhook address that is not https, carries a user name or a password, is not public, or names a host that does not resolve. |
webhook_endpoint_limit_reached | The organization already holds 16 webhook endpoints in this mode. |
webhook_endpoints_busy | Two changes to your endpoints met at the same moment (409). Send the request again. |
idempotency_key_missing | A request that moves money was sent without an Idempotency-Key. |
invalid_request | A request refused without a more precise code; the message says why. |
Keys and permissions
code | What happened |
|---|---|
api_key_missing | The request carried no key. |
api_key_invalid | The key does not exist. Check for a missing character. |
api_key_revoked | The key was revoked. Use another one. |
api_key_expired | The key was rolled and its overlap has ended. Use the new key. |
secret_key_required | A publishable key was used where a secret key is needed. |
live_mode_not_activated | A live key was used before the account was activated. |
account_inactive | The account is suspended or closed. |
test_mode_only | A test helper was called with a live key. |
authentication_required | A 401 without a more precise code; the message says why. |
permission_denied | A 403 without a more precise code; the message says why. |
Payments, idempotency and limits
code | What happened |
|---|---|
card_declined | The card was declined; decline_code may say why. |
idempotency_key_reused | This Idempotency-Key was used for another request. |
idempotency_in_progress | The first request with this Idempotency-Key is still running. |
idempotency_error | An Idempotency-Key refusal without a more precise code; the message says why. |
rate_limit_exceeded | Too many requests; the message names the limit reached and how long to wait. |
test_daily_cap_reached | Test mode's daily cap is reached, until midnight, Morocco time. |
api_error | An error on our side. |
A payment your customer makes on the hosted page does not fail your API call when the card is
declined: your customer sees it and can try again, and you see the reason in the payment intent's
last_payment_error and in the payment_intent.payment_failed event. The reasons are listed in
How payments work.
Read an error in your code
curl -i https://pay-api.adpromax.com/v1/payment_intents/pi_01k7d9x2c5v8b1n4m7q0w3e6rt \
-u "$APX_SECRET_KEY:"const id = 'pi_01k7d9x2c5v8b1n4m7q0w3e6rt'
const response = await fetch(`https://pay-api.adpromax.com/v1/payment_intents/${id}`, {
headers: { Authorization: `Bearer ${process.env.APX_SECRET_KEY}` },
})
if (!response.ok) {
const { error } = await response.json()
if (error.code === 'resource_missing')
console.log('No such payment here:', error.message)
else
console.log(error.type, error.code, error.request_id)
}interface ApiError {
error: {
type: string
code: string
message: string
request_id: string
doc_url: string
param?: string
}
}
const id = 'pi_01k7d9x2c5v8b1n4m7q0w3e6rt'
const response = await fetch(`https://pay-api.adpromax.com/v1/payment_intents/${id}`, {
headers: { Authorization: `Bearer ${process.env.APX_SECRET_KEY}` },
})
if (!response.ok) {
const { error } = (await response.json()) as ApiError
if (error.code === 'resource_missing')
console.log('No such payment here:', error.message)
else
console.log(error.type, error.code, error.request_id)
}import os
import requests
payment_intent_id = "pi_01k7d9x2c5v8b1n4m7q0w3e6rt"
response = requests.get(
f"https://pay-api.adpromax.com/v1/payment_intents/{payment_intent_id}",
auth=(os.environ["APX_SECRET_KEY"], ""),
)
if not response.ok:
error = response.json()["error"]
if error["code"] == "resource_missing":
print("No such payment here:", error["message"])
else:
print(error["type"], error["code"], error["request_id"])<?php
$id = 'pi_01k7d9x2c5v8b1n4m7q0w3e6rt';
$ch = curl_init('https://pay-api.adpromax.com/v1/payment_intents/' . $id);
curl_setopt($ch, CURLOPT_USERPWD, getenv('APX_SECRET_KEY') . ':');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$body = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status >= 400) {
$error = $body['error'];
if ($error['code'] === 'resource_missing') {
echo 'No such payment here: ', $error['message'], PHP_EOL;
} else {
echo $error['type'], ' ', $error['code'], ' ', $error['request_id'], PHP_EOL;
}
}require 'json'
require 'net/http'
id = 'pi_01k7d9x2c5v8b1n4m7q0w3e6rt'
uri = URI("https://pay-api.adpromax.com/v1/payment_intents/#{id}")
request = Net::HTTP::Get.new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(request)
end
unless response.is_a?(Net::HTTPSuccess)
error = JSON.parse(response.body)['error']
if error['code'] == 'resource_missing'
puts "No such payment here: #{error['message']}"
else
puts "#{error['type']} #{error['code']} #{error['request_id']}"
end
endpackage main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
type APIError struct {
Error struct {
Type string `json:"type"`
Code string `json:"code"`
Message string `json:"message"`
RequestID string `json:"request_id"`
} `json:"error"`
}
func main() {
id := "pi_01k7d9x2c5v8b1n4m7q0w3e6rt"
req, err := http.NewRequest(http.MethodGet,
"https://pay-api.adpromax.com/v1/payment_intents/"+id, nil)
if err != nil {
log.Fatal(err)
}
req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode >= 400 {
var failure APIError
if err := json.NewDecoder(res.Body).Decode(&failure); err != nil {
log.Fatal(err)
}
if failure.Error.Code == "resource_missing" {
fmt.Println("No such payment here:", failure.Error.Message)
} else {
fmt.Println(failure.Error.Type, failure.Error.Code,
failure.Error.RequestID)
}
}
}import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Errors {
// The JDK has no JSON parser: a small regex reads one field of the error.
static String field(String json, String name) {
Matcher match = Pattern
.compile("\"" + name + "\"\\s*:\\s*\"([^\"]*)\"")
.matcher(json);
return match.find() ? match.group(1) : "";
}
public static void main(String[] args) throws Exception {
String id = "pi_01k7d9x2c5v8b1n4m7q0w3e6rt";
HttpRequest request = HttpRequest.newBuilder(
URI.create("https://pay-api.adpromax.com/v1/payment_intents/" + id))
.header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
.GET()
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) {
String error = response.body();
if (field(error, "code").equals("resource_missing")) {
System.out.println(
"No such payment here: " + field(error, "message"));
} else {
System.out.println(field(error, "type") + " "
+ field(error, "code") + " " + field(error, "request_id"));
}
}
}
}using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
var json = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower };
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("APX_SECRET_KEY"));
var id = "pi_01k7d9x2c5v8b1n4m7q0w3e6rt";
using var response = await http.GetAsync($"https://pay-api.adpromax.com/v1/payment_intents/{id}");
if (!response.IsSuccessStatusCode)
{
var failure = await response.Content.ReadFromJsonAsync<ApiError>(json);
var error = failure!.Error;
if (error.Code == "resource_missing")
Console.WriteLine($"No such payment here: {error.Message}");
else
Console.WriteLine($"{error.Type} {error.Code} {error.RequestId}");
}
record ApiError(ApiErrorBody Error);
record ApiErrorBody(string Type, string Code, string Message, string RequestId);// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
// serde = { version = "1", features = ["derive"] }
use serde::Deserialize;
#[derive(Deserialize)]
struct ApiError {
error: ErrorBody,
}
#[derive(Deserialize)]
struct ErrorBody {
r#type: String,
code: String,
message: String,
request_id: String,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let id = "pi_01k7d9x2c5v8b1n4m7q0w3e6rt";
let response = reqwest::blocking::Client::new()
.get(format!("https://pay-api.adpromax.com/v1/payment_intents/{id}"))
.bearer_auth(std::env::var("APX_SECRET_KEY")?)
.send()?;
if !response.status().is_success() {
let ApiError { error } = response.json()?;
if error.code == "resource_missing" {
println!("No such payment here: {}", error.message);
} else {
println!("{} {} {}", error.r#type, error.code, error.request_id);
}
}
Ok(())
}Request IDs
Every answer, success or failure, carries a Request-Id header (req_…). The dashboard's request
logs find a request by it, with what you sent and what the API answered. Keep it in your own logs,
and give it when you write to support: it is the fastest way to the answer.