Docs
API 2026-05-27.dahlia Dashboard

Payments

Keep a buyer as a Customer: create, find, update and delete it, and store its tax IDs.

A Customer (cus_...) is a buyer your store keeps on file: an email, a name, a phone, addresses, your own metadata and up to five tax IDs. Create one for a buyer you expect to see again. You can then name it in customer on a PaymentIntent, list the cards saved to it, and look it up by email or by search. A one-off payment does not need a Customer.

This page walks one Customer from create to delete. Every request below runs on a fresh test store in the order shown, and each id comes from an earlier answer. The full list of fields is in the Customers API reference.

Create a Customer#

Send what you know about the buyer. Every field is optional. To store tax IDs at the same time, add tax_id_data entries, numbered from 0, each with a type and a value. Make one Idempotency-Key for each Customer you mean to create: a retry with that key answers the same Customer instead of a second one.

Create a Customer with a CPF
curl https://api.fluveo.com/v1/customers \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: crm-a1042-create" \
  -d email=ada@example.com \
  -d name="Ada Lovelace" \
  -d "metadata[crm_id]=A-1042" \
  -d "tax_id_data[0][type]=br_cpf" \
  -d "tax_id_data[0][value]=52998224725"
const response = await fetch("https://api.fluveo.com/v1/customers", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_test_...",
    "Idempotency-Key": "crm-a1042-create",
  },
  body: new URLSearchParams({
    email: "ada@example.com",
    name: "Ada Lovelace",
    "metadata[crm_id]": "A-1042",
    "tax_id_data[0][type]": "br_cpf",
    "tax_id_data[0][value]": "52998224725",
  }),
});
const customer = await response.json();
console.log(customer.id);
import requests

response = requests.post(
    "https://api.fluveo.com/v1/customers",
    headers={
        "Authorization": "Bearer sk_test_...",
        "Idempotency-Key": "crm-a1042-create",
    },
    data={
        "email": "ada@example.com",
        "name": "Ada Lovelace",
        "metadata[crm_id]": "A-1042",
        "tax_id_data[0][type]": "br_cpf",
        "tax_id_data[0][value]": "52998224725",
    },
)
customer = response.json()
print(customer["id"])
<?php
$ch = curl_init("https://api.fluveo.com/v1/customers");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer sk_test_...",
        "Idempotency-Key: crm-a1042-create",
    ],
    CURLOPT_POSTFIELDS => http_build_query([
        "email" => "ada@example.com",
        "name" => "Ada Lovelace",
        "metadata" => ["crm_id" => "A-1042"],
        "tax_id_data" => [["type" => "br_cpf", "value" => "52998224725"]],
    ]),
]);
$customer = json_decode(curl_exec($ch), true);
echo $customer["id"];

The answer is the new Customer. Keep its id: every later call on this page uses it.

200 response (shortened)
{
  "id": "cus_...",
  "object": "customer",
  "email": "ada@example.com",
  "name": "Ada Lovelace",
  "phone": null,
  "description": null,
  "metadata": { "crm_id": "A-1042" },
  "tax_exempt": "none",
  "preferred_locales": [],
  "created": 1762862400,
  "livemode": false
}

The tax IDs are kept, but a Customer shows its tax_ids list only when you ask for it with expand[]=tax_ids (see Expanding responses).

Retrieve a Customer#

GET /v1/customers/{customer} reads one Customer. Here it also expands tax_ids, so you can see the CPF stored by the create.

Retrieve the Customer with its tax IDs
curl -G https://api.fluveo.com/v1/customers/cus_... \
  -H "Authorization: Bearer sk_test_..." \
  -d "expand[]=tax_ids"
const response = await fetch("https://api.fluveo.com/v1/customers/cus_...?expand[]=tax_ids", {
  headers: { Authorization: "Bearer sk_test_..." },
});
const customer = await response.json();
console.log(customer.tax_ids.data);
import requests

response = requests.get(
    "https://api.fluveo.com/v1/customers/cus_...",
    headers={"Authorization": "Bearer sk_test_..."},
    params={"expand[]": "tax_ids"},
)
customer = response.json()
print(customer["tax_ids"]["data"])
<?php
$ch = curl_init("https://api.fluveo.com/v1/customers/cus_...?expand[]=tax_ids");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer sk_test_..."],
]);
$customer = json_decode(curl_exec($ch), true);
print_r($customer["tax_ids"]["data"]);

