Working with Customers
The Customers API (Contacts endpoint) enables you to discover available customers, suppliers, and locations before creating orders. This solves the common integration challenge of not knowing valid customer IDs upfront.
Overview
Every order requires a customerId. The Customers API provides two endpoints to look up valid customer IDs:
- List Customers: Retrieve all customers with optional search filtering
- Get Single Customer: Look up a specific customer by ID
Authentication
Both customer endpoints require:
- Scope:
contacts:read - Auth Method: OAuth 2.0 M2M (Machine-to-Machine)
- Format: Bearer token in
Authorizationheader
See the Authentication guide for how to obtain a JWT token with the contacts:read scope.
List Customers
Retrieve a paginated list of all customers available to your business.
Request
GET /e9v3/contacts?search=<query>&limit=100
Authorization: Bearer <jwt-token>
Query Parameters
| Parameter | Type | Required | Default | Max | Description |
|---|---|---|---|---|---|
search |
string | No | — | — | Filter by customer name, business name, or email (case-insensitive) |
limit |
integer | No | 100 | 500 | Maximum number of results to return per page |
cursor |
string | No | — | — | Previous page’s nextCursor. Omit for the first page — existing clients that only send search/limit are unchanged. |
Response (200 OK)
{
"success": true,
"contacts": [
{
"contactId": "cust-123",
"businessId": "biz-456",
"name": "Acme Corporation",
"businessName": "Acme Corporation",
"email": "orders@acme.com",
"phone": "+1-555-1234",
"mobile": "+1-555-5678",
"fax": "",
"website": "acme.com",
"contactType": "customer",
"status": "active",
"priority": "high",
"address": {
"line1": "123 Main Street",
"line2": "Suite 200",
"suburb": "Sydney",
"state": "NSW",
"postcode": "2000",
"country": "Australia"
},
"business": {
"abn": "12345678901",
"acn": "987654321",
"isGstRegistered": true
},
"metadata": {
"notes": "Key account - VIP pricing",
"category": "wholesale",
"tags": ["vip", "bulk-discount", "au-based"],
"role": null,
"createdAt": "2024-01-01T10:00:00Z",
"updatedAt": "2024-01-15T14:30:00Z",
"createdBy": "admin@equos.one",
"updatedBy": "sales@equos.one"
},
"integration": {
"externalId": "shopify-12345",
"source": "shopify"
},
"addressStatus": "postcode_only"
},
{
"contactId": "cust-456",
"businessId": "biz-456",
"name": "Widget Inc",
"businessName": "Widget Inc",
"email": "purchasing@widget.com",
"phone": "+1-555-5678",
"address": {
"line1": "456 Tech Road",
"suburb": "Melbourne",
"state": "VIC",
"postcode": "3000",
"country": "Australia"
},
"contactType": "supplier",
"status": "active",
"priority": "medium",
"addressStatus": "postcode_only"
}
],
"count": 2,
"hasMore": false
}
Response Fields
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true for successful requests |
contacts |
array | Array of contact objects |
contacts[].contactId |
string | Unique identifier for the contact (use in orders) |
contacts[].businessId |
string | Business ID this contact belongs to |
contacts[].name |
string | Contact’s preferred name |
contacts[].businessName |
string | Official business name |
contacts[].email |
string | Email address (required or phone/mobile) |
contacts[].phone |
string | Phone number (required or email/mobile) |
contacts[].mobile |
string | Mobile number |
contacts[].fax |
string | Fax number |
contacts[].website |
string | Website URL |
contacts[].contactType |
string | Type: customer, supplier, location, or agent |
contacts[].status |
string | Status: active or inactive on list (archived contacts are excluded). Get-by-id can return archived. |
contacts[].priority |
string | Priority: low, medium, high, or critical |
contacts[].address |
object | Address details (line1, line2, suburb, state, postcode, country) |
contacts[].business |
object | Business info (abn, acn, isGstRegistered) |
contacts[].metadata |
object | Metadata (notes, category, tags, role, createdAt, updatedAt, createdBy, updatedBy) |
contacts[].integration |
object | Integration info (externalId, source) |
contacts[].addressStatus |
string | Address validation status: validated (full match), postcode_only (CSV match), invalid (no match), unverified (unavailable) |
count |
integer | Number of results returned in this page |
List-level paging fields (additive — existing clients that ignore unknown JSON keys are unchanged):
| Field | Type | Description |
|---|---|---|
count |
integer | Number of contacts in this page |
hasMore |
boolean | true when another page is available |
nextCursor |
string | Pass as cursor on the next request. Omitted when hasMore is false |
Paging:
limitis a page size (max 500), not a cap on the directory. ExistingGET /e9v3/contacts?limit=100(or?search=&limit=) calls still return the first page withsuccess,contacts, andcount. To dump the whole book, loop whilehasMoreis true and passnextCursorascursor.
Data source: list and search both read the versioned ContactIndex (as of the last ContactIndexRegenerate). Get-by-id reads the live contact document. Archived contacts are excluded from the list endpoint. If no index is published, list returns
404with"Contact index not published". List rows are the index projection (name, phones, locality, street when the generation includes it, tags, category). ABN, notes, fax, website, and integration live on get-by-id.
Address Validation
Every contact includes an addressStatus field that indicates how thoroughly the address has been validated:
validated— Full address matched against the Australian address database (addressr). Only returned by single contact lookups when addressr is available.postcode_only— Postcode/suburb combination verified against the Australian postcode database. Used by list endpoint (fast, no network calls) and as fallback in single lookups.invalid— Postcode/suburb combination not found in any database. These contacts are excluded from the list endpoint to ensure only usable contacts are returned.unverified— Address validation database was unavailable at startup. Contacts are returned without validation (graceful degradation).
List endpoint behavior: CSV-only. Returns "postcode_only" or "unverified". Contacts with "invalid" status, and archived contacts, are filtered out. List never returns "validated" — that status is only used on get-by-id when the address server matches.
Single contact endpoint behavior: Returns all contacts with their validation status, even if addressStatus is "invalid". This allows you to investigate why a contact isn’t appearing in the list.
Get Single Customer
Look up a specific customer by ID.
Request
GET /e9v3/contacts/:contactId
Authorization: Bearer <jwt-token>
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
contactId |
string | Yes | The customer ID to retrieve |
Response (200 OK)
{
"success": true,
"contacts": [
{
"contactId": "cust-123",
"businessId": "biz-456",
"name": "Acme Corporation",
"businessName": "Acme Corporation",
"email": "orders@acme.com",
"phone": "+1-555-1234",
"mobile": "+1-555-5678",
"website": "acme.com",
"contactType": "customer",
"status": "active",
"priority": "high",
"address": {
"line1": "123 Main Street",
"line2": "Suite 200",
"suburb": "Sydney",
"state": "NSW",
"postcode": "2000",
"country": "Australia"
},
"business": {
"abn": "12345678901",
"acn": "987654321",
"isGstRegistered": true
},
"metadata": {
"notes": "Key account - VIP pricing",
"category": "wholesale",
"tags": ["vip", "bulk-discount"],
"createdAt": "2024-01-01T10:00:00Z",
"updatedAt": "2024-01-15T14:30:00Z",
"createdBy": "admin@equos.one",
"updatedBy": "sales@equos.one"
},
"integration": {
"externalId": "shopify-12345",
"source": "shopify"
},
"addressStatus": "validated"
}
],
"count": 1
}
Response (404 Not Found)
{
"success": false,
"error": "Contact not found",
"count": 0
}
Address Validation Examples
Contact with full address match (validated):
{
"addressStatus": "validated"
}
The address matched against the Australian address database (addressr).
Contact with postcode match only (postcode_only):
{
"addressStatus": "postcode_only"
}
The postcode/suburb combination is valid according to the Australian postcode database, but full address wasn’t matched against addressr.
Contact with invalid postcode (invalid):
{
"addressStatus": "invalid"
}
The postcode/suburb combination doesn’t exist. This contact is excluded from list endpoint results.
Contact when validation unavailable (unverified):
{
"addressStatus": "unverified"
}
The postcode database wasn’t loaded at startup. Contact is returned without validation.
Common Use Cases
Find a Customer by Name
Search for customers by partial name match:
curl -X GET "https://api.equos.one/e9v3/contacts?search=acme" \
-H "Authorization: Bearer <jwt-token>"
Returns all customers whose name, business name, or email contains “acme” (case-insensitive).
List All Customers (Paginated)
Retrieve customers in pages of 100 (default). limit is a page size, not a catalog cap:
# Get first page
curl -X GET "https://api.equos.one/e9v3/contacts?limit=100" \
-H "Authorization: Bearer <jwt-token>"
# Dump the whole book — loop while hasMore is true
curl -X GET "https://api.equos.one/e9v3/contacts?limit=500" \
-H "Authorization: Bearer <jwt-token>"
curl -X GET "https://api.equos.one/e9v3/contacts?limit=500&cursor=<nextCursor>" \
-H "Authorization: Bearer <jwt-token>"
Find Customers with Specific Domain
Search for all customers with a specific email domain:
curl -X GET "https://api.equos.one/e9v3/contacts?search=@company.com" \
-H "Authorization: Bearer <jwt-token>"
Create an Order with a Discovered Customer ID
After discovering a customer ID from the contacts API, use it to create an order:
curl -X POST https://api.equos.one/e9v3/orders/send \
-H "Authorization: Bearer <jwt-token>" \
-H "Content-Type: application/json" \
-d '{
"customerId": "cust-123",
"items": [
{
"sku": "WIDGET-A",
"productName": "Blue Widget",
"quantity": 10
}
]
}'
Error Handling
400 Bad Request
Invalid query parameters or missing required fields.
{
"success": false,
"error": "Invalid limit parameter: must be between 1 and 500"
}
401 Unauthorized
Missing or invalid authentication token.
{
"success": false,
"error": "Missing or invalid authorization header"
}
403 Forbidden
Token lacks the required contacts:read scope.
{
"success": false,
"error": "Insufficient permissions: contacts:read scope required"
}
404 Not Found
Get-by-id: the customer ID does not exist.
{
"success": false,
"error": "Contact not found"
}
List: no ContactIndex is published for the business.
{
"success": false,
"error": "Contact index not published"
}
500 Internal Server Error
Unexpected server error. Retry with exponential backoff.
{
"success": false,
"error": "Failed to query contacts: <error details>"
}
Best Practices
1. Cache Customer Lists Locally
Fetch the full customer list once and cache it in your application:
# Python example
import requests
import json
def get_all_customers(token):
"""Fetch and cache all customers (loop pages)."""
contacts = []
params = {"limit": 500}
while True:
response = requests.get(
"https://api.equos.one/e9v3/contacts",
headers={"Authorization": f"Bearer {token}"},
params=params,
)
payload = response.json()
contacts.extend(payload.get("contacts") or [])
if not payload.get("hasMore"):
return contacts
params["cursor"] = payload["nextCursor"]
# Cache this at startup or refresh daily
customers = get_all_customers(token)
customer_map = {c["contactId"]: c for c in customers}
2. Search Client-Side When Possible
For small customer lists (< 10k), perform search filtering locally to reduce API calls:
// JavaScript example
function searchCustomers(customers, query) {
const lower = query.toLowerCase();
return customers.filter(c =>
c.name.toLowerCase().includes(lower) ||
c.businessName.toLowerCase().includes(lower) ||
c.email.toLowerCase().includes(lower)
);
}
3. Handle Missing Customers Gracefully
When a customerId is provided by a user or system:
def validate_customer_id(contactId, token):
"""Check if customer exists before creating an order"""
response = requests.get(
f"https://api.equos.one/e9v3/contacts/{contactId}",
headers={"Authorization": f"Bearer {token}"}
)
if response.status_code == 404:
return None, "Customer not found"
elif response.status_code != 200:
return None, "Error checking customer"
return response.json()["contacts"][0], None
4. Sync Regularly for Integration Flows
If you maintain a list of valid customers externally (e.g., in your database):
# Refresh customer list every hour or on-demand
import time
last_sync = 0
cached_customers = []
def get_customers_with_cache(token, max_age_seconds=3600):
global last_sync, cached_customers
now = time.time()
if now - last_sync > max_age_seconds:
cached_customers = get_all_customers(token)
last_sync = now
return cached_customers
5. Respect Rate Limits
The contacts API respects the same rate limiting as the orders API:
- Rate: 1 request/second per API key
- Burst: 10 concurrent requests
For bulk customer lookups, fetch once and cache rather than calling per-order.
Troubleshooting
“Contact not found” when creating an order
Problem: You received a 404 when looking up a customer ID.
Solution:
- Verify the
contactIdis spelled correctly - Re-fetch the customer list to ensure the customer still exists
- Contact support if the customer should exist but doesn’t
“Invalid scope” error
Problem: You’re getting a 403 error about insufficient permissions.
Solution:
- Verify your API key includes the
contacts:readscope - Generate a new API key with the correct scopes
- Use the new token in your requests
No results when searching
Problem: GET /contacts?search=<query> returns empty results.
Solution:
- Try searching with a shorter string (e.g., “acme” instead of “acme corporation”)
- Try searching with different fields (email domain, first name, business name)
- Fetch all customers by looping
GET /contacts?limit=500whilehasMoreis true - Verify your customer exists by checking your admin console
Integration Examples
See the Code Examples page for complete working examples in Python, JavaScript, and cURL.
Next Steps
- Create an API key with
contacts:readscope in your admin console - Use the Quick Start guide to create your first order
- Review the API Reference for full endpoint documentation
- Check out Code Examples for language-specific implementations