# Card payments

How a card payment works, 3-D Secure, declines and retries, and how to send your customer to the card page from your own page.

Your customer types their card on the **secure card page of our card partner**, never on your site
and never on ours. You never see, send or store a card number, and the API refuses any text that
looks like one. In test mode that page is our simulation marked TEST, which takes only the
[test cards](https://pay-docs.adpromax.com/en/sandbox/test-cards).

There are two ways to get your customer there:

* **Hosted checkout** (recommended): our page offers card, and cash if you want, and does every
  step below for you. See [Hosted checkout](https://pay-docs.adpromax.com/en/payments/hosted-checkout).
* **Your own page**: you confirm the payment intent yourself and send your customer's browser to
  the card page, as shown on this page.

An embedded card form, on your own page, is planned for later. We will announce it in the
[changelog](https://pay-docs.adpromax.com/en/changelog).

## What happens during a card payment

1. A payment intent is confirmed for a card: an **attempt** (`ch_…`) starts, and the payment intent
   is `requires_action`.
2. Your customer's browser reaches the card page and they type their card.
3. If their bank asks for it, they confirm with **3-D Secure** on a page of their bank. You have
   nothing to do: it happens on the card page. A failed check ends the attempt with
   `authentication_failed`.
4. The card partner confirms the payment to us. The payment intent becomes `succeeded` and you
   receive `payment_intent.succeeded`.

A card page stays usable for about **30 minutes**; after that the attempt ends `expired` and the
payment intent goes back to `requires_payment_method`. A card payment is at least **1.00 MAD**
(`100`).

## From your own page

Create a payment intent for a card, then confirm it with:

* `payment_method_type`: `card`;
* `return_url`: where your customer comes back after the card page, in `https`. Put your own order
  reference in it, such as `https://shop.example.com/orders/1042/return`: the return carries no
  proof of payment.

Both requests need their own `Idempotency-Key`. The answer is the payment intent, in
`requires_action`, with a `next_action` of type `form_post`: an address (`url`), a `method` and
`fields` that your customer's browser must post to reach the card page. The fields change from one
payment to the next: post them all, as they are.

```bash
PI=$(curl -s https://pay-api.adpromax.com/v1/payment_intents \
  -u "$APX_SECRET_KEY:" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-create" \
  -d '{"amount": 25000, "currency": "mad",
       "payment_method_types": ["card"]}' | jq -r .id)

curl https://pay-api.adpromax.com/v1/payment_intents/$PI/confirm \
  -u "$APX_SECRET_KEY:" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-confirm" \
  -d '{"payment_method_type": "card",
       "return_url": "https://shop.example.com/orders/1042/return"}'
```

```js title="card.mjs"
const api = 'https://pay-api.adpromax.com/v1'
const auth = `Bearer ${process.env.APX_SECRET_KEY}`

async function post(path, key, body) {
  const response = await fetch(`${api}${path}`, {
    method: 'POST',
    headers: {
      'Authorization': auth,
      'Content-Type': 'application/json',
      'Idempotency-Key': key,
    },
    body: JSON.stringify(body),
  })
  const answer = await response.json()
  if (!response.ok)
    throw new Error(answer.error.message)
  return answer
}

const intent = await post('/payment_intents', 'order-1042-create', {
  amount: 25000,
  currency: 'mad',
  payment_method_types: ['card'],
})
const confirmed = await post(`/payment_intents/${intent.id}/confirm`,
  'order-1042-confirm', {
    payment_method_type: 'card',
    return_url: 'https://shop.example.com/orders/1042/return',
  })
const formPost = confirmed.next_action.form_post
```

```ts title="card.ts"
interface FormPost {
  url: string
  method: string
  fields: Record<string, string>
}

interface PaymentIntent {
  id: string
  object: 'payment_intent'
  status: string
  next_action: { type: 'form_post', form_post: FormPost } | null
}

interface ApiError {
  error: { type: string, code: string, message: string, request_id: string }
}

const api = 'https://pay-api.adpromax.com/v1'
const auth = `Bearer ${process.env.APX_SECRET_KEY}`

async function post(path: string, key: string, body: object): Promise<PaymentIntent> {
  const response = await fetch(`${api}${path}`, {
    method: 'POST',
    headers: {
      'Authorization': auth,
      'Content-Type': 'application/json',
      'Idempotency-Key': key,
    },
    body: JSON.stringify(body),
  })
  if (!response.ok) {
    const { error } = (await response.json()) as ApiError
    throw new Error(`${error.code}: ${error.message}`)
  }
  return (await response.json()) as PaymentIntent
}

const intent = await post('/payment_intents', 'order-1042-create', {
  amount: 25000,
  currency: 'mad',
  payment_method_types: ['card'],
})
const confirmed = await post(`/payment_intents/${intent.id}/confirm`,
  'order-1042-confirm', {
    payment_method_type: 'card',
    return_url: 'https://shop.example.com/orders/1042/return',
  })
const formPost = confirmed.next_action?.form_post
```

```python title="card.py"
import os

import requests

API = "https://pay-api.adpromax.com/v1"
AUTH = (os.environ["APX_SECRET_KEY"], "")

def post(path, key, body):
    response = requests.post(
        f"{API}{path}", auth=AUTH, json=body,
        headers={"Idempotency-Key": key},
    )
    answer = response.json()
    response.raise_for_status()
    return answer

intent = post("/payment_intents", "order-1042-create", {
    "amount": 25000,
    "currency": "mad",
    "payment_method_types": ["card"],
})
confirmed = post(f"/payment_intents/{intent['id']}/confirm",
                 "order-1042-confirm", {
                     "payment_method_type": "card",
                     "return_url": "https://shop.example.com/orders/1042/return",
                 })
form_post = confirmed["next_action"]["form_post"]
```

```php title="card.php"
<?php
function post(string $path, string $key, array $body): array
{
    $ch = curl_init('https://pay-api.adpromax.com/v1' . $path);
    curl_setopt_array($ch, [
        CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => [
            'Content-Type: application/json',
            'Idempotency-Key: ' . $key,
        ],
        CURLOPT_POSTFIELDS => json_encode($body),
        CURLOPT_RETURNTRANSFER => true,
    ]);
    $answer = json_decode(curl_exec($ch), true);
    return $answer;
}

$intent = post('/payment_intents', 'order-1042-create', [
    'amount' => 25000,
    'currency' => 'mad',
    'payment_method_types' => ['card'],
]);
$confirmed = post("/payment_intents/{$intent['id']}/confirm",
    'order-1042-confirm', [
        'payment_method_type' => 'card',
        'return_url' => 'https://shop.example.com/orders/1042/return',
    ]);
$formPost = $confirmed['next_action']['form_post'];
```

```ruby title="card.rb"
require 'json'
require 'net/http'

API = 'https://pay-api.adpromax.com/v1'

def post(path, key, body)
  uri = URI("#{API}#{path}")
  request = Net::HTTP::Post.new(uri)
  request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
  request['Content-Type'] = 'application/json'
  request['Idempotency-Key'] = key
  request.body = JSON.generate(body)
  response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
    http.request(request)
  end
  answer = JSON.parse(response.body)
  raise answer.dig('error', 'message') unless response.is_a?(Net::HTTPSuccess)

  answer
end

intent = post('/payment_intents', 'order-1042-create',
              amount: 25000, currency: 'mad', payment_method_types: ['card'])
confirmed = post("/payment_intents/#{intent['id']}/confirm", 'order-1042-confirm',
                 payment_method_type: 'card',
                 return_url: 'https://shop.example.com/orders/1042/return')
form_post = confirmed.dig('next_action', 'form_post')
```

```go title="card.go"
package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/http"
	"os"
)

const api = "https://pay-api.adpromax.com/v1"

type FormPost struct {
	URL    string            `json:"url"`
	Method string            `json:"method"`
	Fields map[string]string `json:"fields"`
}

type PaymentIntent struct {
	ID         string `json:"id"`
	Status     string `json:"status"`
	NextAction struct {
		FormPost FormPost `json:"form_post"`
	} `json:"next_action"`
}

type APIError struct {
	Error struct {
		Code    string `json:"code"`
		Message string `json:"message"`
	} `json:"error"`
}

func post(path, key string, body any) (*PaymentIntent, error) {
	payload, _ := json.Marshal(body)
	req, err := http.NewRequest(http.MethodPost, api+path, bytes.NewReader(payload))
	if err != nil {
		return nil, err
	}
	req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Idempotency-Key", key)
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode >= 400 {
		var apiErr APIError
		json.NewDecoder(res.Body).Decode(&apiErr)
		return nil, errors.New(apiErr.Error.Message)
	}
	var intent PaymentIntent
	return &intent, json.NewDecoder(res.Body).Decode(&intent)
}

func main() {
	intent, err := post("/payment_intents", "order-1042-create", map[string]any{
		"amount":               25000,
		"currency":             "mad",
		"payment_method_types": []string{"card"},
	})
	if err != nil {
		log.Fatal(err)
	}
	confirmed, err := post("/payment_intents/"+intent.ID+"/confirm",
		"order-1042-confirm", map[string]any{
			"payment_method_type": "card",
			"return_url":          "https://shop.example.com/orders/1042/return",
		})
	if err != nil {
		log.Fatal(err)
	}
	formPost := confirmed.NextAction.FormPost
	fmt.Println(formPost.Method, formPost.URL)
}
```

```java title="Card.java"
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.regex.Pattern;

public class Card {
    static final String API = "https://pay-api.adpromax.com/v1";
    static final HttpClient HTTP = HttpClient.newHttpClient();

    static String post(String path, String key, String body) throws Exception {
        HttpRequest request = HttpRequest.newBuilder(URI.create(API + path))
            .header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
            .header("Content-Type", "application/json")
            .header("Idempotency-Key", key)
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();
        HttpResponse<String> response =
            HTTP.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() >= 400) {
            throw new IllegalStateException(response.body());
        }
        return response.body();
    }

    public static void main(String[] args) throws Exception {
        String intent = post("/payment_intents", "order-1042-create", """
            {"amount": 25000, "currency": "mad", "payment_method_types": ["card"]}
            """);
        // The JDK has no JSON parser: a regex reads the id here.
        var id = Pattern.compile("\"id\"\\s*:\\s*\"(pi_[^\"]+)\"").matcher(intent);
        if (!id.find()) {
            throw new IllegalStateException("No id: " + intent);
        }
        String confirmed = post("/payment_intents/" + id.group(1) + "/confirm",
            "order-1042-confirm", """
            {"payment_method_type": "card",
             "return_url": "https://shop.example.com/orders/1042/return"}
            """);
        // next_action.form_post: url, method, fields
        System.out.println(confirmed);
    }
}
```

```csharp title="Card.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 { BaseAddress = new Uri("https://pay-api.adpromax.com/v1/") };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("APX_SECRET_KEY"));

var intent = await Post("payment_intents", "order-1042-create", new
{
    amount = 25000,
    currency = "mad",
    payment_method_types = new[] { "card" },
});
var confirm = $"payment_intents/{intent.Id}/confirm";
var confirmed = await Post(confirm, "order-1042-confirm", new
{
    payment_method_type = "card",
    return_url = "https://shop.example.com/orders/1042/return",
});
var formPost = confirmed.NextAction!.FormPost;
Console.WriteLine($"{formPost.Method} {formPost.Url}");

async Task<PaymentIntent> Post(string path, string key, object body)
{
    using var request = new HttpRequestMessage(HttpMethod.Post, path)
    {
        Content = JsonContent.Create(body),
    };
    request.Headers.Add("Idempotency-Key", key);
    using var response = await http.SendAsync(request);
    if (!response.IsSuccessStatusCode)
    {
        var failure = await response.Content.ReadFromJsonAsync<ApiError>(json);
        throw new InvalidOperationException(failure?.Error.Message);
    }
    return (await response.Content.ReadFromJsonAsync<PaymentIntent>(json))!;
}

record PaymentIntent(string Id, string Status, NextAction? NextAction);
record NextAction(string Type, FormPost FormPost);
record FormPost(string Url, string Method, Dictionary<string, string> Fields);
record ApiError(ApiErrorBody Error);
record ApiErrorBody(string Code, string Message);
```

```rust title="main.rs"
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
//             serde = { version = "1", features = ["derive"] }
//             serde_json = "1"
use std::collections::HashMap;

use reqwest::blocking::Client;
use serde::Deserialize;
use serde_json::{json, Value};

const API: &str = "https://pay-api.adpromax.com/v1";

#[derive(Deserialize)]
struct PaymentIntent {
    id: String,
    next_action: Option<NextAction>,
}

#[derive(Deserialize)]
struct NextAction {
    form_post: FormPost,
}

#[derive(Deserialize)]
struct FormPost {
    url: String,
    method: String,
    fields: HashMap<String, String>,
}

#[derive(Deserialize)]
struct ApiError {
    error: ErrorBody,
}

#[derive(Deserialize)]
struct ErrorBody {
    message: String,
}

fn post(
    client: &Client,
    path: &str,
    key: &str,
    body: Value,
) -> Result<PaymentIntent, Box<dyn std::error::Error>> {
    let response = client
        .post(format!("{API}{path}"))
        .bearer_auth(std::env::var("APX_SECRET_KEY")?)
        .header("Idempotency-Key", key)
        .json(&body)
        .send()?;
    if !response.status().is_success() {
        let failure: ApiError = response.json()?;
        return Err(failure.error.message.into());
    }
    Ok(response.json()?)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new();
    let create = json!({
        "amount": 25000,
        "currency": "mad",
        "payment_method_types": ["card"],
    });
    let intent = post(&client, "/payment_intents", "order-1042-create", create)?;
    let confirm = json!({
        "payment_method_type": "card",
        "return_url": "https://shop.example.com/orders/1042/return",
    });
    let path = format!("/payment_intents/{}/confirm", intent.id);
    let confirmed = post(&client, &path, "order-1042-confirm", confirm)?;
    let next_action = confirmed.next_action.ok_or("no form to post")?;
    let form_post = next_action.form_post;
    let fields = form_post.fields.len();
    println!("{} {} ({fields} fields)", form_post.method, form_post.url);
    Ok(())
}
```

```json title="200 OK" response
{
  "id": "pi_01k7e5r2t8w4y6b9c3n7m1x5qa",
  "object": "payment_intent",
  "amount": 25000,
  "currency": "mad",
  "status": "requires_action",
  "payment_method_types": ["card"],
  "latest_charge": "ch_01k7e5r3a6s9d2f5g8h1j4k7mb",
  "next_action": {
    "type": "form_post",
    "form_post": {
      "url": "https://pay.example.com/…",
      "method": "POST",
      "fields": { "…": "…" }
    }
  },
  "livemode": false
}
```

## Send your customer to the card page

Answer your customer's browser with a page holding a form built from `next_action.form_post`: one
hidden input per field, posted to `url`. The form submits itself at once; a visible *Continue*
button is the fallback when scripts are blocked. Escape every value you write into the page.

If your customer reloads the page before paying, read the payment intent again and post the same
`form_post`: it stays valid while the attempt is waiting.

```bash
# Shows what the page must post: the form_post of the confirmed payment intent.
curl -s https://pay-api.adpromax.com/v1/payment_intents/pi_01k7e5r2t8w4y6b9c3n7m1x5qa \
  -u "$APX_SECRET_KEY:" | jq .next_action.form_post
```

```js title="card-form.mjs"
const escape = value => String(value)
  .replace(/[&<>"']/g, character => `&#${character.charCodeAt(0)};`)

// formPost is next_action.form_post of the confirmed payment intent.
export function cardForm(formPost) {
  const inputs = Object.entries(formPost.fields)
    .map(([name, value]) =>
      `<input type="hidden" name="${escape(name)}" value="${escape(value)}">`)
    .join('\n')
  return `<!doctype html>
<form id="card" method="${escape(formPost.method)}" action="${escape(formPost.url)}">
${inputs}
<button type="submit">Continue</button>
</form>
<script>document.getElementById('card').submit()</script>`
}

// Express: res.send(cardForm(confirmed.next_action.form_post))
```

```ts title="card-form.ts"
import type { Response } from 'express'

interface FormPost {
  url: string
  method: string
  fields: Record<string, string>
}

const escape = (value: string): string => value
  .replace(/[&<>"']/g, character => `&#${character.charCodeAt(0)};`)

// formPost is next_action.form_post of the confirmed payment intent.
export function cardForm(formPost: FormPost): string {
  const inputs = Object.entries(formPost.fields)
    .map(([name, value]) =>
      `<input type="hidden" name="${escape(name)}" value="${escape(value)}">`)
    .join('\n')
  return `<!doctype html>
<form id="card" method="${escape(formPost.method)}" action="${escape(formPost.url)}">
${inputs}
<button type="submit">Continue</button>
</form>
<script>document.getElementById('card').submit()</script>`
}

// In an Express route: sendCardForm(res, confirmed.next_action.form_post)
export function sendCardForm(res: Response, formPost: FormPost): void {
  res.type('html').send(cardForm(formPost))
}
```

```python title="app.py"
from flask import render_template_string

CARD_FORM = """<!doctype html>
<form id="card" method="{{ form.method }}" action="{{ form.url }}">
  {% for name, value in form.fields.items() %}
  <input type="hidden" name="{{ name }}" value="{{ value }}">
  {% endfor %}
  <button type="submit">Continue</button>
</form>
<script>document.getElementById('card').submit()</script>"""

def card_form(form_post):
    # Jinja escapes every value.
    return render_template_string(CARD_FORM, form=form_post)
```

```php title="card-form.php"
<?php
// $formPost is next_action.form_post of the confirmed payment intent.
$e = fn ($value) => htmlspecialchars((string) $value, ENT_QUOTES);
?>
<!doctype html>
<form id="card" method="<?= $e($formPost['method']) ?>" action="<?= $e($formPost['url']) ?>">
<?php foreach ($formPost['fields'] as $name => $value): ?>
  <input type="hidden" name="<?= $e($name) ?>" value="<?= $e($value) ?>">
<?php endforeach; ?>
  <button type="submit">Continue</button>
</form>
<script>document.getElementById('card').submit()</script>
```

```ruby title="card_form.rb"
require 'sinatra'

CARD_FORM = <<~HTML
  <!doctype html>
  <form id="card" method="<%= h form['method'] %>" action="<%= h form['url'] %>">
  <% form['fields'].each do |name, value| %>
    <input type="hidden" name="<%= h name %>" value="<%= h value %>">
  <% end %>
    <button type="submit">Continue</button>
  </form>
  <script>document.getElementById('card').submit()</script>
HTML

helpers do
  def h(value)
    Rack::Utils.escape_html(value.to_s)
  end

  # form_post is next_action.form_post of the confirmed payment intent.
  def card_form(form_post)
    erb CARD_FORM, locals: { form: form_post }
  end
end
```

```go title="card_form.go"
package main

import (
	"html/template"
	"net/http"
)

type FormPost struct {
	URL    string            `json:"url"`
	Method string            `json:"method"`
	Fields map[string]string `json:"fields"`
}

// html/template escapes every value.
var cardFormPage = template.Must(template.New("card").Parse(`<!doctype html>
<form id="card" method="{{.Method}}" action="{{.URL}}">
  {{range $name, $value := .Fields}}
  <input type="hidden" name="{{$name}}" value="{{$value}}">
  {{end}}
  <button type="submit">Continue</button>
</form>
<script>document.getElementById('card').submit()</script>`))

// formPost is next_action.form_post of the confirmed payment intent.
func cardForm(w http.ResponseWriter, formPost FormPost) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	cardFormPage.Execute(w, formPost)
}
```

```java title="CardFormController.java"
import java.util.Map;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.util.HtmlUtils;

public class CardFormController {
    record FormPost(String url, String method, Map<String, String> fields) {}

    // formPost is next_action.form_post of the confirmed payment intent.
    static ResponseEntity<String> cardForm(FormPost formPost) {
        StringBuilder inputs = new StringBuilder();
        formPost.fields().forEach((name, value) -> inputs.append(
            "<input type=\"hidden\" name=\"%s\" value=\"%s\">\n"
                .formatted(escape(name), escape(value))));
        String page = """
            <!doctype html>
            <form id="card" method="%s" action="%s">
            %s<button type="submit">Continue</button>
            </form>
            <script>document.getElementById('card').submit()</script>
            """.formatted(escape(formPost.method()), escape(formPost.url()), inputs);
        return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(page);
    }

    static String escape(String value) {
        return HtmlUtils.htmlEscape(value);
    }
}
```

```csharp title="Program.cs"
using System.Net;
using System.Text;

// formPost is next_action.form_post of the confirmed payment intent.
// In a route: return CardForm(confirmed.NextAction.FormPost);
static IResult CardForm(FormPost formPost)
{
    var inputs = new StringBuilder();
    foreach (var (name, value) in formPost.Fields)
    {
        inputs.AppendLine(
            $"<input type=\"hidden\" name=\"{E(name)}\" value=\"{E(value)}\">");
    }
    var page = $"""
        <!doctype html>
        <form id="card" method="{E(formPost.Method)}" action="{E(formPost.Url)}">
        {inputs}<button type="submit">Continue</button>
        </form>
        <script>document.getElementById('card').submit()</script>
        """;
    return Results.Content(page, "text/html");
}

static string E(string value) => WebUtility.HtmlEncode(value);

record FormPost(string Url, string Method, Dictionary<string, string> Fields);
```

```rust title="main.rs"
// Cargo.toml: axum = "0.8", serde = { version = "1", features = ["derive"] }
use std::collections::BTreeMap;

use axum::response::Html;
use serde::Deserialize;

#[derive(Deserialize)]
struct FormPost {
    url: String,
    method: String,
    fields: BTreeMap<String, String>,
}

fn escape(value: &str) -> String {
    value
        .replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
        .replace('\'', "&#39;")
}

// form_post is next_action.form_post of the confirmed payment intent.
fn card_form(form_post: &FormPost) -> Html<String> {
    let inputs: String = form_post
        .fields
        .iter()
        .map(|(name, value)| {
            format!(
                "<input type=\"hidden\" name=\"{}\" value=\"{}\">\n",
                escape(name),
                escape(value)
            )
        })
        .collect();
    Html(format!(
        r#"<!doctype html>
<form id="card" method="{}" action="{}">
{}<button type="submit">Continue</button>
</form>
<script>document.getElementById('card').submit()</script>"#,
        escape(&form_post.method),
        escape(&form_post.url),
        inputs
    ))
}
```

## When your customer comes back

After the card page, your customer's browser comes back to your `return_url`, whatever happened.
That return proves nothing: show "We are confirming your payment", then read the payment intent
with your secret key, and deliver on the webhook `payment_intent.succeeded`. A confirmation can
arrive after your customer is back: see [How payments work](https://pay-docs.adpromax.com/en/payments/how-payments-work#redirects-and-webhooks-which-one-to-trust).

## Declines and retries

A declined attempt sends the payment intent back to `requires_payment_method`, with the reason in
`last_payment_error`, and you receive `payment_intent.payment_failed`.

| `last_payment_error.code` | What happened                                                                 | What your customer can do   |
| ------------------------- | ----------------------------------------------------------------------------- | --------------------------- |
| `card_declined`           | The bank declined. `decline_code` may say more, such as `insufficient_funds`. | Try another card.           |
| `expired_card`            | The card has expired.                                                         | Try another card.           |
| `authentication_failed`   | 3-D Secure did not succeed.                                                   | Try again, or another card. |
| `processing_error`        | The payment could not be processed.                                           | Try again.                  |

Your customer can try again on the **same** payment intent: each try is a new attempt, and the
payment intent still succeeds once.

* With hosted checkout, the page offers *Try another card*, and *Pay in cash instead* when you
  offered cash.
* With your own page, confirm the payment intent again with a **new** `Idempotency-Key`, and send
  your customer to the new `form_post`.

Show your customer a plain sentence, not the code. The numbers that produce each case in test mode
are in [Test cards](https://pay-docs.adpromax.com/en/sandbox/test-cards).

## Refunds

A succeeded card payment can be refunded, in full or in part: see [Refunds](https://pay-docs.adpromax.com/en/payments/refunds).
