Dokumentasyon

Available lang sa Ingles ang dokumentasyon ng produkto.

Contacts endpoints

A contact is a person or a company your firm works with. These endpoints list, create, read, update, and delete those records from your own systems.

All paths sit under the base address https://api.esqase.com, and every request needs an API key. See Authentication and scopes if you have not made your first call yet.

The endpoints

ActionMethod and pathScope needed
ListGET /v1/contactscontacts:read
CreatePOST /v1/contactscontacts:write
RetrieveGET /v1/contacts/{id}contacts:read
UpdatePATCH /v1/contacts/{id}contacts:write
DeleteDELETE /v1/contacts/{id}contacts:write

The contact object

Every contact you get back has the same shape. A single-record read, and a create or update response, also carry customFields and unsupportedCustomFields, described in Custom fields in the API. List entries never carry those two properties.

  • id: the id Esqase assigns. Use it in every other path.
  • type: PERSON or COMPANY. Set on create and fixed after that.
  • status: the contact's lifecycle state. Contacts created through the API start as ACTIVE.
  • name: the display name Esqase builds for you. For a person it is the first, middle, and last name joined together; for a company it is the company name. You never send name yourself.
  • prefix, firstName, middleName, lastName, suffix, nickname: the person name parts, each null when unset.
  • companyName, tradeName, companyType: the company name parts, each null when unset.
  • emails: the contact's email addresses. They are usually returned in the order you sent them, but the order is not guaranteed and the response does not say which one your firm marked primary.
  • phones: the contact's phone numbers, with the same caveat as emails.
  • createdAt, updatedAt: timestamps, or null on the responses noted below.

Note: The name field is derived, never sent. If you create a person with no first or last name the request is rejected, and a company with no companyName is rejected too.

List contacts

GET /v1/contacts returns your contacts a page at a time. Two query parameters control the page:

  • limit: how many records to return. Minimum 1, maximum 100, default 20.
  • offset: how many records to skip before the page starts. Minimum 0, default 0.
curl "https://api.esqase.com/v1/contacts?limit=2&offset=0" \
  -H "Authorization: Bearer $ESQASE_API_KEY"

The records come back in data, with a pagination block telling you the page size and the total count:

{
  "data": [
    {
      "id": "a1b2c3d4-0001-4a1b-9c2d-1234567890ab",
      "type": "PERSON",
      "status": "ACTIVE",
      "name": "Jane Doe",
      "prefix": null,
      "firstName": "Jane",
      "middleName": null,
      "lastName": "Doe",
      "suffix": null,
      "nickname": null,
      "companyName": null,
      "tradeName": null,
      "companyType": null,
      "emails": ["jane@example.com"],
      "phones": ["+13105551234"],
      "createdAt": "2026-07-01T14:32:00.000Z",
      "updatedAt": "2026-07-01T14:32:00.000Z"
    },
    {
      "id": "a1b2c3d4-0002-4a1b-9c2d-1234567890ab",
      "type": "COMPANY",
      "status": "ACTIVE",
      "name": "Acme Holdings",
      "prefix": null,
      "firstName": null,
      "middleName": null,
      "lastName": null,
      "suffix": null,
      "nickname": null,
      "companyName": "Acme Holdings",
      "tradeName": null,
      "companyType": null,
      "emails": ["contact@example.com"],
      "phones": [],
      "createdAt": "2026-07-01T15:00:00.000Z",
      "updatedAt": "2026-07-01T15:00:00.000Z"
    }
  ],
  "pagination": { "limit": 2, "offset": 0, "total": 128 }
}

When offset plus limit reaches total, you have read every contact. List order is not specified, so de-duplicate on id if records may change while you page.

Create a contact

POST /v1/contacts creates one contact and returns 201 Created.

  • type (optional): PERSON or COMPANY. Defaults to PERSON.
  • firstName, middleName, lastName, nickname (optional): up to 128 characters each. A PERSON needs at least a firstName or a lastName.
  • prefix, suffix (optional): up to 32 characters each.
  • companyName, tradeName (optional): up to 255 characters each. A COMPANY needs a companyName.
  • companyType (optional): up to 64 characters.
  • emails (optional): a list of email addresses, up to 20 entries, each up to 255 characters and each a valid email address. The first is stored as the primary email.
  • phones (optional): a list of phone numbers, up to 20 entries, each up to 255 characters. The first is stored as the primary phone.