Fluveo stores a tax ID in the usual written form of its type, so the digits 52998224725 come back as 529.982.247-25.

200 response (shortened)
{
  "id": "cus_...",
  "object": "customer",
  "email": "ada@example.com",
  "tax_ids": {
    "object": "list",
    "data": [
      {
        "id": "txi_...",
        "object": "tax_id",
        "type": "br_cpf",
        "value": "529.982.247-25",
        "customer": "cus_..."
      }
    ],
    "has_more": false,
    "url": "/v1/customers/cus_.../tax_ids"
  }
}

Update a Customer#

POST /v1/customers/{customer} changes only the fields you send and leaves the rest as they were. metadata merges key by key: a new key is added, and a key sent with an empty value is removed. Tax IDs are not changed here: use the tax ID calls below.

Update the description and the metadata
curl https://api.fluveo.com/v1/customers/cus_... \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: crm-a1042-update-1" \
  -d description="Wholesale buyer" \
  -d "metadata[segment]=wholesale"
const response = await fetch("https://api.fluveo.com/v1/customers/cus_...", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_test_...",
    "Idempotency-Key": "crm-a1042-update-1",
  },
  body: new URLSearchParams({
    description: "Wholesale buyer",
    "metadata[segment]": "wholesale",
  }),
});
const customer = await response.json();
console.log(customer.description, customer.metadata);
import requests

response = requests.post(
    "https://api.fluveo.com/v1/customers/cus_...",
    headers={
        "Authorization": "Bearer sk_test_...",
        "Idempotency-Key": "crm-a1042-update-1",
    },
    data={
        "description": "Wholesale buyer",
        "metadata[segment]": "wholesale",
    },
)
customer = response.json()
print(customer["description"], customer["metadata"])
<?php
$ch = curl_init("https://api.fluveo.com/v1/customers/cus_...");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer sk_test_...",
        "Idempotency-Key: crm-a1042-update-1",
    ],
    CURLOPT_POSTFIELDS => http_build_query([
        "description" => "Wholesale buyer",
        "metadata" => ["segment" => "wholesale"],
    ]),
]);
$customer = json_decode(curl_exec($ch), true);
echo $customer["description"];

The Customer comes back with the new description and both metadata keys.

200 response (shortened)
{
  "id": "cus_...",
  "object": "customer",
  "description": "Wholesale buyer",
  "metadata": { "crm_id": "A-1042", "segment": "wholesale" }
}

Find Customers#

List by email#

GET /v1/customers lists your Customers, newest first, paged with limit (1 to 100, default 10), starting_after and ending_before (see Pagination). email narrows the list to one exact address. The match is case-sensitive: Ada@example.com does not find ada@example.com. created filters on the creation time.

List the Customers with one email
curl -G https://api.fluveo.com/v1/customers \
  -H "Authorization: Bearer sk_test_..." \
  -d email=ada@example.com \
  -d limit=3
const query = new URLSearchParams({ email: "ada@example.com", limit: "3" });
const response = await fetch("https://api.fluveo.com/v1/customers?" + query, {
  headers: { Authorization: "Bearer sk_test_..." },
});
const list = await response.json();
console.log(list.data.length, list.has_more);
import requests

response = requests.get(
    "https://api.fluveo.com/v1/customers",
    headers={"Authorization": "Bearer sk_test_..."},
    params={"email": "ada@example.com", "limit": 3},
)
page = response.json()
print(len(page["data"]), page["has_more"])
<?php
$query = http_build_query(["email" => "ada@example.com", "limit" => 3]);
$ch = curl_init("https://api.fluveo.com/v1/customers?" . $query);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer sk_test_..."],
]);
$list = json_decode(curl_exec($ch), true);
echo count($list["data"]);
200 response (shortened)
{
  "object": "list",
  "data": [{ "id": "cus_...", "object": "customer", "email": "ada@example.com" }],
  "has_more": false,
  "url": "/v1/customers"
}

GET /v1/customers/search takes a query and answers a search_result with data, has_more and next_page. A Customer can be found as soon as the write that made it returns.

Search by email
curl -G https://api.fluveo.com/v1/customers/search \
  -H "Authorization: Bearer sk_test_..." \
  --data-urlencode 'query=email:"ada@example.com"'
