Sendry Docs
API Reference

Contacts

Manage your subscriber contacts via the API.

Contacts represent individual email subscribers. They can belong to one or more audiences and can be individually suppressed or unsubscribed.


Create contact

POST /v1/contacts

Requires sending_access scope.

Request body

{
  "email": "alice@example.com",
  "first_name": "Alice",
  "last_name": "Smith",
  "metadata": {"plan": "pro", "source": "signup"}
}
FieldTypeRequiredDescription
emailstringYesContact's email address.
first_namestringNoFirst name.
last_namestringNoLast name.
metadataRecord<string, any>NoArbitrary key/value data.

Response

{
  "id": "con_abc123",
  "email": "alice@example.com",
  "first_name": "Alice",
  "last_name": "Smith",
  "metadata": {"plan": "pro"},
  "subscribed": true,
  "created_at": "2025-03-12T09:00:00Z"
}

Get contact

GET /v1/contacts/:id

Requires read_only scope.


Get contact by email

GET /v1/contacts/by-email

Requires read_only scope. Looks up a contact by email address, case-insensitively.

Query parameters

ParameterTypeDefaultDescription
emailstringContact email address. Required.
audience_idstringOptional audience membership filter.

Response

Returns the same shape as Get contact.

{
  "id": "con_abc123",
  "email": "alice@example.com",
  "first_name": "Alice",
  "last_name": "Smith",
  "metadata": {"plan": "pro"},
  "unsubscribed": false,
  "properties": {"plan": "pro"},
  "created_at": "2025-03-12T09:00:00Z",
  "updated_at": "2025-03-12T09:00:00Z"
}

Returns 404 not_found if no contact with that email exists.


Update contact by email

PATCH /v1/contacts/by-email

Requires sending_access scope. Partially updates a contact identified by email, case-insensitively.

Query parameters

ParameterTypeDefaultDescription
emailstringContact email address. Required.

Request body

All fields are optional; only include fields you want to change.

FieldTypeRequiredDescription
emailstringNoUpdated email address.
first_namestring | nullNoUpdated first name.
last_namestring | nullNoUpdated last name.
metadataobject | nullNoUpdated metadata key/value pairs.
unsubscribedbooleanNoUpdated unsubscribe status.
{
  "first_name": "Alicia",
  "metadata": {"plan": "business"}
}

Response

Returns the same shape as Get contact. The body options match Update contact.


Get contact activity timeline

GET /v1/contacts/:id/activity

Requires read_only scope. Returns the contact's email events (sends, opens, clicks, bounces, and so on), newest first.

Query parameters

ParameterTypeDefaultDescription
cursorstringPagination cursor from the previous response's next_cursor.

Response

