Delete a customer
The customers.delete operation: DELETE /api/v1/customers/{id} in the REST API and the customers_delete tool in the MCP server.
/api/v1/customers/{id}Deletes a customer: a soft delete that sets deletedAt. The customer stays readable, because invoices,
payments and evidence belong to it and must be kept (spec §6.6), but it can't be changed any more, and its
email address can be used for a new customer. Deleted customers are left out of customers.list unless asked
for. Erasing personal data (pseudonymisation) is a separate operator action, still to come.
Give a reason: it goes to the audit log. Needs customers:admin (owners and admins). Emits
customer.updated with deletedAt set.
At a glance
- REST:
DELETE /api/v1/customers/{id} - MCP tool:
customers_delete - Operation ID:
customers.delete - Scope: Brand
- Permission:
customers:admin - Risk tier:
write - Dry run: Supported (send
Ownrecord-Dry-Run: true) - Idempotency:
Idempotency-Keyoptional - Modes: Test and live
- Authentication: API key with the
customers:adminscope - Since: M4
Request
Path parameters
| Field | Type | Description |
|---|---|---|
id | cus_… ID | The customer's ID (cus_…). |
Body
A JSON object (Content-Type: application/json):
| Field | Type | Required | Description |
|---|---|---|---|
reason | string | No | Why; recorded in the audit log. |
Response
200 OK with a JSON object:
| Field | Type | Description |
|---|---|---|
id | cus_… ID | |
object | "customer" | |
livemode | boolean | |
entityId | ent_… ID | |
brandId | brd_… ID | |
email | string | |
name | string or null | |
businessName | string or null | |
locale | string or null | |
type | one of individual, business, unknown | Inferred (spec §13.1): business with a tax ID that isn't rejected, or with a business name when declared a business. Tax is decided per sale: only a valid tax ID makes a sale business-to-business. |
declaredType | one of individual, business, unknown | What you declared. |
billingAddress | object or null | |
taxIds | array of object | |
taxIds[].id | string | Identifies the tax ID on the customer: its canonical number, e.g. ATU12345678. |
taxIds[].type | string | eu_vat, gb_vat, ch_uid/ch_vat, no_org/no_vat, us_ein, or another type (unverified). |
taxIds[].value | string | The number as given. |
taxIds[].country | string | |
taxIds[].number | string or null | The canonical number (null for types Ownrecord can't check). |
taxIds[].verification | object | The latest check (spec §9.4). |
taxIds[].verification.status | one of valid, invalid, unavailable, format_only, unverified | valid, invalid, unavailable (the register couldn't answer), format_only (no register: format and checksum) or unverified (a type Ownrecord can't check). |
taxIds[].verification.source | string | Who decided: VIES, HMRC, CH_UID, FORMAT (format and checksum only) or NONE. |
taxIds[].verification.reason | string or null | Why, e.g. not_registered or service_unavailable. |
taxIds[].verification.checkedAt | date-time | |
taxIds[].verification.consultationNumber | string or null | The register's proof of the check (VIES request identifier, HMRC consultation number). |
taxIds[].verification.simulated | boolean | A fixture answer (development and test environments), not a register's. |
taxIds[].history | array of object | Earlier checks, newest first (at most 10). |
taxIds[].history[].status | one of valid, invalid, unavailable, format_only, unverified | valid, invalid, unavailable (the register couldn't answer), format_only (no register: format and checksum) or unverified (a type Ownrecord can't check). |
taxIds[].history[].source | string | Who decided: VIES, HMRC, CH_UID, FORMAT (format and checksum only) or NONE. |
taxIds[].history[].reason | string or null | Why, e.g. not_registered or service_unavailable. |
taxIds[].history[].checkedAt | date-time | |
taxIds[].history[].consultationNumber | string or null | The register's proof of the check (VIES request identifier, HMRC consultation number). |
taxIds[].history[].simulated | boolean | A fixture answer (development and test environments), not a register's. |
taxIds[].addedAt | date-time | |
taxCountryLocked | object or null | Where the customer's purchases are taxed (spec §9.3): set at the first successful payment, moved only when a top-up's evidence resolves to another country. |
taxExempt | object or null | Tax exemption (spec §9.5). Not applied yet: exemption certificates come in a later release. |
processorRefs | array of object | The customer at each payment processor used (set by checkout). |
processorRefs[].processorConnectionId | pcon_… ID | |
processorRefs[].customerRef | string | |
risk | object | |
risk.level | one of normal, elevated, blocked | |
risk.reasons | array of string | |
risk.firstPaymentAt | date-time or null | |
risk.totalPaid | money | |
portalLastLoginAt | date-time or null | |
testClockId | clk_… ID or null | Test mode: the test clock the customer was created on (spec §16.13). |
metadata | object | |
createdAt | date-time | |
updatedAt | date-time | |
deletedAt | date-time or null | When the customer was deleted (soft delete); null while active. |
Examples
Delete a duplicate customer
DELETE /api/v1/customers/cus_01m4ty3c2b5qx7mhd9k8w6s0ab
Authorization: Bearer or_test_…
Content-Type: application/json
{
"reason": "Duplicate of cus_01m4ty3c2b5qx7mhd9k8w6s0aa"
}
Response (200):
{
"id": "cus_01m4ty3c2b5qx7mhd9k8w6s0ab",
"object": "customer",
"livemode": false,
"entityId": "ent_01m4tx5nj8emwbyf7y9vcf47x2",
"brandId": "brd_01m4tx5nj8emwbyf7y9vcf47x4",
"email": "ana@example.com",
"name": "Ana Schmidt",
"businessName": null,
"locale": "de",
"type": "individual",
"declaredType": "individual",
"billingAddress": {
"line1": null,
"line2": null,
"city": null,
"postalCode": null,
"region": null,
"country": "DE"
},
"taxIds": [],
"taxCountryLocked": {
"country": "DE",
"subdivision": null,
"lockedAt": "2026-10-14T09:30:00.000Z",
"evidenceSetId": "evs_01m4ty3c2b5qx7mhd9k8w6s0ac"
},
"taxExempt": null,
"processorRefs": [
{
"processorConnectionId": "pcon_01m4ty3c2b5qx7mhd9k8w6s0af",
"customerRef": "cus_QxY12AbC"
}
],
"risk": {
"level": "normal",
"reasons": [],
"firstPaymentAt": "2026-10-14T09:30:00.000Z",
"totalPaid": {
"amount": "119000000",
"currency": "EUR"
}
},
"portalLastLoginAt": null,
"testClockId": null,
"metadata": {},
"createdAt": "2026-10-14T09:00:00.000Z",
"updatedAt": "2026-10-14T10:00:00.000Z",
"deletedAt": "2026-10-14T10:00:00.000Z"
}
Errors
| Code | HTTP status | When | What to do |
|---|---|---|---|
validation_failed | 400 | id isn't a customer ID, or the reason is too long. | Fix the fields listed in details.issues and send the request again. |
resource_not_found | 404 | This brand has no such customer in this mode (customers belong to one brand and one mode). | Check the ID and that your credentials are scoped to the right organization, entity, brand and mode. |
customer_deleted | 400 | The customer was already deleted. | Create a new customer for this buyer. Deleted customers stay readable for the records they belong to, but can't be changed. |
version_conflict | 409 | The customer changed at the same moment; retry. | Fetch the object again and retry the change. |
scope_required | 400 | The request names no brand (an org or entity key without Ownrecord-Brand). | Call the operation with credentials or a target at its level: an organization, a legal entity, a brand or the platform (details.required_scope). |
livemode_not_allowed | 403 | You have no live-mode access, so you can't act on live customers. | Use test mode, or credentials for the target's mode. Developers need live access granted by an organization owner or admin. |
permission_denied | 403 | You can't delete this brand's customers (customers:admin: owners and admins). | Ask an organization owner or admin for a role or scope that allows this action. |
It can also return these common errors: api_version_unsupported, invalid_json, unsupported_media_type, authentication_required, api_key_invalid, idempotency_key_reused, idempotency_in_progress, approval_required, approval_rejected, approval_expired, stale_approval, approval_policy_denied, rate_limited, internal_error.