const query = new URLSearchParams({ query: 'email:"ada@example.com"' });
const response = await fetch("https://api.fluveo.com/v1/customers/search?" + query, {
  headers: { Authorization: "Bearer sk_test_..." },
});
const result = await response.json();
console.log(result.data.length);
import requests

response = requests.get(
    "https://api.fluveo.com/v1/customers/search",
    headers={"Authorization": "Bearer sk_test_..."},
    params={"query": 'email:"ada@example.com"'},
)
result = response.json()
print(len(result["data"]))
<?php
$query = http_build_query(["query" => 'email:"ada@example.com"']);
$ch = curl_init("https://api.fluveo.com/v1/customers/search?" . $query);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer sk_test_..."],
]);
$result = json_decode(curl_exec($ch), true);
echo count($result["data"]);
200 response (shortened)
{
  "object": "search_result",
  "url": "/v1/customers/search",
  "data": [{ "id": "cus_...", "object": "customer", "email": "ada@example.com" }],
  "has_more": false,
  "next_page": null
}

A query is made of clauses: a field, an operator and a value in quotes. The fields are email, name, phone, created and metadata["key"].

QueryFinds
email:"ada@example.com"that address, in any letter case
name:"ada lovelace"names holding those whole words, in that order, in any case
name~"love"names holding "love" anywhere; a ~ value needs at least 3 characters, and a shorter one finds nothing
metadata["crm_id"]:"A-1042"Customers whose crm_id metadata is A-1042
created>1762000000Customers made after that Unix time; created takes =, :, >, >=, < and <= with a whole number
-phone:nullCustomers that have a phone; a - negates a clause
email:"a@example.com" OR email:"b@example.com"either address

Join up to 10 clauses with AND or with OR, never both in one query, and without parentheses. A space between clauses counts as AND. When has_more is true, send next_page back as page, with the same query, to read the next page. Add expand[]=total_count to get total_count, the number of matches. A deleted Customer is never found.

Tax IDs#

A Customer holds up to five tax IDs (txi_...). Each has a type and a value. The type is br_cpf for a CPF or br_cnpj for a CNPJ. Send the value with or without its dots, dash and slash: Fluveo checks that it has the form of its type and answers it in that written form. Add tax IDs with tax_id_data when you create the Customer, or one at a time with the calls below. A tax ID cannot be edited: delete it and add the new one.

Add a tax ID#

Add a CNPJ to the Customer
curl https://api.fluveo.com/v1/customers/cus_.../tax_ids \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: crm-a1042-cnpj" \
  -d type=br_cnpj \
  -d value=11222333000181
const response = await fetch("https://api.fluveo.com/v1/customers/cus_.../tax_ids", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_test_...",
    "Idempotency-Key": "crm-a1042-cnpj",
  },
  body: new URLSearchParams({ type: "br_cnpj", value: "11222333000181" }),
});
const taxId = await response.json();
console.log(taxId.id, taxId.value);
import requests

response = requests.post(
    "https://api.fluveo.com/v1/customers/cus_.../tax_ids",
    headers={
        "Authorization": "Bearer sk_test_...",
        "Idempotency-Key": "crm-a1042-cnpj",
    },
    data={"type": "br_cnpj", "value": "11222333000181"},
)
tax_id = response.json()
print(tax_id["id"], tax_id["value"])
<?php
$ch = curl_init("https://api.fluveo.com/v1/customers/cus_.../tax_ids");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer sk_test_...",
        "Idempotency-Key: crm-a1042-cnpj",
    ],
    CURLOPT_POSTFIELDS => http_build_query(["type" => "br_cnpj", "value" => "11222333000181"]),
]);
$taxId = json_decode(curl_exec($ch), true);
echo $taxId["id"];

The answer is the new tax ID. Keep its id for the calls below.

200 response (shortened)
{
  "id": "txi_...",
  "object": "tax_id",
  "type": "br_cnpj",
  "value": "11.222.333/0001-81",
  "customer": "cus_...",
  "created": 1762862460,
  "livemode": false
}

List a Customer's tax IDs#

GET /v1/customers/{customer}/tax_ids answers them oldest first, paged with limit, starting_after and ending_before. This Customer now has two: the CPF from the create and the CNPJ.

List the tax IDs
curl https://api.fluveo.com/v1/customers/cus_.../tax_ids \
  -H "Authorization: Bearer sk_test_..."
