# Handling errors

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

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

```json title="404 Not Found" response
{
  "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](https://pay-docs.adpromax.com/en/api-reference/idempotency).                                                 |
| `rate_limit_error`      | `429`               | Too many requests in a short time.                                                                                                                       | Wait for the `Retry-After` delay, then retry. See [Rate limits](https://pay-docs.adpromax.com/en/api-reference/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](https://pay-docs.adpromax.com/en/payments/how-payments-work).

## Read an error in your code

```bash
curl -i https://pay-api.adpromax.com/v1/payment_intents/pi_01k7d9x2c5v8b1n4m7q0w3e6rt \
  -u "$APX_SECRET_KEY:"
```

```js title="errors.mjs"
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)
}
```

```ts title="errors.ts"
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)
}
```

```python title="errors.py"
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 title="errors.php"
<?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;
    }
}
```

```ruby title="errors.rb"
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
```

```go title="errors.go"
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)
		}
	}
}
```

```java title="Errors.java"
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"));
            }
        }
    }
}
```

```csharp title="Errors.cs"
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);
```

```rust title="main.rs"
// 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.