Example: create a person.

curl -X POST https://api.esqase.com/v1/contacts \
  -H "Authorization: Bearer $ESQASE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "PERSON",
    "firstName": "Jane",
    "lastName": "Doe",
    "emails": ["jane@example.com"],
    "phones": ["+13105551234"]
  }'
{
  "data": {
    "id": "a1b2c3d4-0001-4a1b-9c2d-1234567890ab",
    "type": "PERSON",
    "status": "ACTIVE",
    "name": "Jane Doe",
    "prefix": null,
    "firstName": "Jane",
    "middleName": null,
    "lastName": "Doe",
    "suffix": null,
    "nickname": null,
    "companyName": null,
    "tradeName": null,
    "companyType": null,
    "emails": ["jane@example.com"],
    "phones": ["+13105551234"],
    "customFields": {},
    "unsupportedCustomFields": [],
    "createdAt": null,
    "updatedAt": null
  }
}

Note: The create response echoes what you sent, so createdAt and updatedAt come back as null. Read the contact by id if you need its real timestamps.

Example: create a company. A company sends companyName instead of person name parts.

curl -X POST https://api.esqase.com/v1/contacts \
  -H "Authorization: Bearer $ESQASE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "COMPANY",
    "companyName": "Acme Holdings",
    "companyType": "Corporation",
    "emails": ["contact@example.com"]
  }'

Retrieve a contact

GET /v1/contacts/{id} returns one contact with its real timestamps.

curl https://api.esqase.com/v1/contacts/a1b2c3d4-0001-4a1b-9c2d-1234567890ab \
  -H "Authorization: Bearer $ESQASE_API_KEY"
{
  "data": {
    "id": "a1b2c3d4-0001-4a1b-9c2d-1234567890ab",
    "type": "PERSON",
    "status": "ACTIVE",
    "name": "Jane Doe",
    "prefix": null,
    "firstName": "Jane",
    "middleName": null,
    "lastName": "Doe",
    "suffix": null,
    "nickname": null,
    "companyName": null,
    "tradeName": null,
    "companyType": null,
    "emails": ["jane@example.com"],
    "phones": ["+13105551234"],
    "customFields": {},
    "unsupportedCustomFields": [],
    "createdAt": "2026-07-01T14:32:00.000Z",
    "updatedAt": "2026-07-01T14:32:00.000Z"
  }
}

If no contact in your firm has that id, you get 404 with the code not_found.

Update a contact

PATCH /v1/contacts/{id} changes the fields you send and leaves the rest alone. Every field is optional, so {} is a valid body. A request with no body at all is rejected with 400.

You can send the same name fields as on create: prefix, firstName, middleName, lastName, suffix, nickname, companyName, tradeName, companyType. Each also accepts null, which clears it. The name is rebuilt from the merged parts.

type cannot be changed. It is not accepted on update, and the contact keeps the type it was created with.

emails and phones replace, they do not append. When you send emails, it replaces every email the contact has, so send the full list you want to end up with. The same goes for phones. Sending only emails leaves the phones as they were, and the other way round, and contact methods you cannot set through the API, such as a fax number or a website, are left untouched either way.

curl -X PATCH https://api.esqase.com/v1/contacts/a1b2c3d4-0001-4a1b-9c2d-1234567890ab \
  -H "Authorization: Bearer $ESQASE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "lastName": "Doe-Smith",
    "emails": ["jane@example.com", "jane.doe@work.example.com"]
  }'
{
  "data": {
    "id": "a1b2c3d4-0001-4a1b-9c2d-1234567890ab",
    "type": "PERSON",
    "status": "ACTIVE",
    "name": "Jane Doe-Smith",
    "prefix": null,
    "firstName": "Jane",
    "middleName": null,
    "lastName": "Doe-Smith",
    "suffix": null,
    "nickname": null,
    "companyName": null,
    "tradeName": null,
    "companyType": null,
    "emails": ["jane@example.com", "jane.doe@work.example.com"],
    "phones": ["+13105551234"],
    "customFields": {},
    "unsupportedCustomFields": [],
    "createdAt": null,
    "updatedAt": null
  }
}

