Customers
A customer is a person or company you write with. Mail from one of their addresses or domains gets their reply time, and your team sees their notes and fields next to the conversation. Reply times explains the feature; this page covers the API. Scopes: customers:read and customers:write. Customers belong to the whole organization, so a key limited to some mailboxes or domains gets 403 from these endpoints.
Last updated
The customer object
| Field | Type | Description |
|---|---|---|
| id | string | Customer ID, starting with cust_. |
| name | string or null | The person's name. |
| company | string or null | The company they work for. |
| notes | string or null | Private notes your team sees next to the customer's conversations. |
| service_level_id | string or null | The reply time this customer gets. Null means the mailbox default. |
| fields | object | Values of your customer fields, keyed by field key. |
| identifiers | array | Email addresses and domains that identify the customer's mail, each with kind (email or domain) and value. |
| external_ids | array | IDs of the customer in other systems, each with source, external_id and url. One per source. |
| created_at | string | ISO 8601 timestamp. |
| updated_at | string | ISO 8601 timestamp. |
An identifier only matches a sender whose address Shipmail can verify, so a forged From never gets a customer's reply time. Free email domains such as gmail.com, disposable domains and your own domains cannot be identifiers.
Sync from a CRM or billing system
/v1/customers/external/:source/:external_idCreates the customer that source knows by external_id and returns 201, or updates it in place and returns 200. Sending the same body again changes nothing, so run it on every CRM change. source is a lowercase slug such as hubspot or stripe. Percent-encode the external ID: it is decoded exactly once, so contact%2F42 is the ID contact/42.
curl -X PUT https://shipmail.to/api/v1/customers/external/hubspot/contact%2F42 \
-H "Authorization: Bearer sm_live_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Jane Doe",
"company": "Acme",
"service_level_id": "svl_...",
"fields": { "plan": "enterprise" },
"identifiers": [{ "kind": "domain", "value": "acme.com" }],
"url": "https://app.hubspot.com/contacts/42"
}'An address or domain that already belongs to another customer is never moved. The request is refused with 409, and the error names the owner, so you can merge the two records yourself:
{
"error": {
"type": "conflict",
"message": "acme.com already belongs to customer cust_....",
"details": [
{
"field": "identifiers",
"message": "This identifier belongs to another customer.",
"code": "identifier_conflict",
"params": { "kind": "domain", "value": "acme.com", "customer_id": "cust_..." }
}
]
}
}List and find customers
/v1/customersCursor pagination with these optional filters:
emailordomain: the customer with that exact identifier.external_sourcewithexternal_id: the customer another system knows by that ID.q: search names, companies and identifiers.
/v1/customers/:idCreate, update and delete
/v1/customers/v1/customers/:id/v1/customers/:idCreate and update take name, company, notes, service_level_id, fields, identifiers and external_ids. An update changes only what you send; identifiers, external_ids and fields replace the stored set. Changing the reply time moves the deadlines of the customer's open conversations. Deleting a customer puts their conversations back on the mailbox default.
Reply times and fields
/v1/service-levels/v1/customer-fieldsBoth are read-only and managed in the app. A reply time has name, reply_target_minutes and business_hours (null when the clock always runs). A field has the key you use in a customer's fields, a label and a type: text, number, date, url or boolean.
On inbox conversations
Conversations from the mailbox inbox API carry customer_id and reply_target. The reply target is null when no reply is owed or reply times are off for the mailbox.
"reply_target": {
"state": "at_risk",
"service_level_id": "svl_...",
"started_at": "2026-11-01T08:00:00.000Z",
"warn_at": "2026-11-01T11:00:00.000Z",
"due_at": "2026-11-01T12:00:00.000Z"
}state is on_track, at_risk once a quarter of the time is left, or missed after the deadline. The webhooks conversation.reply_target_at_risk and conversation.reply_target_missed fire at those moments.