Skip to content

Knak Send Contacts API Guide

The Knak Send Contacts API lets you create, update, and remove contacts, define custom fields, and set field values on contacts directly from your own application. This guide walks through authentication, the available endpoints, and a recommended end-to-end flow.

Working with the Contacts API generally involves three concepts:

  • Contacts: the people you send to. Every contact has a set of core attributes (email, first_name, last_name, and an optional avatar_url).
  • Fields: custom attributes you define (for example “Department” or “Start Date”). Each field has a key, a display name, and a type.
  • Field values: the value of a given field for a given contact (for example department = "Engineering").

A typical integration:

  • defines any custom fields it needs (once)
  • creates or updates contacts, optionally setting field values in the same request
  • updates field values or removes contacts over time as your source data changes

The Contacts API authenticates requests with a bearer token. Use a permanent API key generated for an API user.

  1. Create an API user and generate a key by following Setting up an API User.

  2. The account used must have Knak Send access enabled. If you are unsure whether your account has Knak Send, contact your administrator.

  3. Send the token in the Authorization header of every request:

    Authorization: Bearer YOUR_ACCESS_TOKEN

Requests without a valid, unexpired token receive a 401 UNAUTHENTICATED response. If your account does not have Knak Send enabled, you will receive a 403 ACCESS_DENIED response.

All endpoints in this guide are relative to the following base URL:

https://send.knak.io/api/public/v1

Requests and responses use JSON. Successful list and detail responses wrap their payload in a data key, and paginated responses include a meta object.

Before you can set a custom field value on a contact, the field must exist. Create one with a name and a type.

Supported field types are string, number, date, datetime, and boolean.

Example request:

POST https://send.knak.io/api/public/v1/fields
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
{
"name": "Department",
"type": "string"
}

Example response (201 Created):

{
"data": {
"id": "c0a8012e-1f2a-4b3c-9d4e-5f6a7b8c9d0e",
"key": "department",
"name": "Department",
"type": "string",
"directory_id": "9b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e"
}
}

Knak derives the field key from the name. A single word is lowercased ("Department" becomes department), and a multi-word name is converted to camel case ("Start Date" becomes startDate). Always use the key returned in the response when setting field values on contacts.

Retrieve all fields available to your company to discover their keys and types. This endpoint is not paginated.

Example request:

GET https://send.knak.io/api/public/v1/fields
Authorization: Bearer YOUR_ACCESS_TOKEN

Example response:

{
"data": [
{
"id": "c0a8012e-1f2a-4b3c-9d4e-5f6a7b8c9d0e",
"key": "department",
"name": "Department",
"type": "string",
"directory_id": "9b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e"
},
{
"id": "d1b9123f-2a3b-4c5d-ae6f-7a8b9c0d1e2f",
"key": "startDate",
"name": "Start Date",
"type": "date",
"directory_id": "9b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e"
}
]
}

Create a single contact with POST /contacts. The email, first_name, and last_name fields are required; avatar_url and field_values are optional.

field_values is an object keyed by field key, with scalar values. Each key must reference a field that exists in your API directory, and the value must match the field’s type.

Example request:

POST https://send.knak.io/api/public/v1/contacts
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
{
"email": "john@example.com",
"first_name": "John",
"last_name": "Doe",
"avatar_url": "https://example.com/avatar.jpg",
"field_values": {
"department": "Engineering",
"startDate": "2026-01-15"
}
}

Example response (201 Created):

{
"data": {
"id": "5f6a7b8c-9d0e-1f2a-3b4c-5d6e7f8a9b0c",
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"avatar_url": "https://example.com/avatar.jpg",
"supervisor_id": null,
"created_at": "2026-01-15T10:30:45+00:00",
"updated_at": "2026-01-15T10:30:45+00:00"
}
}

A few behaviors to be aware of:

  • POST /contacts is create-only. If an active contact with the same email already exists, the request returns 409 CONTACT_ALREADY_EXISTS. To update an existing contact, use the bulk endpoint or update its field values.
  • If a contact with that email was previously deleted, creating it again restores and updates the contact rather than creating a duplicate.
  • If your company has a recipient domain allowlist configured, an email whose domain is not on the allowlist returns 400 EMAIL_DOMAIN_NOT_ALLOWED.
  • Referencing an unknown field key, or supplying a value that does not match the field’s type (for example a non-numeric value for a number field), returns 422.

