Developer Docs

← Back to Console

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:

Authentication

Both customer endpoints require:

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: limit is a page size (max 500), not a cap on the directory. Existing GET /e9v3/contacts?limit=100 (or ?search=&limit=) calls still return the first page with success, contacts, and count. To dump the whole book, loop while hasMore is true and pass nextCursor as cursor.

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 404 with "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:

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:

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:

  1. Verify the contactId is spelled correctly
  2. Re-fetch the customer list to ensure the customer still exists
  3. Contact support if the customer should exist but doesn’t

“Invalid scope” error

Problem: You’re getting a 403 error about insufficient permissions.

Solution:

  1. Verify your API key includes the contacts:read scope
  2. Generate a new API key with the correct scopes
  3. Use the new token in your requests

No results when searching

Problem: GET /contacts?search=<query> returns empty results.

Solution:

  1. Try searching with a shorter string (e.g., “acme” instead of “acme corporation”)
  2. Try searching with different fields (email domain, first name, business name)
  3. Fetch all customers by looping GET /contacts?limit=500 while hasMore is true
  4. 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