Like the create response, the update response echoes the merged record, so its createdAt and updatedAt are null.

Delete a contact

DELETE /v1/contacts/{id} is a soft delete: the contact is archived out of the active lists rather than erased, exactly as deleting it in the dashboard does.

curl -X DELETE https://api.esqase.com/v1/contacts/a1b2c3d4-0001-4a1b-9c2d-1234567890ab \
  -H "Authorization: Bearer $ESQASE_API_KEY"
{
  "data": {
    "id": "a1b2c3d4-0001-4a1b-9c2d-1234567890ab",
    "deleted": true
  }
}

Custom fields

Contacts accept your firm's contact custom fields through an optional customFields object on create and update, and single-record reads return the values back. See Custom fields in the API for the full rules.

curl -X POST https://api.esqase.com/v1/contacts \
  -H "Authorization: Bearer $ESQASE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "PERSON",
    "firstName": "Jane",
    "lastName": "Doe",
    "customFields": { "referral_source": "website" }
  }'

Full examples in JavaScript and Python

Both examples read the key from an environment variable, create a contact, then read it back.

JavaScript (fetch)

const BASE = "https://api.esqase.com/v1";
const headers = {
  Authorization: `Bearer ${process.env.ESQASE_API_KEY}`,
  "Content-Type": "application/json",
};

// Create a contact.
const createResponse = await fetch(`${BASE}/contacts`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    type: "PERSON",
    firstName: "Jane",
    lastName: "Doe",
    emails: ["jane@example.com"],
  }),
});
const { data: contact } = await createResponse.json();

// Read it back with its real timestamps.
const readResponse = await fetch(`${BASE}/contacts/${contact.id}`, { headers });
const { data: saved } = await readResponse.json();
console.log(saved.name, saved.createdAt);

// Page through every contact.
let offset = 0;
let total = Infinity;
while (offset < total) {
  const page = await fetch(`${BASE}/contacts?limit=100&offset=${offset}`, { headers });
  const { data, pagination } = await page.json();
  total = pagination.total;
  offset += pagination.limit;
  for (const row of data) console.log(row.id, row.name);
}

Python (requests)

import os
import requests

BASE = "https://api.esqase.com/v1"
headers = {
    "Authorization": f"Bearer {os.environ['ESQASE_API_KEY']}",
    "Content-Type": "application/json",
}

# Create a contact.
contact = requests.post(
    f"{BASE}/contacts",
    headers=headers,
    json={
        "type": "PERSON",
        "firstName": "Jane",
        "lastName": "Doe",
        "emails": ["jane@example.com"],
    },
).json()["data"]

# Read it back with its real timestamps.
saved = requests.get(f"{BASE}/contacts/{contact['id']}", headers=headers).json()["data"]
print(saved["name"], saved["createdAt"])

# Page through every contact.
offset, total = 0, None
while total is None or offset < total:
    payload = requests.get(
        f"{BASE}/contacts", headers=headers, params={"limit": 100, "offset": offset}
    ).json()
    total = payload["pagination"]["total"]
    offset += payload["pagination"]["limit"]
    for row in payload["data"]:
        print(row["id"], row["name"])

Errors

StatusCodeWhen
400invalid_requestA field is the wrong type or too long, a person has no first or last name, or a company has no companyName. The message names the problem.
400invalid_custom_fieldSomething in the customFields object could not be written. See Custom fields in the API.
401unauthorizedThe key is missing, wrong, revoked, or expired.
403forbiddenThe key is missing contacts:read or contacts:write, or the member who created it lacks the matching permission.
404not_foundNo contact in your firm has that id.
429rate_limitedToo many requests. Wait the Retry-After seconds.

The full list of status codes is in Authentication and scopes.

Common questions

  • Can I change a contact from a person to a company? No. The type is fixed at creation. Create a new contact instead.
  • How do I add one email without losing the others? Read the contact, add your address to the emails list you got back, and send the whole list on the update.
  • Does deleting erase the record? No. Deletes are soft deletes, the same as deleting in the app.
  • Can I filter or search the list? Not yet. The list endpoint accepts limit and offset only. Page through and filter on your side.
  • Why are createdAt and updatedAt null after a create or update? Those responses echo the record you sent. Retrieve the contact by id for its stored timestamps.