{
  "data": [
    {
      "id": "evt_abc123",
      "type": "opened",
      "email_id": "email_abc123",
      "subject": "Welcome to Acme!",
      "from": "Acme <hello@acme.com>",
      "recipient": "alice@example.com",
      "metadata": {
        "user_agent": "Mozilla/5.0...",
        "ip": "1.2.3.4"
      },
      "created_at": "2025-03-12T09:15:30Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Pagination is cursor-based. Pass next_cursor as the cursor parameter to get the next page.


Get contact property values

GET /v1/contacts/:id/properties

Requires read_only scope. Returns every property definition for the organization alongside this contact's current value for it.

Response

{
  "data": [
    {
      "property_id": "cprop_abc123",
      "name": "plan",
      "label": "Plan",
      "type": "string",
      "value": "pro"
    }
  ]
}

value is null when the contact has no value set for that property. type is one of string, number, boolean, or date.


Set contact property values

PUT /v1/contacts/:id/properties

Requires sending_access scope. Upserts the supplied property values for the contact. Property names must already exist as definitions.

Request body

A flat map of property name to value. Values may be a string, number, boolean, or null.

{
  "plan": "business",
  "seats": 25,
  "trial": false
}

Response

{
  "updated": 3
}

List contacts

GET /v1/contacts

Requires read_only scope.

Query parameters

ParameterTypeDefaultDescription
limitnumber50Results per page (1–100).
cursorstringPagination cursor.

Update contact

PUT /v1/contacts/:id

Requires sending_access scope.


Delete contact

DELETE /v1/contacts/:id

Requires full_access scope. Permanently removes the contact.


Bulk delete contacts

DELETE /v1/contacts/bulk

Requires full_access scope. Permanently removes up to 500 contacts in a single request.

Request body

FieldTypeRequiredDescription
idsstring[]YesContact IDs to delete. Between 1 and 500.
{
  "ids": ["con_abc123", "con_abc124"]
}

Response

{
  "deleted": 2
}

Bulk import contacts

POST /v1/contacts/import

Requires full_access scope. Import up to 1,000 contacts in a single request. The import checks your plan's contact limit before inserting — if the batch would exceed your limit, the entire import is rejected.

Request body

{
  "contacts": [
    {"email": "alice@example.com", "first_name": "Alice"},
    {"email": "bob@example.com", "first_name": "Bob"}
  ]
}

Response

{
  "imported": 2,
  "skipped": 0,
  "errors": []
}

Import contacts from file

POST /v1/contacts/import/file

Requires full_access scope. Uploads a CSV or Excel file and imports up to 1,000 contacts. Existing contacts (matched by email) are updated, new ones are created. Send this request as multipart/form-data.

Request body

FieldTypeRequiredDescription
filefileYes.csv, .xlsx, or .xls file. Max 10 MB.
mappingstringYesJSON object mapping contact field names to file column headers. Must include an email key.
audience_idstringNoAudience ID to add all imported contacts to.

mapping supports email, first_name, last_name, unsubscribed, and arbitrary metadata.<key> entries. For example, {"email": "Email Address", "first_name": "First", "metadata.plan": "Plan"}.

Response

{
  "created": 8,
  "updated": 2,
  "total": 10
}

List property definitions

GET /v1/contacts/properties

Requires read_only scope. Returns every custom contact property defined for the organization.

Response

{
  "data": [
    {
      "id": "cprop_abc123",
      "name": "plan",
      "label": "Plan",
      "type": "string",
      "default_value": "free",
      "created_at": "2025-03-12T09:00:00Z"
    }
  ]
}

Create property definition

POST /v1/contacts/properties

Requires full_access scope. Defines a new custom contact property. An organization can define up to 50 properties.

Request body

FieldTypeRequiredDescription
namestringYesMachine-readable key. 1–50 characters, lowercase letters, digits, and underscores only.
labelstringYesHuman-readable label. 1–200 characters.
typestringYesValue type: string, number, boolean, or date.
default_valuestringNoDefault value, serialized as a string.

Example request

{
  "name": "plan",
  "label": "Plan",
  "type": "string",
  "default_value": "free"
}

Response

{
  "id": "cprop_abc123",
  "name": "plan",
  "label": "Plan",
  "type": "string",
  "default_value": "free",
  "created_at": "2025-03-12T09:00:00Z"
}

Returns 409 property_exists if a property with the same name already exists, and 403 property_limit_reached once the organization has 50 properties.


Delete property definition

DELETE /v1/contacts/properties/:propId

Requires full_access scope. Deletes the definition and all associated contact values.

Response

{
  "deleted": true
}

Plan limits

PlanContacts
Free500
Pro10,000
Business100,000
EnterpriseUnlimited

Suppression and unsubscribes

Contacts who unsubscribe or hard-bounce are automatically added to the suppression list. Attempting to send to a suppressed address returns an error.

See the suppression endpoints for manual management:

  • GET /v1/suppression — list suppressed addresses
  • POST /v1/suppression — add a suppression
  • DELETE /v1/suppression/:email — remove a suppression

Unsubscribe management:

  • GET /v1/unsubscribes — list all unsubscribes
  • POST /v1/unsubscribes — add an unsubscribe
  • DELETE /v1/unsubscribes/:id — remove an unsubscribe

On this page