const response = await fetch("https://api.fluveo.com/v1/customers/cus_.../tax_ids", {
  headers: { Authorization: "Bearer sk_test_..." },
});
const list = await response.json();
console.log(list.data.map((taxId) => taxId.type));
import requests

response = requests.get(
    "https://api.fluveo.com/v1/customers/cus_.../tax_ids",
    headers={"Authorization": "Bearer sk_test_..."},
)
print([tax_id["type"] for tax_id in response.json()["data"]])
<?php
$ch = curl_init("https://api.fluveo.com/v1/customers/cus_.../tax_ids");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer sk_test_..."],
]);
$list = json_decode(curl_exec($ch), true);
echo count($list["data"]);
200 response (shortened)
{
  "object": "list",
  "data": [
    { "id": "txi_...", "object": "tax_id", "type": "br_cpf", "value": "529.982.247-25" },
    { "id": "txi_...", "object": "tax_id", "type": "br_cnpj", "value": "11.222.333/0001-81" }
  ],
  "has_more": false,
  "url": "/v1/customers/cus_.../tax_ids"
}

Retrieve a tax ID#

Retrieve the CNPJ
curl https://api.fluveo.com/v1/customers/cus_.../tax_ids/txi_... \
  -H "Authorization: Bearer sk_test_..."
const response = await fetch("https://api.fluveo.com/v1/customers/cus_.../tax_ids/txi_...", {
  headers: { Authorization: "Bearer sk_test_..." },
});
const taxId = await response.json();
console.log(taxId.value);
import requests

response = requests.get(
    "https://api.fluveo.com/v1/customers/cus_.../tax_ids/txi_...",
    headers={"Authorization": "Bearer sk_test_..."},
)
print(response.json()["value"])
<?php
$ch = curl_init("https://api.fluveo.com/v1/customers/cus_.../tax_ids/txi_...");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer sk_test_..."],
]);
$taxId = json_decode(curl_exec($ch), true);
echo $taxId["value"];

The answer is the same tax ID object the create returned.

Delete a tax ID#

Delete the CNPJ
curl -X DELETE https://api.fluveo.com/v1/customers/cus_.../tax_ids/txi_... \
  -H "Authorization: Bearer sk_test_..."
const response = await fetch("https://api.fluveo.com/v1/customers/cus_.../tax_ids/txi_...", {
  method: "DELETE",
  headers: { Authorization: "Bearer sk_test_..." },
});
const deleted = await response.json();
console.log(deleted.deleted);
import requests

response = requests.delete(
    "https://api.fluveo.com/v1/customers/cus_.../tax_ids/txi_...",
    headers={"Authorization": "Bearer sk_test_..."},
)
print(response.json()["deleted"])
<?php
$ch = curl_init("https://api.fluveo.com/v1/customers/cus_.../tax_ids/txi_...");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => "DELETE",
    CURLOPT_HTTPHEADER => ["Authorization: Bearer sk_test_..."],
]);
$deleted = json_decode(curl_exec($ch), true);
var_dump($deleted["deleted"]);
200 response
{
  "id": "txi_...",
  "object": "tax_id",
  "deleted": true
}

The tax ID leaves the Customer's list at once, and its id then answers 404 resource_missing.

Use a Customer in a payment#

Name it on a PaymentIntent#

Send the Customer's id in customer when you create a PaymentIntent. The payment is then tied to that buyer, and the PaymentIntent and its charge answer that customer. The amount is in the currency's smallest unit: 2000 is $20.00. This request only creates the PaymentIntent; to confirm and pay it, see Accept a payment.

Create a PaymentIntent for the Customer
curl https://api.fluveo.com/v1/payment_intents \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: order-2077-pay" \
  -d amount=2000 \
  -d currency=usd \
  -d "payment_method_types[]=card" \
  -d customer=cus_...
const response = await fetch("https://api.fluveo.com/v1/payment_intents", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk_test_...",
    "Idempotency-Key": "order-2077-pay",
  },
  body: new URLSearchParams({
    amount: "2000",
    currency: "usd",
    "payment_method_types[]": "card",
    customer: "cus_...",
  }),
});
const paymentIntent = await response.json();
console.log(paymentIntent.id, paymentIntent.customer);
import requests