Use POST /contacts/bulk to create or update many contacts in a single request. Contacts are matched by email: an email that does not yet exist is created, and an existing one is updated. You can submit between 1 and 1000 contacts per request.

This endpoint supports partial success — valid rows are processed even when other rows fail. The response reports how many contacts were created, updated, and failed, along with per-row error details.

Example request:

POST https://send.knak.io/api/public/v1/contacts/bulk
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
{
"contacts": [
{
"email": "alice@example.com",
"first_name": "Alice",
"last_name": "Smith",
"field_values": { "department": "Sales" }
},
{
"email": "bob@example.com",
"first_name": "Bob",
"last_name": "Jones"
}
]
}

Example response (200 OK):

{
"data": {
"created": 2,
"updated": 0,
"failed": 0,
"errors": []
}
}

When some rows fail, those rows are reported in errors with their original index, email, and validation details, while the valid rows are still processed:

{
"data": {
"created": 1,
"updated": 0,
"failed": 1,
"errors": [
{
"index": 1,
"email": "not-an-email",
"details": {
"email": ["The email must be a valid email address."]
}
}
]
}
}

Set or replace the custom field values on an existing contact with PUT /contacts/{id}/field-values. The field_values object accepts between 1 and 20 entries, keyed by field key.

This endpoint uses replace-all semantics for your API directory: any field value that exists for the contact in the API directory but is not included in the request is removed.

Example request:

PUT https://send.knak.io/api/public/v1/contacts/5f6a7b8c-9d0e-1f2a-3b4c-5d6e7f8a9b0c/field-values
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
{
"field_values": {
"department": "Marketing",
"startDate": "2026-02-01"
}
}

Example response (200 OK):

{
"data": [
{
"field_id": "c0a8012e-1f2a-4b3c-9d4e-5f6a7b8c9d0e",
"key": "department",
"name": "Department",
"type": "string",
"value": "Marketing"
},
{
"field_id": "d1b9123f-2a3b-4c5d-ae6f-7a8b9c0d1e2f",
"key": "startDate",
"name": "Start Date",
"type": "date",
"value": "2026-02-01"
}
]
}

List contacts with GET /contacts. Results are paginated (default 25 per page, maximum 100).

Example request:

GET https://send.knak.io/api/public/v1/contacts?page[number]=1&page[size]=25
Authorization: Bearer YOUR_ACCESS_TOKEN

Supported query parameters:

ParameterExampleDescription
page[number]2Page to retrieve (1-indexed, default 1).
page[size]50Items per page (default 25, maximum 100).
sort[]email or -emailSort by email, first_name, last_name, or created_at. Prefix with - for descending.
filter[email]john@example.comFilter by email (also available for first_name and last_name). Defaults to an exact match; additional operators such as startsWith, contains, and between are also supported (see the API Reference for the full operator syntax).
searchjohnFree-text search across email, first_name, and last_name.

Example response:

{
"data": [
{
"id": "5f6a7b8c-9d0e-1f2a-3b4c-5d6e7f8a9b0c",
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"avatar_url": "https://example.com/avatar.jpg",
"supervisor_id": null,
"created_at": "2026-01-15T10:30:45+00:00",
"updated_at": "2026-01-15T10:30:45+00:00"
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1,
"from": 1,
"to": 1
}
}

To retrieve a single contact along with its field values, use GET /contacts/{id}:

{
"data": {
"id": "5f6a7b8c-9d0e-1f2a-3b4c-5d6e7f8a9b0c",
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"avatar_url": "https://example.com/avatar.jpg",
"supervisor_id": null,
"created_at": "2026-01-15T10:30:45+00:00",
"updated_at": "2026-01-15T10:30:45+00:00",
"field_values": [
{
"field_id": "c0a8012e-1f2a-4b3c-9d4e-5f6a7b8c9d0e",
"key": "department",
"name": "Department",
"type": "string",
"value": "Engineering"
}
]
}
}

To fetch only a contact’s field values, use GET /contacts/{id}/field-values, which returns the same array found under field_values above.

To see the distinct values currently in use for a field across your contacts, use GET /fields/{id}/values. Results are paginated.

Example request:

GET https://send.knak.io/api/public/v1/fields/c0a8012e-1f2a-4b3c-9d4e-5f6a7b8c9d0e/values
Authorization: Bearer YOUR_ACCESS_TOKEN

Example response:

{
"data": ["Engineering", "Marketing", "Sales"],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 3,
"last_page": 1,
"from": 1,
"to": 3
}
}

