Payments

Customers

Record who pays, link their payments to them, and find them again by email.

Open as Markdown
On this page
  1. Create a customer
  2. Link a payment to a customer
  3. Find a customer by email
  4. Every customer request
  5. On the dashboard
  6. Collect only what you need

A customer records who pays you: an email, a name, a phone number and your own references. It is optional: a payment works without one. With one, the dashboard groups a customer's payments on one page, and you find them again by email.

A customer belongs to one organization and one mode. A customer created with a test key exists in test mode only.

Create a customer

Send what you know; every field is optional:

  • email;
  • name;
  • phone, in international format with the country code: +212612345678;
  • metadata: your own references, such as your account number for this person. See Metadata.

An Idempotency-Key is accepted, not required. With one, a request sent twice creates one customer.

The answer is the customer, with its ID (cus_…). Store it with your own record of the person.

  • curl
  • Node.js
  • TypeScript
  • Python
  • PHP
  • Ruby
  • Go
  • Java
  • C#
  • Rust
customer.mjscustomer.tscustomer.pycustomer.phpcustomer.rbcustomer.goCustomer.javaCustomer.csmain.rs
curl https://pay-api.adpromax.com/v1/customers \
  -u "$APX_SECRET_KEY:" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "name": "Salma Idrissi",
    "phone": "+212612345678",
    "metadata": { "account_id": "8841" }
  }'
const response = await fetch('https://pay-api.adpromax.com/v1/customers', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    email: '[email protected]',
    name: 'Salma Idrissi',
    phone: '+212612345678',
    metadata: { account_id: '8841' },
  }),
})
const customer = await response.json()
if (!response.ok)
  throw new Error(customer.error.message)
console.log(customer.id)
interface Customer {
  id: string
  object: 'customer'
  email: string | null
  name: string | null
  phone: 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/customers', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    email: '[email protected]',
    name: 'Salma Idrissi',
    phone: '+212612345678',
    metadata: { account_id: '8841' },
  }),
})
if (!response.ok) {
  const { error } = (await response.json()) as ApiError
  throw new Error(`${error.code}: ${error.message}`)
}
const customer = (await response.json()) as Customer
console.log(customer.id)
import os

import requests

response = requests.post(
    "https://pay-api.adpromax.com/v1/customers",
    auth=(os.environ["APX_SECRET_KEY"], ""),
    json={
        "email": "[email protected]",
        "name": "Salma Idrissi",
        "phone": "+212612345678",
        "metadata": {"account_id": "8841"},
    },
)
customer = response.json()
response.raise_for_status()
print(customer["id"])
<?php
$ch = curl_init('https://pay-api.adpromax.com/v1/customers');
curl_setopt_array($ch, [
    CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode([
        'email' => '[email protected]',
        'name' => 'Salma Idrissi',
        'phone' => '+212612345678',
        'metadata' => ['account_id' => '8841'],
    ]),
    CURLOPT_RETURNTRANSFER => true,
]);
$customer = json_decode(curl_exec($ch), true);
echo $customer['id'], PHP_EOL;
require 'json'
require 'net/http'

uri = URI('https://pay-api.adpromax.com/v1/customers')
request = Net::HTTP::Post.new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
request['Content-Type'] = 'application/json'
request.body = JSON.generate(
  email: '[email protected]',
  name: 'Salma Idrissi',
  phone: '+212612345678',
  metadata: { account_id: '8841' }
)

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

puts customer['id']
package main

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

type Customer struct {
	ID    string `json:"id"`
	Email string `json:"email"`
	Name  string `json:"name"`
}

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

