Customers API (feature 062)
These are the routes added or changed by 062, with curl examples. The full contract is specs/062-customer-module/contracts/customers-api.md.
TOKEN is a Firebase ID token and BIZ a business ID. Errors use the house body { "error": "<CODE>", "message": …, …details }.
Search: GET /customers/search (view)
curl -s "$API/customers/search?businessId=$BIZ&q=5201%206975" -H "Authorization: Bearer $TOKEN"
# { "data": [ { "id", "displayName", "primaryPhone", "defaultTaxId", "matchedOn": "phone",
# "matchedElementId", "exact": true, "counts": {…}, "status": "active" } ] }
Input shorter than 4 digits or 3 letters returns {"data": []}. limit defaults to 20, with a maximum of 50.
Read: GET /customers/:id (view)
Returns the CustomerView: core fields, the four collections, version, the deletion and anonymization timestamps, and the legacy mirror fields. Responds with ETag: "<version>". A deleted customer is still returned, with deletedAt set.
Create: POST /customers (manage)
curl -s -X POST "$API/customers" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{
"businessId": "'$BIZ'", "firstName": "José", "lastName": "Pérez",
"phones": [{ "type": "mobile", "e164": "+502 5201-6975" }],
"billingProfiles": [{ "taxIdType": "NIT", "taxId": "1234567-9", "legalName": "José Pérez" }],
"emails": [{ "email": "jose@example.com", "receivesFel": true }]
}'
# 201 + ETag: "1" → CustomerView + "duplicateCandidates": [...]
- The legacy body (
phone,taxId,taxName, …) still works. - Sending a legacy field together with its collection is
400 CUSTOMER_LEGACY_AND_COLLECTION_CONFLICT. - Validation failures are
400with afieldpath:INVALID_PHONEINVALID_NITINVALID_CUICONSUMIDOR_FINAL_NOT_A_PROFILEINVALID_EMAILCOLLECTION_LIMIT_EXCEEDEDDUPLICATE_ELEMENT_IN_CUSTOMERLOCATION_NOT_IN_BUSINESS
Duplicate check: POST /customers/duplicates (view)
curl -s -X POST "$API/customers/duplicates" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{ "businessId": "'$BIZ'", "phones": ["5201-6975"], "taxIds": [{ "taxIdType": "NIT", "taxId": "1234567-9" }] }'
Update: PATCH /customers/:id and PATCH /customers/:id/status (manage)
If-Match is optional on these two routes until FE-CUST ships (FR-034a). Without it the save is last write wins and is audited as unversioned.
curl -s -X PATCH "$API/customers/$ID" -H "Authorization: Bearer $TOKEN" -H 'If-Match: "1"' \
-H 'Content-Type: application/json' -d '{ "lastName": "Pérez Gómez" }'
# 200 + ETag: "2"; a stale If-Match → 409 { "error": "CUSTOMER_VERSION_CONFLICT", "currentVersion": 2 }
Elements (manage; If-Match required)
{collection} is one of phones, addresses, billing-profiles or emails.
# add: the server issues the id
curl -s -X POST "$API/customers/$ID/phones" -H "Authorization: Bearer $TOKEN" -H 'If-Match: "2"' \
-H 'Content-Type: application/json' -d '{ "type": "home", "e164": "2233-4455" }'
# 201 + Location: /customers/$ID/phones/<itemId> + "createdElementId"
# replace (the id never changes); an unknown or removed id → 404 CUSTOMER_ELEMENT_NOT_FOUND
curl -s -X PUT "$API/customers/$ID/phones/$ITEM" -H 'If-Match: "3"' …
# remove (the primary/default passes to the oldest remaining element)
curl -s -X DELETE "$API/customers/$ID/phones/$ITEM" -H 'If-Match: "4"' …
A missing If-Match is 428 CUSTOMER_VERSION_REQUIRED.
Delete: DELETE /customers/:id (delete)
This is a soft delete. Every sale, invoice and order keeps referring to the customer, and the customer leaves search and duplicate checks. Writing to a deleted customer returns 409 CUSTOMER_DELETED.
Taxpayer lookup: GET /customers/taxpayers/:nit (create or update)
curl -s "$API/customers/taxpayers/1234567-9?businessId=$BIZ" -H "Authorization: Bearer $TOKEN"
# 200 { "status": "found", "taxId": "12345679", "legalName": "…", "nameParts": {…}, "rawName": "…", "cached": false }
# 200 { "status": "not_found" | "unavailable" | "not_configured" }
# 400 INVALID_NIT (the certifier is not called) · 429 TAXPAYER_LOOKUP_RATE_LIMITED { "scope": "user" | "business" }
Checkout lookup: POST /fel/get-shared-info (now guarded)
The body is unchanged. The route now requires membership plus any of customer create, customer update or sale create. It applies the 30-per-user-per-minute limit only, never the daily cap.