Errors

Handling errors

The error object, the seven error types, the codes you meet first, and how to handle each.

Open as Markdown
On this page
  1. The error object
  2. Error types
  3. The codes you meet first
  4. Requests
  5. Keys and permissions
  6. Payments, idempotency and limits
  7. Read an error in your code
  8. Request IDs

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

Response404 Not Found
{
  "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"
  }
}
FieldWhat it holds
typeOne of the seven types below: the broad kind of failure.
codeThe precise reason, stable over time: branch on it.
decline_codeFor a declined card: the bank's reason, such as insufficient_funds.
paramThe parameter at fault, as a path (metadata.order_id), when there is one.
messageAn 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_urlThe page that explains the code.
request_idThe request's ID (req_…), also in the Request-Id header of every answer.

Error types

typeStatusWhat happenedWhat to do
invalid_request_error400, 404, 409The 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_error401No key, or a key that does not work.Check the key and its mode.
permission_error403The key works but may not do this.See the code: a live key before activation, a test helper with a live key…
card_error402A card was declined.Show your customer a message and let them try another card.
idempotency_error400, 409An Idempotency-Key was reused for another request, or its first request still runs.See Idempotency.
rate_limit_error429Too many requests in a short time.Wait for the Retry-After delay, then retry. See Rate limits.
api_error500, 503Something 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

codeWhat happened
parameter_missingA required parameter is absent; param names it.
parameter_unknownA parameter the request does not take; param names it. Check its spelling.
parameter_invalidA 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_missingNo 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_urlNo API route has this method and this address; the message names both. Check the path and the HTTP method.
api_version_invalidThe 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_smallThe amount is under the method's minimum: 100.00 MAD for cash, 1.00 MAD for a card.
amount_too_largeA refund exceeds what remains refundable on the payment.
payment_method_unavailableThe payment method cannot be used for your account in this mode.
payment_intent_unexpected_stateThe payment intent's status does not allow this action, such as confirming a payment that already succeeded.
payment_intent_busyAnother request is confirming or changing this payment right now. Nothing was changed: send the request again in a moment.
charge_unexpected_stateThe charge's status does not allow this test helper, such as completing a charge that already succeeded.
refund_unsupported_payment_methodA cash payment cannot be refunded.
url_not_allowedA 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_reachedThe organization already holds 16 webhook endpoints in this mode.
webhook_endpoints_busyTwo changes to your endpoints met at the same moment (409). Send the request again.
idempotency_key_missingA request that moves money was sent without an Idempotency-Key.
invalid_requestA request refused without a more precise code; the message says why.

Keys and permissions

codeWhat happened
api_key_missingThe request carried no key.
api_key_invalidThe key does not exist. Check for a missing character.
api_key_revokedThe key was revoked. Use another one.
api_key_expiredThe key was rolled and its overlap has ended. Use the new key.
secret_key_requiredA publishable key was used where a secret key is needed.
live_mode_not_activatedA live key was used before the account was activated.
account_inactiveThe account is suspended or closed.
test_mode_onlyA test helper was called with a live key.
authentication_requiredA 401 without a more precise code; the message says why.
permission_deniedA 403 without a more precise code; the message says why.

Payments, idempotency and limits

codeWhat happened
card_declinedThe card was declined; decline_code may say why.
idempotency_key_reusedThis Idempotency-Key was used for another request.
idempotency_in_progressThe first request with this Idempotency-Key is still running.
idempotency_errorAn Idempotency-Key refusal without a more precise code; the message says why.
rate_limit_exceededToo many requests; the message names the limit reached and how long to wait.
test_daily_cap_reachedTest mode's daily cap is reached, until midnight, Morocco time.
api_errorAn 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
  • Node.js
  • TypeScript
  • Python
  • PHP
  • Ruby
  • Go
  • Java
  • C#
  • Rust
errors.mjserrors.tserrors.pyerrors.phperrors.rberrors.goErrors.javaErrors.csmain.rs
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
end
package 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.