response = requests.post(
    "https://api.fluveo.com/v1/payment_intents",
    headers={
        "Authorization": "Bearer sk_test_...",
        "Idempotency-Key": "order-2077-pay",
    },
    data={
        "amount": 2000,
        "currency": "usd",
        "payment_method_types[]": "card",
        "customer": "cus_...",
    },
)
payment_intent = response.json()
print(payment_intent["id"], payment_intent["customer"])
<?php
$ch = curl_init("https://api.fluveo.com/v1/payment_intents");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer sk_test_...",
        "Idempotency-Key: order-2077-pay",
    ],
    CURLOPT_POSTFIELDS => http_build_query([
        "amount" => 2000,
        "currency" => "usd",
        "payment_method_types" => ["card"],
        "customer" => "cus_...",
    ]),
]);
$paymentIntent = json_decode(curl_exec($ch), true);
echo $paymentIntent["customer"];
200 response (shortened)
{
  "id": "pi_...",
  "object": "payment_intent",
  "amount": 2000,
  "currency": "usd",
  "customer": "cus_...",
  "status": "requires_payment_method"
}

GET /v1/payment_intents takes customer too, to list one buyer's payments. See the customer parameter in the reference.

List its saved cards#

GET /v1/customers/{customer}/payment_methods lists the PaymentMethods saved to a Customer. Narrow it with type=card, and page it with limit, starting_after and ending_before. A new Customer has none, so this list is empty until a card is saved to it.

List the Customer's cards
curl -G https://api.fluveo.com/v1/customers/cus_.../payment_methods \
  -H "Authorization: Bearer sk_test_..." \
  -d type=card
const response = await fetch("https://api.fluveo.com/v1/customers/cus_.../payment_methods?type=card", {
  headers: { Authorization: "Bearer sk_test_..." },
});
const list = await response.json();
console.log(list.data.length);
import requests

response = requests.get(
    "https://api.fluveo.com/v1/customers/cus_.../payment_methods",
    headers={"Authorization": "Bearer sk_test_..."},
    params={"type": "card"},
)
print(len(response.json()["data"]))
<?php
$ch = curl_init("https://api.fluveo.com/v1/customers/cus_.../payment_methods?type=card");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer sk_test_..."],
]);
$list = json_decode(curl_exec($ch), true);
echo count($list["data"]);
200 response
{
  "object": "list",
  "data": [],
  "has_more": false,
  "url": "/v1/customers/cus_.../payment_methods"
}

GET /v1/customers/{customer}/payment_methods/{payment_method} reads one card through its Customer.

Delete a Customer#

DELETE /v1/customers/{customer} deletes a Customer for good. There is no undo.

Delete the Customer
curl -X DELETE https://api.fluveo.com/v1/customers/cus_... \
  -H "Authorization: Bearer sk_test_..."
const response = await fetch("https://api.fluveo.com/v1/customers/cus_...", {
  method: "DELETE",
  headers: { Authorization: "Bearer sk_test_..." },
});
const deleted = await response.json();
console.log(deleted.deleted);
import requests

response = requests.delete(
    "https://api.fluveo.com/v1/customers/cus_...",
    headers={"Authorization": "Bearer sk_test_..."},
)
print(response.json()["deleted"])
<?php
$ch = curl_init("https://api.fluveo.com/v1/customers/cus_...");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => "DELETE",
    CURLOPT_HTTPHEADER => ["Authorization: Bearer sk_test_..."],
]);
$deleted = json_decode(curl_exec($ch), true);
var_dump($deleted["deleted"]);
200 response
{
  "id": "cus_...",
  "object": "customer",
  "deleted": true
}

What changes after a delete:

  • Reading it. GET /v1/customers/{customer} still answers 200, with only id, object and "deleted": true. The list and the search leave the Customer out. An update answers 404 resource_missing.
  • Its saved cards. They are not detached: each keeps customer set to the deleted id, and no payment_method.detached event is sent. They can no longer pay. Confirming a PaymentIntent that holds one is refused with 400 payment_method_unexpected_state, and no charge is made. You can still detach a card with POST /v1/payment_methods/{payment_method}/detach.
  • Open PaymentIntents. A PaymentIntent that names the deleted Customer and is not paid yet cannot be confirmed: the confirm is refused with resource_missing on customer, and no charge is made. A new PaymentIntent that names the deleted Customer is refused the same way.
  • Past payments. The delete changes only the Customer. PaymentIntents, charges and refunds made before it stay as they were, with their customer id.
  • Events. Only customer.deleted is sent.

Parameters#

