# Hosted checkout

A payment page we host for card and cash, in your colours. How to create a checkout session, what your customer sees, and how a session ends.

Hosted checkout is a payment page that AdProMaxPay hosts for you. Your server creates a **checkout
session**, your customer pays on the page by card or in cash, and your server learns the result
from a webhook. You never handle a card number, and the same session works for both methods.

The page:

* shows your logo, your business name, the amount and your support contact;
* speaks French, English and Arabic: it follows your customer's browser, falls back to French when
  it cannot tell, and has a language switch;
* is made for phones first, with the *Pay* button always in reach;
* carries a striped **TEST** band in test mode, so nobody mistakes a test page for a real one.

To try it end to end in ten minutes, follow the [card quickstart](https://pay-docs.adpromax.com/en/get-started/quickstart-card).

## The life of a session

The statuses of a checkout session:

- `open` → `complete`: paid
- `open` → `expired`: ended unpaid, or you expired it

A session is `open` from its creation. It becomes `complete` once its payment intent succeeds, and
`expired` when it reaches its `expires_at` unpaid, or when you expire it. Each session has one
payment intent, named by its `payment_intent`.

## Create a session

Create one session for each order your customer is about to pay, with
`POST /v1/checkout/sessions`. The `Idempotency-Key` header is required: see
[Idempotency](https://pay-docs.adpromax.com/en/api-reference/idempotency).

| Parameter              | What it holds                                                                                                                  |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `amount`               | The amount, an integer in centimes: `45000` is 450.00 MAD.                                                                     |
| `currency`             | Always `mad`.                                                                                                                  |
| `payment_method_types` | The methods your customer may choose: `card`, `cash`, or both. Cash is offered for 100.00 MAD and more.                        |
| `success_url`          | Where the page sends your customer after paying. `https`; `http://localhost` is accepted in test mode only.                    |
| `cancel_url`           | Where *Back* sends your customer before paying. Same rule.                                                                     |
| `expires_at`           | Optional. When the session ends, in Unix seconds, between 30 minutes and 24 hours from now. 24 hours by default.               |
| `customer`             | Optional. A [customer](https://pay-docs.adpromax.com/en/payments/customers) (`cus_…`) of the same organization and mode: the payment appears in their history. |
| `metadata`             | Optional. Your own references, such as the order number: see [Metadata](https://pay-docs.adpromax.com/en/payments/metadata).                                   |

The answer is the session: its `id`, its `url`, its `payment_intent` and its `status`. Store the
payment intent's ID with your order: the events you receive name it.

```bash
curl https://pay-api.adpromax.com/v1/checkout/sessions \
  -u "$APX_SECRET_KEY:" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: checkout-order-2087" \
  -d '{
    "amount": 45000,
    "currency": "mad",
    "payment_method_types": ["card", "cash"],
    "success_url": "https://shop.example.com/orders/2087/thanks",
    "cancel_url": "https://shop.example.com/cart",
    "expires_at": '"$(($(date +%s) + 7200))"',
    "customer": "cus_01k7e2f9g3h6j8k2m5n7p4q1sd",
    "metadata": { "order_id": "2087" }
  }'
```

```js title="session.mjs"
const response = await fetch('https://pay-api.adpromax.com/v1/checkout/sessions', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'checkout-order-2087',
  },
  body: JSON.stringify({
    amount: 45000,
    currency: 'mad',
    payment_method_types: ['card', 'cash'],
    success_url: 'https://shop.example.com/orders/2087/thanks',
    cancel_url: 'https://shop.example.com/cart',
    // Two hours from now, in Unix seconds.
    expires_at: Math.floor(Date.now() / 1000) + 2 * 60 * 60,
    customer: 'cus_01k7e2f9g3h6j8k2m5n7p4q1sd',
    metadata: { order_id: '2087' },
  }),
})
const session = await response.json()
if (!response.ok)
  throw new Error(session.error.message)
console.log(session.payment_intent, session.url)
```

```ts title="session.ts"
interface CheckoutSession {
  id: string
  object: 'checkout.session'
  amount: number
  currency: string
  payment_intent: string
  status: string
  url: string
  expires_at: number
  customer: string | null
  metadata: Record<string, string>
}

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

const response = await fetch('https://pay-api.adpromax.com/v1/checkout/sessions', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'checkout-order-2087',
  },
  body: JSON.stringify({
    amount: 45000,
    currency: 'mad',
    payment_method_types: ['card', 'cash'],
    success_url: 'https://shop.example.com/orders/2087/thanks',
    cancel_url: 'https://shop.example.com/cart',
    // Two hours from now, in Unix seconds.
    expires_at: Math.floor(Date.now() / 1000) + 2 * 60 * 60,
    customer: 'cus_01k7e2f9g3h6j8k2m5n7p4q1sd',
    metadata: { order_id: '2087' },
  }),
})
if (!response.ok) {
  const { error } = (await response.json()) as ApiError
  throw new Error(`${error.code}: ${error.message}`)
}
const session = (await response.json()) as CheckoutSession
console.log(session.payment_intent, session.url)
```

```python title="session.py"
import os
import time

import requests

response = requests.post(
    "https://pay-api.adpromax.com/v1/checkout/sessions",
    auth=(os.environ["APX_SECRET_KEY"], ""),
    headers={"Idempotency-Key": "checkout-order-2087"},
    json={
        "amount": 45000,
        "currency": "mad",
        "payment_method_types": ["card", "cash"],
        "success_url": "https://shop.example.com/orders/2087/thanks",
        "cancel_url": "https://shop.example.com/cart",
        # Two hours from now, in Unix seconds.
        "expires_at": int(time.time()) + 2 * 60 * 60,
        "customer": "cus_01k7e2f9g3h6j8k2m5n7p4q1sd",
        "metadata": {"order_id": "2087"},
    },
)
session = response.json()
response.raise_for_status()
print(session["payment_intent"], session["url"])
```

```php title="session.php"
<?php
$ch = curl_init('https://pay-api.adpromax.com/v1/checkout/sessions');
curl_setopt_array($ch, [
    CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Idempotency-Key: checkout-order-2087',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'amount' => 45000,
        'currency' => 'mad',
        'payment_method_types' => ['card', 'cash'],
        'success_url' => 'https://shop.example.com/orders/2087/thanks',
        'cancel_url' => 'https://shop.example.com/cart',
        // Two hours from now, in Unix seconds.
        'expires_at' => time() + 2 * 60 * 60,
        'customer' => 'cus_01k7e2f9g3h6j8k2m5n7p4q1sd',
        'metadata' => ['order_id' => '2087'],
    ]),
    CURLOPT_RETURNTRANSFER => true,
]);
$session = json_decode(curl_exec($ch), true);
echo $session['payment_intent'], ' ', $session['url'], PHP_EOL;
```

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

uri = URI('https://pay-api.adpromax.com/v1/checkout/sessions')
request = Net::HTTP::Post.new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
request['Content-Type'] = 'application/json'
request['Idempotency-Key'] = 'checkout-order-2087'
request.body = JSON.generate(
  amount: 45000,
  currency: 'mad',
  payment_method_types: %w[card cash],
  success_url: 'https://shop.example.com/orders/2087/thanks',
  cancel_url: 'https://shop.example.com/cart',
  # Two hours from now, in Unix seconds.
  expires_at: Time.now.to_i + 2 * 60 * 60,
  customer: 'cus_01k7e2f9g3h6j8k2m5n7p4q1sd',
  metadata: { order_id: '2087' }
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end
session = JSON.parse(response.body)
raise session.dig('error', 'message') unless response.is_a?(Net::HTTPSuccess)

puts "#{session['payment_intent']} #{session['url']}"
```

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

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

type CheckoutSession struct {
	ID            string `json:"id"`
	PaymentIntent string `json:"payment_intent"`
	Status        string `json:"status"`
	URL           string `json:"url"`
	ExpiresAt     int64  `json:"expires_at"`
}

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

func main() {
	// Two hours from now, in Unix seconds.
	expiresAt := time.Now().Add(2 * time.Hour).Unix()
	body, err := json.Marshal(map[string]any{
		"amount":               45000,
		"currency":             "mad",
		"payment_method_types": []string{"card", "cash"},
		"success_url":          "https://shop.example.com/orders/2087/thanks",
		"cancel_url":           "https://shop.example.com/cart",
		"expires_at":           expiresAt,
		"customer":             "cus_01k7e2f9g3h6j8k2m5n7p4q1sd",
		"metadata":             map[string]string{"order_id": "2087"},
	})
	if err != nil {
		log.Fatal(err)
	}
	req, err := http.NewRequest(http.MethodPost,
		"https://pay-api.adpromax.com/v1/checkout/sessions", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}
	req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Idempotency-Key", "checkout-order-2087")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer res.Body.Close()

	if res.StatusCode >= 400 {
		var apiErr APIError
		json.NewDecoder(res.Body).Decode(&apiErr)
		log.Fatal(apiErr.Error.Message)
	}
	var session CheckoutSession
	if err := json.NewDecoder(res.Body).Decode(&session); err != nil {
		log.Fatal(err)
	}
	fmt.Println(session.PaymentIntent, session.URL)
}
```

```java title="Session.java"
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Instant;

public class Session {
    public static void main(String[] args) throws Exception {
        // Two hours from now, in Unix seconds.
        long expiresAt = Instant.now().getEpochSecond() + 2 * 60 * 60;
        String body = """
            {
              "amount": 45000,
              "currency": "mad",
              "payment_method_types": ["card", "cash"],
              "success_url": "https://shop.example.com/orders/2087/thanks",
              "cancel_url": "https://shop.example.com/cart",
              "expires_at": %d,
              "customer": "cus_01k7e2f9g3h6j8k2m5n7p4q1sd",
              "metadata": { "order_id": "2087" }
            }
            """.formatted(expiresAt);
        HttpRequest request = HttpRequest.newBuilder(
                URI.create("https://pay-api.adpromax.com/v1/checkout/sessions"))
            .header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
            .header("Content-Type", "application/json")
            .header("Idempotency-Key", "checkout-order-2087")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() >= 400) {
            throw new IllegalStateException(response.body());
        }
        // The session as JSON: read payment_intent and url with a JSON library.
        System.out.println(response.statusCode() + " " + response.body());
    }
}
```

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

using var request = new HttpRequestMessage(
    HttpMethod.Post, "https://pay-api.adpromax.com/v1/checkout/sessions");
request.Headers.Add("Idempotency-Key", "checkout-order-2087");
request.Content = JsonContent.Create(new
{
    amount = 45000,
    currency = "mad",
    payment_method_types = new[] { "card", "cash" },
    success_url = "https://shop.example.com/orders/2087/thanks",
    cancel_url = "https://shop.example.com/cart",
    // Two hours from now, in Unix seconds.
    expires_at = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 2 * 60 * 60,
    customer = "cus_01k7e2f9g3h6j8k2m5n7p4q1sd",
    metadata = new { order_id = "2087" },
});

using var response = await http.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
    var failure = await response.Content.ReadFromJsonAsync<ApiError>(json);
    throw new InvalidOperationException(failure?.Error.Message);
}
var session = await response.Content.ReadFromJsonAsync<CheckoutSession>(json);
Console.WriteLine($"{session!.PaymentIntent} {session.Url}");

record CheckoutSession(string Id, string PaymentIntent, string Status, string Url);
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::time::{SystemTime, UNIX_EPOCH};

use serde::Deserialize;
use serde_json::json;

#[derive(Deserialize)]
struct CheckoutSession {
    payment_intent: String,
    url: String,
}

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

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

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Two hours from now, in Unix seconds.
    let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
    let expires_at = now + 2 * 60 * 60;

    let response = reqwest::blocking::Client::new()
        .post("https://pay-api.adpromax.com/v1/checkout/sessions")
        .bearer_auth(std::env::var("APX_SECRET_KEY")?)
        .header("Idempotency-Key", "checkout-order-2087")
        .json(&json!({
            "amount": 45000,
            "currency": "mad",
            "payment_method_types": ["card", "cash"],
            "success_url": "https://shop.example.com/orders/2087/thanks",
            "cancel_url": "https://shop.example.com/cart",
            "expires_at": expires_at,
            "customer": "cus_01k7e2f9g3h6j8k2m5n7p4q1sd",
            "metadata": { "order_id": "2087" }
        }))
        .send()?;
    if !response.status().is_success() {
        let failure: ApiError = response.json()?;
        return Err(failure.error.message.into());
    }
    let session: CheckoutSession = response.json()?;
    println!("{} {}", session.payment_intent, session.url);
    Ok(())
}
```

```json title="200 OK" response
{
  "id": "cs_01k7e3m5q8w2r6t9y4b7n1v5ca",
  "amount": 45000,
  "currency": "mad",
  "payment_method_types": ["card", "cash"],
  "payment_intent": "pi_01k7e3m5px4z8c2v6b9n3m7hra",
  "customer": "cus_01k7e2f9g3h6j8k2m5n7p4q1sd",
  "status": "open",
  "url": "https://pay.example.com/c/cs_01k7e3m5q8w2r6t9y4b7n1v5ca#…",
  "success_url": "https://shop.example.com/orders/2087/thanks",
  "cancel_url": "https://shop.example.com/cart",
  "expires_at": 1790854200,
  "metadata": { "order_id": "2087" },
  "livemode": false
}
```

## Send your customer to the page

Redirect your customer's browser to the session's `url` with an HTTP `303`, as in
[the quickstart](https://pay-docs.adpromax.com/en/get-started/quickstart-card#3-send-your-customer-to-the-page).

Keep the whole address, including the part after `#`. That part is your customer's access to this
session: browsers never send it to a server, so it stays out of logs. Do not log it, shorten it or
send it to anyone but your customer.

## What your customer sees

First, the amount and a choice between the methods you offered.

**By card:**

1. Your customer chooses card and presses *Pay*. The page prepares the payment, then explains that
   it is taking them to the secure card page of our card partner. 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).
2. They type their card on that page. If their bank asks for 3-D Secure, they confirm there.
3. They come back to our page, which shows "Confirming with your bank…" until the payment is
   confirmed, then the success page.
4. After 5 seconds, the success page sends them to your `success_url`.

If the card is declined, the page gives the reason and offers *Try another card*, or *Pay in cash
instead* when you offered cash and the amount is at least 100.00 MAD. See
[Card payments](https://pay-docs.adpromax.com/en/payments/card-payments).

**In cash:** your customer gives their full name and their city (a phone number is optional), then
receives a voucher with a payment code to pay at a partner payment agent. See
[Cash payments](https://pay-docs.adpromax.com/en/payments/cash-payments).

For both methods, your customer may give an email address for a receipt; the receipt is sent only
when they do.

The page's *← Back* link, which names your business, sends your customer to your `cancel_url`. The
session stays `open` until it expires, so the same `url` still works if they come back.

## Know when it is paid

Fulfil the order on one of these events, never on the return to your `success_url`:

| Event                        | When                                                   |
| ---------------------------- | ------------------------------------------------------ |
| `payment_intent.succeeded`   | The session's payment intent succeeded.                |
| `checkout.session.completed` | The session was paid: it names its `payment_intent`.   |
| `checkout.session.expired`   | The session ended unpaid. Free the order or the stock. |

You also receive the payment intent's other events, such as `payment_intent.payment_failed` after a
declined card. Read [Redirects and webhooks](https://pay-docs.adpromax.com/en/payments/how-payments-work#redirects-and-webhooks-which-one-to-trust)
for why the return proves nothing.

A cash code your customer already received keeps its own deadline, about three days, even when the
session's `expires_at` comes first: they can still pay it.

## Expire a session

If the order is canceled or the stock is gone, expire the open session yourself with
`POST /v1/checkout/sessions/{id}/expire`. The session becomes `expired` and its unpaid payment intent
is canceled: your customer can no longer pay it. Only an `open` session can be expired.

To read a session again, use `GET /v1/checkout/sessions/{id}`; to list them,
`GET /v1/checkout/sessions`.

```bash
curl -X POST \
  https://pay-api.adpromax.com/v1/checkout/sessions/cs_01k7e3m5q8w2r6t9y4b7n1v5ca/expire \
  -u "$APX_SECRET_KEY:"
```

```js title="expire.mjs"
const id = 'cs_01k7e3m5q8w2r6t9y4b7n1v5ca'
const response = await fetch(`https://pay-api.adpromax.com/v1/checkout/sessions/${id}/expire`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.APX_SECRET_KEY}` },
})
const session = await response.json()
console.log(session.status) // expired
```

```ts title="expire.ts"
interface CheckoutSession {
  id: string
  object: 'checkout.session'
  status: string
}

const id = 'cs_01k7e3m5q8w2r6t9y4b7n1v5ca'
const response = await fetch(`https://pay-api.adpromax.com/v1/checkout/sessions/${id}/expire`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.APX_SECRET_KEY}` },
})
const session = (await response.json()) as CheckoutSession
console.log(session.status) // expired
```

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

import requests

session_id = "cs_01k7e3m5q8w2r6t9y4b7n1v5ca"
response = requests.post(
    f"https://pay-api.adpromax.com/v1/checkout/sessions/{session_id}/expire",
    auth=(os.environ["APX_SECRET_KEY"], ""),
)
print(response.json()["status"])  # expired
```

```php title="expire.php"
<?php
$id = 'cs_01k7e3m5q8w2r6t9y4b7n1v5ca';
$ch = curl_init("https://pay-api.adpromax.com/v1/checkout/sessions/{$id}/expire");
curl_setopt_array($ch, [
    CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
]);
$session = json_decode(curl_exec($ch), true);
echo $session['status'], PHP_EOL; // expired
```

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

id = 'cs_01k7e3m5q8w2r6t9y4b7n1v5ca'
uri = URI("https://pay-api.adpromax.com/v1/checkout/sessions/#{id}/expire")
request = Net::HTTP::Post.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
puts JSON.parse(response.body)['status'] # expired
```

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

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

type CheckoutSession struct {
	ID     string `json:"id"`
	Status string `json:"status"`
}

func main() {
	id := "cs_01k7e3m5q8w2r6t9y4b7n1v5ca"
	req, err := http.NewRequest(http.MethodPost,
		"https://pay-api.adpromax.com/v1/checkout/sessions/"+id+"/expire", 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()

	var session CheckoutSession
	if err := json.NewDecoder(res.Body).Decode(&session); err != nil {
		log.Fatal(err)
	}
	fmt.Println(session.Status) // expired
}
```

```java title="Expire.java"
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Expire {
    public static void main(String[] args) throws Exception {
        String id = "cs_01k7e3m5q8w2r6t9y4b7n1v5ca";
        HttpRequest request = HttpRequest.newBuilder(
                URI.create("https://pay-api.adpromax.com/v1/checkout/sessions/" + id + "/expire"))
            .header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
            .POST(HttpRequest.BodyPublishers.noBody())
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        // The session as JSON: its status is now "expired".
        System.out.println(response.body());
    }
}
```

```csharp title="Expire.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 = "cs_01k7e3m5q8w2r6t9y4b7n1v5ca";
using var response = await http.PostAsync(
    $"https://pay-api.adpromax.com/v1/checkout/sessions/{id}/expire", null);
var session = await response.Content.ReadFromJsonAsync<CheckoutSession>(json);
Console.WriteLine(session!.Status); // expired

record CheckoutSession(string Id, string Status);
```

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

#[derive(Deserialize)]
struct CheckoutSession {
    status: String,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let id = "cs_01k7e3m5q8w2r6t9y4b7n1v5ca";
    let session: CheckoutSession = reqwest::blocking::Client::new()
        .post(format!("https://pay-api.adpromax.com/v1/checkout/sessions/{id}/expire"))
        .bearer_auth(std::env::var("APX_SECRET_KEY")?)
        .send()?
        .json()?;
    println!("{}", session.status); // expired
    Ok(())
}
```

## Your branding

Set the page's look once, on the dashboard under **Settings → Branding**:

* a logo, in PNG, JPEG or WebP, up to 512 KB;
* the display name and your support email and phone;
* a button colour: if white text would be hard to read on it, it is darkened until it is readable,
  and the settings page tells you;
* a white background, or one lightly tinted with your colour.

The voucher page of a cash payment uses the same settings.

## No code at all

A **payment link**, made on the dashboard, opens the same hosted page for anyone who has the link.
See [No code: a payment link](https://pay-docs.adpromax.com/en/get-started/quickstart-card#no-code-a-payment-link).