func main() {
	body, err := json.Marshal(map[string]any{
		"email":    "[email protected]",
		"name":     "Salma Idrissi",
		"phone":    "+212612345678",
		"metadata": map[string]string{"account_id": "8841"},
	})
	if err != nil {
		log.Fatal(err)
	}
	req, err := http.NewRequest(http.MethodPost,
		"https://pay-api.adpromax.com/v1/customers", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}
	req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
	req.Header.Set("Content-Type", "application/json")

	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 customer Customer
	if err := json.NewDecoder(res.Body).Decode(&customer); err != nil {
		log.Fatal(err)
	}
	fmt.Println(customer.ID)
}
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Customer {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "email": "[email protected]",
              "name": "Salma Idrissi",
              "phone": "+212612345678",
              "metadata": { "account_id": "8841" }
            }
            """;
        HttpRequest request = HttpRequest.newBuilder(
                URI.create("https://pay-api.adpromax.com/v1/customers"))
            .header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
            .header("Content-Type", "application/json")
            .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 customer as JSON: read its id with a JSON library.
        System.out.println(response.body());
    }
}
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 response = await http.PostAsJsonAsync("https://pay-api.adpromax.com/v1/customers", new
{
    email = "[email protected]",
    name = "Salma Idrissi",
    phone = "+212612345678",
    metadata = new { account_id = "8841" },
});
if (!response.IsSuccessStatusCode)
{
    var failure = await response.Content.ReadFromJsonAsync<ApiError>(json);
    throw new InvalidOperationException(failure?.Error.Message);
}
var customer = await response.Content.ReadFromJsonAsync<Customer>(json);
Console.WriteLine(customer!.Id);

record Customer(string Id, string? Email, string? Name, string? Phone);
record ApiError(ApiErrorBody Error);
record ApiErrorBody(string Code, string Message);
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
//             serde = { version = "1", features = ["derive"] }
//             serde_json = "1"
use serde::Deserialize;
use serde_json::json;

#[derive(Deserialize)]
struct Customer {
    id: String,
}

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

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

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let response = reqwest::blocking::Client::new()
        .post("https://pay-api.adpromax.com/v1/customers")
        .bearer_auth(std::env::var("APX_SECRET_KEY")?)
        .json(&json!({
            "email": "[email protected]",
            "name": "Salma Idrissi",
            "phone": "+212612345678",
            "metadata": { "account_id": "8841" }
        }))
        .send()?;
    if !response.status().is_success() {
        let failure: ApiError = response.json()?;
        return Err(failure.error.message.into());
    }
    let customer: Customer = response.json()?;
    println!("{}", customer.id);
    Ok(())
}
Response200 OK
{
  "id": "cus_01k7e3m9q2v5b8n1x4c7z0r6tw",
  "object": "customer",
  "email": "[email protected]",
  "name": "Salma Idrissi",
  "phone": "+212612345678",
  "metadata": { "account_id": "8841" },
  "created": 1790855000,
  "livemode": false
}

Pass the customer's ID as customer when you create a payment intent or a checkout session. The payment then appears on the customer's page in the dashboard, and the payment intent names its customer.

The customer must belong to your organization and to the same mode as the payment. Any other ID is refused with 400, resource_missing, and param set to customer: a customer created with a test key cannot pay in live mode.

To list a customer's payments, filter the payment intents: GET /v1/payment_intents?customer=cus_….

Find a customer by email

GET /v1/customers lists your customers, newest first; with email, only those with that address. Before you create a customer, look for one: your records then hold one customer per person.

A list answers a list object: data holds the customers, has_more says whether more follow. Ask for up to 100 at a time with limit (10 by default), then for the next ones with starting_after set to the last ID you received; ending_before goes the other way.

  • curl
  • Node.js
  • TypeScript
  • Python
  • PHP
  • Ruby
  • Go
  • Java
  • C#
  • Rust
find-customer.mjsfind-customer.tsfind_customer.pyfind_customer.phpfind_customer.rbfind_customer.goFindCustomer.javaFindCustomer.csmain.rs
EMAIL=[email protected]
CUS=$(curl -s -G https://pay-api.adpromax.com/v1/customers \
  -u "$APX_SECRET_KEY:" \
  --data-urlencode "email=$EMAIL" \
  -d limit=1 | jq -r '.data[0].id // empty')

if [ -z "$CUS" ]; then
  CUS=$(curl -s https://pay-api.adpromax.com/v1/customers \
    -u "$APX_SECRET_KEY:" \
    -H "Content-Type: application/json" \
    -d "{\"email\": \"$EMAIL\", \"name\": \"Salma Idrissi\"}" | jq -r .id)
fi
echo "$CUS"
const api = 'https://pay-api.adpromax.com/v1'
const headers = {
  'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
  'Content-Type': 'application/json',
}
const email = '[email protected]'

const query = new URLSearchParams({ email, limit: '1' })
const found = await fetch(`${api}/customers?${query}`, { headers })
  .then(response => response.json())
let customer = found.data[0]
if (customer === undefined) {
  const response = await fetch(`${api}/customers`, {
    method: 'POST',
    headers,
    body: JSON.stringify({ email, name: 'Salma Idrissi' }),
  })
  customer = await response.json()
}
console.log(customer.id)
interface Customer {
  id: string
  object: 'customer'
  email: string | null
  name: string | null
}

interface CustomerList {
  object: 'list'
  data: Customer[]
  has_more: boolean
}

const api = 'https://pay-api.adpromax.com/v1'
const headers = {
  'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
  'Content-Type': 'application/json',
}
const email = '[email protected]'

const query = new URLSearchParams({ email, limit: '1' })
const found = await fetch(`${api}/customers?${query}`, { headers })
  .then(response => response.json() as Promise<CustomerList>)
let customer: Customer | undefined = found.data[0]
if (customer === undefined) {
  const response = await fetch(`${api}/customers`, {
    method: 'POST',
    headers,
    body: JSON.stringify({ email, name: 'Salma Idrissi' }),
  })
  customer = (await response.json()) as Customer
}
console.log(customer.id)
import os

import requests

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

found = requests.get(
    f"{API}/customers", auth=AUTH, params={"email": email, "limit": 1}
).json()
customer = found["data"][0] if found["data"] else None
if customer is None:
    customer = requests.post(
        f"{API}/customers", auth=AUTH,
        json={"email": email, "name": "Salma Idrissi"},
    ).json()
print(customer["id"])
<?php
function api(string $method, string $path, ?array $body = null): array
{
    $ch = curl_init('https://pay-api.adpromax.com/v1' . $path);
    curl_setopt_array($ch, [
        CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
        CURLOPT_RETURNTRANSFER => true,
    ]);
    if ($body !== null) {
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
    }
    $answer = json_decode(curl_exec($ch), true);
    return $answer;
}

$email = '[email protected]';
$query = http_build_query(['email' => $email, 'limit' => 1]);
$customer = api('GET', "/customers?{$query}")['data'][0] ?? null;
if ($customer === null) {
    $customer = api('POST', '/customers', [
        'email' => $email,
        'name' => 'Salma Idrissi',
    ]);
}
echo $customer['id'], PHP_EOL;
require 'json'
require 'net/http'

API = 'https://pay-api.adpromax.com/v1'
email = '[email protected]'

def call(request)
  uri = request.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
  JSON.parse(response.body)
end

uri = URI("#{API}/customers")
uri.query = URI.encode_www_form(email: email, limit: 1)
customer = call(Net::HTTP::Get.new(uri))['data'].first
if customer.nil?
  request = Net::HTTP::Post.new(URI("#{API}/customers"))
  request['Content-Type'] = 'application/json'
  request.body = JSON.generate(email: email, name: 'Salma Idrissi')
  customer = call(request)
end
puts customer['id']
package main

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

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

type Customer struct {
	ID    string `json:"id"`
	Email string `json:"email"`
}

type CustomerList struct {
	Data []Customer `json:"data"`
}

func call(method, path string, body io.Reader, into any) error {
	req, err := http.NewRequest(method, api+path, body)
	if err != nil {
		return err
	}
	req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
	req.Header.Set("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	return json.NewDecoder(res.Body).Decode(into)
}

func main() {
	email := "[email protected]"
	query := url.Values{"email": {email}, "limit": {"1"}}

	var found CustomerList
	path := "/customers?" + query.Encode()
	if err := call(http.MethodGet, path, nil, &found); err != nil {
		log.Fatal(err)
	}
	var customer Customer
	if len(found.Data) > 0 {
		customer = found.Data[0]
	} else {
		body, _ := json.Marshal(map[string]string{
			"email": email,
			"name":  "Salma Idrissi",
		})
		err := call(http.MethodPost, "/customers", bytes.NewReader(body), &customer)
		if err != nil {
			log.Fatal(err)
		}
	}
	fmt.Println(customer.ID)
}
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;

public class FindCustomer {
    static final String API = "https://pay-api.adpromax.com/v1";
    static final HttpClient HTTP = HttpClient.newHttpClient();
    // The JDK has no JSON parser: a regex reads a customer's id here.
    static final Pattern ID = Pattern.compile("\"id\"\\s*:\\s*\"(cus_[^\"]+)\"");

    static String send(HttpRequest.Builder request) throws Exception {
        request.header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"));
        request.header("Content-Type", "application/json");
        return HTTP.send(request.build(), HttpResponse.BodyHandlers.ofString()).body();
    }

    public static void main(String[] args) throws Exception {
        String email = "[email protected]";
        String query = "email=" + URLEncoder.encode(email, StandardCharsets.UTF_8);
        String found = send(HttpRequest.newBuilder(
            URI.create(API + "/customers?" + query + "&limit=1")).GET());
        var id = ID.matcher(found);
        if (!id.find()) {
            String created = send(HttpRequest.newBuilder(URI.create(API + "/customers"))
                .POST(HttpRequest.BodyPublishers.ofString("""
                    {"email": "%s", "name": "Salma Idrissi"}
                    """.formatted(email))));
            id = ID.matcher(created);
            if (!id.find()) {
                throw new IllegalStateException(created);
            }
        }
        System.out.println(id.group(1));
    }
}
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 email = "[email protected]";
var found = await http.GetFromJsonAsync<CustomerList>(
    $"customers?email={Uri.EscapeDataString(email)}&limit=1", json);
var customer = found!.Data.FirstOrDefault();
if (customer is null)
{
    using var response = await http.PostAsJsonAsync(
        "customers", new { email, name = "Salma Idrissi" });
    customer = await response.Content.ReadFromJsonAsync<Customer>(json);
}
Console.WriteLine(customer!.Id);

record CustomerList(List<Customer> Data, bool HasMore);
record Customer(string Id, string? Email, string? Name);
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
//             serde = { version = "1", features = ["derive"] }
//             serde_json = "1"
use reqwest::blocking::Client;
use serde::Deserialize;
use serde_json::json;

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

#[derive(Deserialize)]
struct Customer {
    id: String,
}

#[derive(Deserialize)]
struct CustomerList {
    data: Vec<Customer>,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new();
    let key = std::env::var("APX_SECRET_KEY")?;
    let email = "[email protected]";

    let found: CustomerList = client
        .get(format!("{API}/customers"))
        .bearer_auth(&key)
        .query(&[("email", email), ("limit", "1")])
        .send()?
        .json()?;
    let first = found.data.into_iter().next();
    let customer = match first {
        Some(customer) => customer,
        None => client
            .post(format!("{API}/customers"))
            .bearer_auth(&key)
            .json(&json!({ "email": email, "name": "Salma Idrissi" }))
            .send()?
            .json()?,
    };
    println!("{}", customer.id);
    Ok(())
}
Response200 OK
{
  "object": "list",
  "data": [
    {
      "id": "cus_01k7e3m9q2v5b8n1x4c7z0r6tw",
      "object": "customer",
      "email": "[email protected]",
      "name": "Salma Idrissi",
      "phone": "+212612345678",
      "metadata": { "account_id": "8841" },
      "created": 1790855000,
      "livemode": false
    }
  ],
  "has_more": false,
  "url": "/v1/customers"
}

Every customer request

RequestWhat it does
POST /v1/customersCreates a customer.
GET /v1/customers/{id}Retrieves a customer.
POST /v1/customers/{id}Updates a customer: send the fields that change.
DELETE /v1/customers/{id}Deletes a customer. The answer is { "id": "cus_…", "object": "customer", "deleted": true }.
GET /v1/customersLists your customers, newest first; email keeps those with that address.
GET /v1/payment_intents?customer=cus_…Lists a customer's payments.

On the dashboard

  • Customers lists them with their email, phone, number of payments, total paid and last payment, and a search box.
  • New customer creates one by hand: name, email, phone and metadata.
  • A customer's page shows its payments and its metadata. Edit the details in place, on the page.

Collect only what you need

A customer holds personal data. Ask your customers only for what your business needs, and keep secrets out of metadata: your whole team and your webhook endpoints can read it.