Customers
Exchange your organization's customer records with your own system. This is a separate resource from the customer name and addresses inside an order. Customer access covers your organization's customer directory, including customers created through inquiries; it is not limited to customers created through this API connection.
#Permission and sharing approval
All three endpoints require a valid API key and the organization's current approval to share customer data. The owner or an administrator grants or withdraws that approval under Integrations → API access → Share customer data with your system. It applies to both reading and writing, including validation-only requests. Without it the API returns 403 customer_bridge_not_approved.
| Method and path | Required scope | Test key | Live key |
|---|---|---|---|
GET /v1/customers | customers:read | Reads real customer records after approval | Reads customer records after approval |
GET /v1/customers/{id} | customers:read | Reads real customer records after approval | Reads customer records after approval |
POST /v1/customers | customers:write | Only validate_only: true; no saved changes | Validate or create/update |
Write does not imply read. Order permissions do not grant customer-directory access. Unlike orders, customers have no test flag or separate test dataset: a test key's reads are real, and its ordinary POST is rejected with 403 test_key_read_only.
Revoking sharing blocks subsequent customer calls. It cannot recall data already fetched by your system. There is no customer webhook or automatic outbound customer delivery; your system reads changes when needed.
#Create or update a customer
POST /v1/customers — requires customers:write and sharing approval.
The same endpoint creates or updates a record. There is no separate customer PATCH endpoint. Matching proceeds in this order:
external_reference: exactly one matching reference anywhere in your organization's customer directory.code: the Meykt customer number, including an inactive customer with that code.email: case-insensitive matching against active customers only, after trimming and lowercasing.- No match: create a new customer.
Names alone are not matching keys: repeating only a display name can create another customer. Retain a stable external reference or known customer code rather than relying on a name for duplicate protection.
A higher-priority match determines the target. Multiple matches for the external reference or email return 409 customer_match_ambiguous; Meykt does not choose a customer for you. Use a stable reference or the known customer code to disambiguate. Do not assume that supplying a different email forces a new record when the external reference already matches.
These references are organization-wide, not a separate namespace per key or connection. If several source systems share the same directory, coordinate their references; for example, use a stable system prefix in external_reference. A newly supplied reference is filled in when the matched customer has none. An existing reference is never silently replaced. It is not returned in customer responses, so retain your source-to-Meykt mapping yourself.
Every request, including an update, needs at least one nonempty name, company, last_name or email. A reference or code alone is insufficient. Ordinary string inputs are trimmed and must be nonempty when supplied; JSON null and empty strings are not a way to clear them.
#Validate before writing
Save this fictional example as customer.json. Start with a test key and validate_only: true:
{
"external_reference": "ERP-CUSTOMER-42",
"code": "C-2042",
"name": "Example GmbH",
"customer_type": "b2b",
"company": "Example GmbH",
"first_name": "Erika",
"last_name": "Muster",
"email": "erika@example.invalid",
"phone": "+49 30 5550100",
"street": "Example Street",
"house_number": "7",
"postal_code": "10115",
"city": "Berlin",
"country": "DE",
"preferred_content_language": "de",
"custom_fields": { "segment": "Wholesale" },
"delivery_address": {
"company": "Example GmbH",
"street": "Warehouse Road",
"house_number": "2",
"postal_code": "10115",
"city": "Berlin",
"country": "DE"
},
"contacts": [{
"first_name": "Erika",
"last_name": "Muster",
"position": "Purchasing",
"email": "erika@example.invalid",
"is_primary": true
}],
"update_mode": "overwrite",
"validate_only": true
}curl -X POST https://api.meykt.com/v1/customers \
-H "Authorization: Bearer $MEYKT_API_KEY" \
-H "Content-Type: application/json" \
-d @customer.jsonFor a previously unknown customer, the validation response is 200:
{"data":{"result":"would_create","matched_by":null,"customer_id":null}}A matching record instead yields would_update, its customer_id, and matched_by equal to external_reference, code or email. Validation saves nothing and does not reserve a customer number or freeze the decision. The result can change if another writer changes the directory before your live request.
To save, use a live key and set validate_only to false or omit it. A creation returns 201 with result: "created"; an update returns 200 with result: "updated". Both include data.customer and nullable data.matched_by. Retain the returned customer id and code.
#Request fields and limits
| Field | Accepted input and behavior |
|---|---|
external_reference | String, 1–200 characters; preferred source identity, not a returned field |
code | String, 1–60; matches an existing Meykt customer or selects the number for a new one; omitted on creation means Meykt assigns one |
name | String, 1–300; creation display name and required identity input, not a matching key; does not rename a matched customer's display name |
salutation | mr, mrs, diverse, dr, prof |
first_name, last_name | String, 1–120 each |
email | Email address, at most 254; trimmed and normalized for matching |
phone, mobile | String, 1–60 each |
company | String, 1–200 |
customer_type | b2c, b2b, wholesale; defaults to b2c only for creation |
street, address_line2 | String, 1–200 each; billing address input at the top level |
house_number | String, 1–40 |
postal_code | String, 1–30; preserve leading zeroes |
city | String, 1–120 |
country | String, 1–80; unlike the order address contract, this is not restricted to a two-letter value |
website_url | String, 1–300 |
company_size | String, 1–60 |
preferred_content_language | Two-character string; supply a valid two-letter language code such as de or en |
custom_fields | Object of additional customer-field values, keys at most 80 characters; not arbitrary customer metadata |
delivery_address | Optional nonempty object; complete replacement of the default delivery address |
contacts | Optional list of 1–20 complete contacts; replaces the contact list |
update_mode | overwrite (default) or fill_empty |
validate_only | Boolean; true evaluates matching without saving |
The complete JSON request is limited to 256 KiB, 32 nesting levels and 25000 values. No customer ID, organization ID, is_active, source, order revision or order-command idempotency field is writable through this contract. Send only documented fields.
#Update and omission rules
overwrite applies supplied nonempty ordinary contact and billing-address fields. Omitted fields stay as they are. fill_empty only fills currently empty ordinary fields; it preserves existing values in those fields.
The following rules are separate from that ordinary-field policy:
nameandcodeare used for creation/identification and do not rename or renumber a matched customer. Use Meykt for those edits. Supplying only a name does not change an existing display name.- An explicitly supplied
customer_typeis applied even infill_empty; omission preserves the current type. custom_fieldsmerges the supplied keys into existing custom fields. A supplied key can replace its previous value even withfill_empty; omitted keys remain.{}does not clear all custom fields.- A supplied
delivery_addressreplaces the whole default delivery address, including clearing omitted fields in that object, even withfill_empty. Omit the object to preserve the address.{}andnullare invalid. - A supplied
contactslist replaces the entire contact list, even withfill_empty. Omit it to preserve existing contacts.[]andnullare invalid.
The delivery-address object accepts company, first_name, last_name, street, house_number, address_line2, postal_code, city, and country with the same lengths as above. A contact requires both first_name and last_name; optional fields are salutation, position (1–120), email (up to 254), phone, mobile (up to 60 each), and is_primary. The first submitted contact is marked primary; other supplied primary flags are retained. Reads return primary contacts first and then by creation time. Contact and address IDs are not exposed for individual patching.
For example, with a live key and an existing matching reference, this updates the phone and leaves omitted address/contact sections untouched. The required identity name does not rename the customer:
{
"external_reference": "ERP-CUSTOMER-42",
"name": "Example GmbH",
"phone": "+49 30 5550101",
"update_mode": "overwrite"
}Matching stops at the first decisive level. If external_reference matches customer A, a supplied code belonging to B does not override A or create an identity-conflict response; the existing code is not renamed. If that external reference is itself ambiguous, supplying a code alongside it does not bypass the ambiguity. Resolve the reference, or deliberately omit it to match by code after checking the preview.
#Retry and concurrency
This is a matching/upsert API, not the order-command journal. It has no immutable idempotency UUID, revision precondition or separate test/live customer namespace. A sequential repeat with the same stable reference normally updates the same customer and returns 200, rather than replaying an immutable 201 result. Changed content can update that customer instead of producing an order-style content conflict.
Serialize writes for a source customer. Do not assume exactly-once creation under concurrent requests or reuse one reference for different people. If a timeout or 500 occurs, a customer may already have been saved, or some updates may have applied before a later failure. Read/reconcile the known customer and repeat with the same reference/code; do not manufacture a new identity to escape an uncertain outcome.
#Read one customer
GET /v1/customers/{id} — requires customers:read and sharing approval. {id} is the Meykt customer UUID, not the code or your external reference. An inactive customer can be read directly when its ID is known.
curl https://api.meykt.com/v1/customers/0f150bf1-cbe2-43a9-9f19-60394450c701 \
-H "Authorization: Bearer $MEYKT_API_KEY"The response is 200 with data.customer. The same complete customer shape is returned after a successful write:
{
"data": {
"customer": {
"id": "0f150bf1-cbe2-43a9-9f19-60394450c701",
"code": "C-2042",
"name": "Example GmbH",
"salutation": null,
"first_name": "Erika",
"last_name": "Muster",
"email": "erika@example.invalid",
"phone": "+49 30 5550100",
"mobile": null,
"company": "Example GmbH",
"customer_type": "b2b",
"billing_address": { "street": "Example Street", "house_number": "7", "address_line2": null, "postal_code": "10115", "city": "Berlin", "country": "DE" },
"website_url": null,
"company_size": null,
"preferred_content_language": "de",
"is_active": true,
"custom_fields": { "segment": "Wholesale" },
"delivery_address": { "company": "Example GmbH", "first_name": null, "last_name": null, "street": "Warehouse Road", "house_number": "2", "address_line2": null, "postal_code": "10115", "city": "Berlin", "country": "DE" },
"contacts": [{ "salutation": null, "first_name": "Erika", "last_name": "Muster", "position": "Purchasing", "email": "erika@example.invalid", "phone": null, "mobile": null, "is_primary": true }],
"created_at": "2026-10-19T09:00:00Z",
"updated_at": "2026-10-19T09:00:00Z"
}
}
}Optional record values can be null; a missing default delivery address is delivery_address: null, and no contacts is contacts: []. Responses omit the source reference and private link/organization metadata. Foreign, unknown and malformed customer IDs answer the same 404 not_found, once key/scope/consent checks pass.
#List and reconcile customers
GET /v1/customers — requires customers:read and sharing approval.
curl 'https://api.meykt.com/v1/customers?include_inactive=true&limit=100' \
-H "Authorization: Bearer $MEYKT_API_KEY"| Query | Meaning |
|---|---|
limit | Page size, default 25; numeric values are truncated/clamped to 1–100 |
starting_after | Last returned customer's id, supplied from next_cursor |
updated_since | Timestamp; returns customers changed at or after it |
include_inactive | Exactly true includes inactive customers; default excludes them |
The result is {data:{customers:[...],has_more,next_cursor}}. Every member has the complete customer shape above. next_cursor is a customer ID when more pages exist, otherwise null; pass it as starting_after while keeping the other filters fixed. A malformed, unknown or foreign cursor and an invalid timestamp return a neutral 422 validation_failed. A failed cursor lookup is a 500 internal_error; keep the previous cursor and retry instead of treating it as an empty page.
Include inactive customers when reconciling status. Otherwise an archived record disappears from the active list without a deletion notice. This endpoint has no tombstone stream or customer webhook; it is not the inventory cursor protocol. Do not treat an absent result or failed page as a deletion. Use overlapping update windows and periodic reconciliation rather than advancing a watermark before all pages have been stored.
Customers are ordered by creation time descending, then ID descending. Continuation uses both values, so customers sharing the same creation timestamp are included across page boundaries. Keep the returned ID cursor intact; do not reconstruct it from rounded timestamps. This is keyset pagination, not a frozen snapshot: keep filters fixed across a read and reconcile subsequent updates separately.
#Errors and recovery
| HTTP / code | Meaning and remedy |
|---|---|
401 invalid_api_key | Check the key, connection, expiry and revocation before retrying |
403 insufficient_scope | Explicitly grant the required customer read/write scope on a new key |
403 customer_bridge_not_approved | An owner/admin must approve current customer sharing in Meykt |
403 test_key_read_only | Use validate_only:true, or explicitly switch to an authorized live key to save |
422 validation_failed | Fix missing identity, invalid values, empty replacement address/contact list or invalid read filters; POST reports its first validation issue |
409 customer_match_ambiguous | Supply a known code/reference or resolve ambiguous records; never guess a target |
409 customer_code_taken | A creation could not claim its supplied code; reconcile the existing customer and intended identity |
404 not_found | The single customer is unavailable to this organization, or the endpoint is wrong |
429 rate_limited | Honor Retry-After; all endpoints share the key's 120-request/minute limit |
500 internal_error | Read/reconcile the known customer before retrying an uncertain write; a failed response is not proof that nothing was saved |
For shared JSON/body errors and key handling see Errors and Authentication. Customer-directory updates do not modify the address snapshot of an existing order; use the order's own change command when that is the intended action. Customer operations are not offered as integration-profile operations in the current capability catalog.
Machine-readable version of this page: https://www.meykt.com/developers/customers.md