Customer fields#

POST /v1/customers takes all of these. POST /v1/customers/{customer} takes all but tax_id_data.

Body parameters
  • emailstringOptional

    The buyer's email address. It must look like an address, and it is stored as sent.

  • namestringOptional

    The person's or the company's name. Leading and trailing spaces are dropped.

  • phonestringOptional

    The buyer's phone number, trimmed like name.

  • descriptionstringOptional

    Your own note about this buyer.

  • addressobjectOptional

    The buyer's address.

    6 child fields
  • shippingobjectOptional

    Where to ship orders. When you send shipping, it needs a name and an address.

    3 child fields
    • namestringRequired
    • addressobjectRequired

      The same fields as the Customer's address.

    • phonestringOptional
  • metadataobjectOptional

    Up to 50 of your own key-value pairs, sent as metadata[key]=value. On an update, an empty value removes its key. See Metadata.

  • preferred_localesarrayOptional

    The buyer's languages, most preferred first, as preferred_locales[]=pt-BR. Send preferred_locales= to empty the list.

  • tax_exemptstringOptional

    none (the default), exempt or reverse. Fluveo stores and shows it, and an empty value sets it back to none.

  • tax_id_dataarrayOptional

    Create only. Tax IDs made with the Customer, as tax_id_data[0][type] and tax_id_data[0][value], then [1] and so on, up to five.

    2 child fields
    • typestringRequired

      br_cpf or br_cnpj.

    • valuestringRequired

      The CPF or CNPJ, with or without punctuation.

Tax ID fields#

Body parameters of POST /v1/customers/{customer}/tax_ids
  • typestringRequired

    br_cpf for a CPF, br_cnpj for a CNPJ.

  • valuestringRequired

    The number, with or without its punctuation. It is answered in the written form of its type.

States and events#

A Customer has no status field. It exists until you delete it, and then reads back as deleted.

EventWhen
customer.createdA Customer was created. A create with tax_id_data first sends one customer.tax_id.created per tax ID, then this event.
customer.updatedAn update changed the Customer. data.previous_attributes holds the old values.
customer.deletedThe Customer was deleted. It is the only event a delete sends.
customer.tax_id.createdA tax ID was added, at create or with POST /v1/customers/{customer}/tax_ids.
customer.tax_id.deletedA tax ID was deleted.
customer.tax_id.updatedA tax ID's verification check finished. A br_cpf or br_cnpj tax ID goes through no such check, so it never sends this event.

Errors you may see#

ErrorWhy
400 email_invalidThe email is not an email address. No Customer is created.
400 parameter_missingshipping was sent without its name, or without its address.
400 parameter_unknownA parameter this call does not take, such as balance or tax.
400 tax_id_invalidThe value does not have the form of its type, such as a CPF with too few digits.
400 resource_already_existsThe Customer already has a tax ID with this type and value.
400 "Customer cus_... already has the maximum 5 tax IDs attached."The Customer has five tax IDs: delete one first.
404 resource_missingThe cus_... or txi_... is not one of your store's, or the Customer was deleted (on update).
400 payment_method_unexpected_stateThe PaymentIntent holds a card of a deleted Customer. Confirm it with another payment method.

See Errors for the shape of every error.

Test it#

  1. Create a Customer with email, name and tax_id_data[0][type]=br_cpf with tax_id_data[0][value]=52998224725: you get a cus_..., one customer.tax_id.created and then customer.created.
  2. Retrieve it with expand[]=tax_ids: the list holds the CPF as 529.982.247-25.
  3. Update its description: the answer shows the new value, and customer.updated carries the old one in previous_attributes.
  4. List with email= and the same address: the Customer is there. List with the address in capitals: the list is empty. Search with email:"...": it is found.
  5. Add a br_cnpj tax ID: you get a txi_.... Add the same one again: 400 resource_already_exists. Add type=br_cpf with value=123: 400 tax_id_invalid.
  6. Delete the CNPJ: "deleted": true, and customer.tax_id.deleted arrives.
  7. Create a PaymentIntent with customer=cus_...: it answers that customer. List the Customer's cards: the list is empty.
  8. Delete the Customer: "deleted": true and customer.deleted. Retrieve it: 200 with "deleted": true. Search for it: no match. Confirm the PaymentIntent of step 7 with payment_method=pm_card_visa: it is refused with resource_missing on customer.

Next#