Skip to main content

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 400 with a field path:
    • INVALID_PHONE
    • INVALID_NIT
    • INVALID_CUI
    • CONSUMIDOR_FINAL_NOT_A_PROFILE
    • INVALID_EMAIL
    • COLLECTION_LIMIT_EXCEEDED
    • DUPLICATE_ELEMENT_IN_CUSTOMER
    • LOCATION_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.