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
| Action | Method and path | Scope needed |
|---|---|---|
| List | GET /v1/contacts | contacts:read |
| Create | POST /v1/contacts | contacts:write |
| Retrieve | GET /v1/contacts/{id} | contacts:read |
| Update | PATCH /v1/contacts/{id} | contacts:write |
| Delete | DELETE /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:PERSONorCOMPANY. Set on create and fixed after that.status: the contact's lifecycle state. Contacts created through the API start asACTIVE.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 sendnameyourself.prefix,firstName,middleName,lastName,suffix,nickname: the person name parts, eachnullwhen unset.companyName,tradeName,companyType: the company name parts, eachnullwhen 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 asemails.createdAt,updatedAt: timestamps, ornullon 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):PERSONorCOMPANY. Defaults toPERSON.firstName,middleName,lastName,nickname(optional): up to 128 characters each. APERSONneeds at least afirstNameor alastName.prefix,suffix(optional): up to 32 characters each.companyName,tradeName(optional): up to 255 characters each. ACOMPANYneeds acompanyName.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
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | A 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. |
| 400 | invalid_custom_field | Something in the customFields object could not be written. See Custom fields in the API. |
| 401 | unauthorized | The key is missing, wrong, revoked, or expired. |
| 403 | forbidden | The key is missing contacts:read or contacts:write, or the member who created it lacks the matching permission. |
| 404 | not_found | No contact in your firm has that id. |
| 429 | rate_limited | Too 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
emailslist 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
limitandoffsetonly. Page through and filter on your side. - Why are
createdAtandupdatedAtnullafter a create or update? Those responses echo the record you sent. Retrieve the contact by id for its stored timestamps.