Delete a single contact with DELETE /contacts/{id}. A successful delete returns 204 No Content with an empty body. Deletes are soft deletes; the contact is removed from queries but can be restored by creating it again with the same email.

DELETE https://send.knak.io/api/public/v1/contacts/5f6a7b8c-9d0e-1f2a-3b4c-5d6e7f8a9b0c
Authorization: Bearer YOUR_ACCESS_TOKEN

To delete many contacts at once, use DELETE /contacts/bulk with an array of contact IDs (1 to 1000 per request). The response reports how many contacts were actually deleted (IDs that do not exist or were already deleted are not counted).

Example request:

DELETE https://send.knak.io/api/public/v1/contacts/bulk
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json
{
"ids": [
"5f6a7b8c-9d0e-1f2a-3b4c-5d6e7f8a9b0c",
"6a7b8c9d-0e1f-2a3b-4c5d-6e7f8a9b0c1d"
]
}

Example response (200 OK):

{
"data": {
"deleted": 2
}
}

The general flow that we recommend for keeping your contacts in sync via the API is as follows:

  1. Create an API user and generate an access token

  2. Define any custom fields your contacts need

    • Create each field with POST /fields and note the key returned in the response
    • Or call GET /fields to discover the keys and types of fields that already exist
  3. Create or update your contacts

    • For a single contact, use POST /contacts
    • To create and update many contacts at once (matched by email), use POST /contacts/bulk
    • Include a field_values object in either request to set custom field values, keyed by field key
  4. Keep field values up to date as your source data changes

    • Use PUT /contacts/{id}/field-values to replace a contact’s custom field values
  5. Read back your data to verify

    • List contacts with GET /contacts, retrieve one with GET /contacts/{id}, or fetch a contact’s values with GET /contacts/{id}/field-values
  6. Remove contacts that should no longer receive sends

    • Delete one with DELETE /contacts/{id}, or many with DELETE /contacts/bulk

Requests are rate limited per API client:

  • Standard endpoints: up to 1000 requests per minute
  • Bulk endpoints (POST /contacts/bulk and DELETE /contacts/bulk): up to 60 requests per minute

Exceeding a limit returns 429 Too Many Requests. Use the bulk endpoints to keep large synchronization jobs well within these limits.

Most application errors (HTTP 400, 401, 403, 404, 409, and 503) are returned with an error object containing a machine-readable code and a human-readable message. Branch on the code programmatically; the message text is subject to change.

{
"error": {
"code": "CONTACT_NOT_FOUND",
"message": "Contact not found."
}
}

Common error codes:

CodeHTTP StatusMeaning
UNAUTHENTICATED401Missing, invalid, or expired bearer token.
ACCESS_DENIED403Knak Send access is not enabled for the account.
EMAIL_DOMAIN_NOT_ALLOWED400The email’s domain is not on your recipient domain allowlist.
CONTACT_NOT_FOUND404No contact with the given ID exists for your company.
FIELD_NOT_FOUND404No field with the given ID exists for your company.
CONTACT_ALREADY_EXISTS409An active contact with the given email already exists (single create).
FIELD_ALREADY_EXISTS409A field with the given key already exists in your API directory.
SERVICE_UNAVAILABLE503A dependent Knak service is temporarily unavailable.

Validation errors (for example a missing required field or a value of the wrong type) use a different envelope: a top-level errors array rather than a single error object. Each entry has an identifier, a human-readable details string, and a meta object that lists the offending fields and their messages.

{
"error_at": "2026-01-15T10:30:45+00:00",
"errors": [
{
"identifier": "ValidationError",
"details": "One or more of the given request fields failed backend validation.",
"meta": {
"email": ["The email field is required."]
}
}
]
}

When you exceed a rate limit, the response uses a message/type object rather than the error shape:

{ "message": "Too Many Attempts.", "type": "HttpException" }

That’s it! You’re ready to keep Knak Send contacts and their field values in sync with your own systems.

For a complete reference of every endpoint, see the Knak Send API Reference.

Need help? Reach out via the chat bubble in Knak or email support@knak.com.