Knak Contacts API

Create, retrieve and list contacts in Knak Send.

OpenAPI Specification

knak-contacts-api-openapi.yml Raw ↑
openapi: 3.1.0
info:
  description: |
    # Overview
    Welcome to the developer documentation for the Knak Send API.
    We provide a RESTful interface to key resources within Knak Send to enable your own automation workflows.
    This API will allow you to automate processes regarding contact and field management within your Knak Send environment.
    You can download the formal definition of this public interface in OpenAPI 3 (formerly Swagger) format using the link above.

    ## Endpoint
    `https://send.knak.io/api/public/v1`

    ## Authentication
    All requests are authenticated using a Bearer token in the `Authorization` header:
    ```
    curl --location --request GET 'https://send.knak.io/api/public/v1/contacts' \
    --header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbG...'
    ```
    A token can be created as a non-expiring token through the Enterprise UI, via the [API Access menu](https://enterprise.knak.io/account/api-access). The account associated with the token must have Knak Send access enabled.

    ## Errors
    Errors in requests made to the API can be viewed directly from the response code that is returned.

    Below are a list of the common error responses returned and an explanation of what they mean.

    | Code | Reason |Description |
    | ----------------| ----------- | ----------- |
    | **400**  | **Bad Request** | The request was rejected (for example, a contact's email domain is not in the account's allow list). |
    | **401**      | **Unauthenticated** | The access token is missing, invalid, or expired. |
    | **403**    | **Forbidden** | The account does not have Knak Send access enabled, or the token lacks the required scope. |
    | **404**  | **Not Found** | The requested resource could not be found. Verify that the resource exists and that you are using the correct identifier. |
    | **409**  | **Conflict** | The resource could not be created because it already exists (for example, a field with the same generated key). |
    | **422**  | **Unprocessable Entity** | One or more request fields failed backend validation. The `meta` object lists the offending fields. |
    | **429**  | **Too Many Requests** | The rate limit for the endpoint has been exceeded. |
    | **503**  | **Service Unavailable** | An upstream authentication service is temporarily unavailable. This can occur on any request; retry after a short delay. |

    Three error response shapes are used. Where an error `code` (or `identifier`) is present, branch on it
    programmatically; the human-readable `message`/`details` text is subject to change.

    Authentication, authorization, bad-request, not-found, conflict and service errors
    (400, 401, 403, 404, 409, 503) return a compact `error` object with a machine-readable `code`:
    ```json
    { "error": { "code": "UNAUTHENTICATED", "message": "Invalid or expired access token." } }
    ```
    Validation errors (422) return an `errors` array:
    ```json
    {
      "error_at": "2026-06-02T13:56:31+00:00",
      "errors": [
        {
          "identifier": "ValidationError",
          "details": "One or more of the given request fields failed backend validation.",
          "meta": { "email": ["The email field is required."] }
        }
      ]
    }
    ```
    Rate-limit (429) errors return a `message`/`type` object:
    ```json
    { "message": "Too Many Attempts.", "type": "HttpException" }
    ```

    ## Rate limiting
    Requests are rate limited per API client. Standard endpoints allow up to **1000 requests per minute**.
    Bulk endpoints (such as `POST /contacts/bulk`) are limited to **60 requests per minute**. Exceeding a limit
    returns a `429 Too Many Requests` response.

    ## Pagination
    List endpoints that can return large result sets are paginated using the `page` query parameter:

    - `page[number]` — the page to return (1-based, default `1`).
    - `page[size]` — the number of records per page (default `25`, maximum `100`).

    Paginated responses include a `meta` object describing the result window (`total`, `current_page`,
    `last_page`, `per_page`, `from`, `to`).

    ## Filtering
    List endpoints support filtering on specific fields using the `filter` query parameter, in the form
    `filter[field_name]=value`. The fields that can be filtered are listed on each endpoint.

    For simple equality, pass the value directly: `filter[first_name]=Jane`. For other comparisons, pass an
    `operator` and `value`: `filter[first_name][operator]=startsWith&filter[first_name][value]=Ja`. Supported
    operators include `=`, `!=`, `>`, `<`, `~`, `contains`, `notContains`, `startsWith`, `between`, `exists`
    and `notExists`.

    ## Sorting
    List endpoints support sorting via the `sort` query parameter. Pass one or more field names; prefix a field
    with `-` for descending order. For example `sort[]=last_name&sort[]=-created_at`. The fields that can be
    sorted are listed on each endpoint.
  version: V1
  title: Knak Send Contacts API Reference — Contacts
  x-logo:
    url: https://s3.amazonaws.com/assets.knak.io/img/Knak-Logo-Medium.png
servers:
- url: https://send.knak.io/api/public/v1
  description: production
tags:
- name: Contacts
  description: Create, retrieve and list contacts in Knak Send.
paths:
  /contacts:
    get:
      tags:
      - Contacts
      summary: List contacts
      description: |
        Returns a paginated list of contacts. Supports pagination, sorting and filtering.

        - **Sortable fields:** `email`, `first_name`, `last_name`, `created_at`
        - **Filterable fields:** `email`, `first_name`, `last_name`
      operationId: listContacts
      parameters:
      - $ref: '#/components/parameters/PageNumber'
      - $ref: '#/components/parameters/PageSize'
      - name: sort[]
        in: query
        required: false
        description: 'Fields to sort by, passed as a repeated `sort[]` parameter. Prefix a field with
          `-` for descending order. Allowed fields: `email`, `first_name`, `last_name`, `created_at`.'
        style: form
        explode: true
        schema:
          type: array
          items:
            type: string
            enum:
            - email
            - first_name
            - last_name
            - created_at
            - -email
            - -first_name
            - -last_name
            - -created_at
        example:
        - -created_at
      - name: filter[email]
        in: query
        required: false
        description: Filter by email address.
        schema:
          type: string
        example: jane.doe@example.com
      - name: filter[first_name]
        in: query
        required: false
        description: Filter by first name.
        schema:
          type: string
      - name: filter[last_name]
        in: query
        required: false
        description: Filter by last name.
        schema:
          type: string
      responses:
        '200':
          description: A paginated list of contacts.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Contact'
                  meta:
                    $ref: '#/components/schemas/PaginationMeta'
              examples:
                default:
                  value:
                    data:
                    - id: 9b1c2d3e-4f56-7890-abcd-ef1234567890
                      first_name: Jane
                      last_name: Doe
                      email: jane.doe@example.com
                      avatar_url: https://cdn.example.com/avatars/jane.png
                      supervisor_id: 1a2b3c4d-5e6f-7890-abcd-ef0987654321
                      created_at: '2026-05-01T12:00:00+00:00'
                      updated_at: '2026-05-20T09:30:00+00:00'
                    meta:
                      total: 1
                      current_page: 1
                      last_page: 1
                      per_page: 25
                      from: 1
                      to: 1
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    post:
      tags:
      - Contacts
      summary: Create a contact
      description: Creates a single contact. To create or update many contacts at once, use `POST /contacts/bulk`.
      operationId: createContact
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactInput'
            examples:
              default:
                value:
                  email: jane.doe@example.com
                  first_name: Jane
                  last_name: Doe
                  avatar_url: https://cdn.example.com/avatars/jane.png
                  field_values:
                    department: Engineering
                    startDate: '2026-01-15'
      responses:
        '201':
          description: The contact was created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Contact'
        '400':
          $ref: '#/components/responses/EmailDomainNotAllowed'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/ContactConflict'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /contacts/{id}:
    get:
      tags:
      - Contacts
      summary: Retrieve a contact
      description: Returns a single contact by ID, including its field values.
      operationId: getContact
      parameters:
      - $ref: '#/components/parameters/ContactId'
      responses:
        '200':
          description: The requested contact.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/ContactDetail'
              examples:
                default:
                  value:
                    data:
                      id: 9b1c2d3e-4f56-7890-abcd-ef1234567890
                      first_name: Jane
                      last_name: Doe
                      email: jane.doe@example.com
                      avatar_url: https://cdn.example.com/avatars/jane.png
                      supervisor_id: 1a2b3c4d-5e6f-7890-abcd-ef0987654321
                      created_at: '2026-05-01T12:00:00+00:00'
                      updated_at: '2026-05-20T09:30:00+00:00'
                      field_values:
                      - field_id: f1e2d3c4-b5a6-7890-abcd-ef1234567890
                        key: department
                        name: Department
                        type: string
                        value: Engineering
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    delete:
      tags:
      - Contacts
      summary: Delete a contact
      description: Soft-deletes a contact by ID. The contact is removed from listings but retained internally.
      operationId: deleteContact
      parameters:
      - $ref: '#/components/parameters/ContactId'
      responses:
        '204':
          description: The contact was deleted. The response body is empty.
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /contacts/{id}/field-values:
    get:
      tags:
      - Contacts
      summary: List a contact's field values
      description: Returns the field values associated with a single contact.
      operationId: getContactFieldValues
      parameters:
      - $ref: '#/components/parameters/ContactId'
      responses:
        '200':
          description: The contact's field values.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/FieldValue'
              examples:
                default:
                  value:
                    data:
                    - field_id: f1e2d3c4-b5a6-7890-abcd-ef1234567890
                      key: department
                      name: Department
                      type: string
                      value: Engineering
                    - field_id: a9b8c7d6-e5f4-3210-abcd-ef0987654321
                      key: startDate
                      name: Start Date
                      type: date
                      value: '2026-01-15'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    put:
      tags:
      - Contacts
      summary: Replace a contact's field values
      description: |
        Replaces the field values for a single contact. Each key in `field_values` must match the `key` of an
        existing field (see `GET /fields`) and its value is validated against the field's type. Between 1 and 20
        field values may be provided.
      operationId: replaceContactFieldValues
      parameters:
      - $ref: '#/components/parameters/ContactId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FieldValuesInput'
            examples:
              default:
                value:
                  field_values:
                    department: Engineering
                    startDate: '2026-01-15'
      responses:
        '200':
          description: The contact's updated field values.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/FieldValue'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /contacts/bulk:
    post:
      tags:
      - Contacts
      summary: Bulk upsert contacts
      description: |
        Creates or updates up to 1000 contacts in a single request, matching existing contacts by email.

        Validation is performed per contact. The request as a whole succeeds with a `200` response even when
        some individual contacts fail; the response body reports counts of `created`, `updated` and `failed`
        contacts, along with an `errors` array describing each failure by its index in the submitted array.

        This endpoint is subject to the stricter bulk rate limit (60 requests per minute).
      operationId: bulkUpsertContacts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - contacts
              properties:
                contacts:
                  type: array
                  minItems: 1
                  maxItems: 1000
                  items:
                    $ref: '#/components/schemas/ContactInput'
            examples:
              default:
                value:
                  contacts:
                  - email: jane.doe@example.com
                    first_name: Jane
                    last_name: Doe
                    field_values:
                      department: Engineering
                  - email: john@notallowed.com
                    first_name: John
                    last_name: Smith
      responses:
        '200':
          description: |
            The bulk operation was processed. Inspect `created`, `updated`, `failed` and `errors` to determine
            the outcome of each contact.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/BulkUpsertResult'
              examples:
                default:
                  value:
                    data:
                      created: 1
                      updated: 0
                      failed: 1
                      errors:
                      - index: 1
                        email: john@notallowed.com
                        details:
                          email:
                          - Domain is not allowed
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    delete:
      tags:
      - Contacts
      summary: Bulk delete contacts
      description: |
        Deletes up to 1000 contacts in a single request, identified by their IDs. The response reports the
        number of contacts actually deleted; IDs that do not match an existing contact are ignored rather than
        causing the request to fail.

        This endpoint is subject to the stricter bulk rate limit (60 requests per minute).
      operationId: bulkDeleteContacts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - ids
              properties:
                ids:
                  type: array
                  minItems: 1
                  maxItems: 1000
                  items:
                    type: string
                    maxLength: 255
            examples:
              default:
                value:
                  ids:
                  - 9b1c2d3e-4f56-7890-abcd-ef1234567890
                  - 1a2b3c4d-5e6f-7890-abcd-ef0987654321
      responses:
        '200':
          description: The bulk delete was processed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      deleted:
                        type: integer
                        description: The number of contacts that were deleted.
              examples:
                default:
                  value:
                    data:
                      deleted: 2
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/TooManyRequests'
components:
  responses:
    Forbidden:
      description: Knak Send access is not enabled for the account, or the token lacks the required scope.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorObject'
          example:
            error:
              code: ACCESS_DENIED
              message: Knak Send access is not enabled for this account.
    TooManyRequests:
      description: |
        The rate limit for the endpoint has been exceeded. A `Retry-After` header indicates how many seconds
        to wait before retrying.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/HttpError'
          example:
            message: Too Many Attempts.
            type: HttpException
    NotFound:
      description: The requested resource could not be found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorObject'
          example:
            error:
              code: CONTACT_NOT_FOUND
              message: Contact not found.
    EmailDomainNotAllowed:
      description: The contact's email domain is not in the account's allow list.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorObject'
          example:
            error:
              code: EMAIL_DOMAIN_NOT_ALLOWED
              message: The address john@notallowed.com has a domain that is not in your allow list.
    ValidationError:
      description: One or more request fields failed backend validation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorList'
          example:
            error_at: '2026-06-02T13:56:31+00:00'
            errors:
            - identifier: ValidationError
              details: One or more of the given request fields failed backend validation.
              meta:
                email:
                - The email field is required.
    Unauthenticated:
      description: The access token is missing, invalid or expired.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorObject'
          example:
            error:
              code: UNAUTHENTICATED
              message: Invalid or expired access token.
    ContactConflict:
      description: A contact with the given email already exists.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorObject'
          example:
            error:
              code: CONTACT_ALREADY_EXISTS
              message: A contact with this email already exists.
  schemas:
    BulkUpsertResult:
      type: object
      description: The outcome of a bulk upsert operation.
      properties:
        created:
          type: integer
          description: The number of contacts created.
        updated:
          type: integer
          description: The number of existing contacts updated.
        failed:
          type: integer
          description: The number of contacts that failed validation or processing.
        errors:
          type: array
          description: One entry per failed contact.
          items:
            type: object
            properties:
              index:
                type: integer
                description: The 0-based position of the contact in the submitted `contacts` array.
              email:
                type:
                - string
                - 'null'
                description: The email of the failed contact, if available.
              details:
                type: object
                description: A map of field name to an array of validation messages.
                additionalProperties:
                  type: array
                  items:
                    type: string
    ErrorObject:
      type: object
      description: The compact error shape returned for authentication, authorization and conflict errors.
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              description: A machine-readable error code.
            message:
              type: string
              description: A human-readable description of the error.
    FieldValue:
      type: object
      description: A single field value belonging to a contact.
      properties:
        field_id:
          type: string
          description: The ID of the field this value belongs to.
        key:
          type:
          - string
          - 'null'
          description: The generated key of the field.
        name:
          type:
          - string
          - 'null'
          description: The human-readable name of the field.
        type:
          description: The data type of the field. May be `null` if the field's metadata is unavailable.
          oneOf:
          - $ref: '#/components/schemas/FieldType'
          - type: 'null'
        value:
          type: string
          description: The field value, represented as a string.
    PaginationMeta:
      type: object
      description: Pagination metadata describing the current result window.
      properties:
        total:
          type: integer
          description: The total number of matching records.
        current_page:
          type: integer
        last_page:
          type: integer
        per_page:
          type: integer
        from:
          type:
          - integer
          - 'null'
          description: The index of the first record on the current page.
        to:
          type:
          - integer
          - 'null'
          description: The index of the last record on the current page.
    ContactDetail:
      type: object
      description: A contact with its associated field values, returned when retrieving a single contact.
      allOf:
      - $ref: '#/components/schemas/Contact'
      - type: object
        properties:
          field_values:
            type: array
            items:
              $ref: '#/components/schemas/FieldValue'
    FieldValuesInput:
      type: object
      description: The payload used to replace a contact's field values.
      required:
      - field_values
      properties:
        field_values:
          type: object
          minProperties: 1
          maxProperties: 20
          description: |
            A map of field key to scalar value. Each key must match the `key` of an existing field
            (see `GET /fields`). Between 1 and 20 entries may be provided. Replaces the contact's existing
            field values.
          additionalProperties:
            type:
            - string
            - number
            - boolean
          example:
            department: Engineering
            startDate: '2026-01-15'
    ContactInput:
      type: object
      description: The payload used to create or upsert a contact.
      required:
      - email
      - first_name
      - last_name
      properties:
        email:
          type: string
          format: email
          maxLength: 255
          description: The contact's email address. Used as the match key for bulk upserts.
        first_name:
          type: string
          maxLength: 255
        last_name:
          type: string
          maxLength: 255
        avatar_url:
          type:
          - string
          - 'null'
          format: uri
          description: |
            URL of the contact's avatar image. For `POST /contacts` this must be a valid HTTP(S) URL.
            For bulk upserts (`POST /contacts/bulk`) the value is accepted as any string and is not validated
            as a URL.
        field_values:
          type: object
          description: |
            A map of field key to scalar value. Each key must match the `key` of an existing field
            (see `GET /fields`). Values are validated against the field's type.
          additionalProperties:
            type:
            - string
            - number
            - boolean
          example:
            department: Engineering
            startDate: '2026-01-15'
    HttpError:
      type: object
      description: The error shape returned for rate-limit (429) errors.
      properties:
        message:
          type: string
          description: A human-readable description of the error.
        type:
          type: string
          description: The error type. Always `HttpException` for this shape.
    FieldType:
      type: string
      description: The data type of a field.
      enum:
      - string
      - number
      - date
      - datetime
      - boolean
    Contact:
      type: object
      description: A contact as returned in list and create responses.
      properties:
        id:
          type: string
          description: The unique identifier of the contact.
        first_name:
          type: string
        last_name:
          type: string
        email:
          type: string
          format: email
        avatar_url:
          type:
          - string
          - 'null'
          format: uri
          description: URL of the contact's avatar image, if set.
        supervisor_id:
          type:
          - string
          - 'null'
          description: The ID of this contact's supervisor, if one is assigned.
        created_at:
          type:
          - string
          - 'null'
          format: date-time
          description: ISO 8601 creation timestamp.
        updated_at:
          type:
          - string
          - 'null'
          format: date-time
          description: ISO 8601 last-updated timestamp.
    ErrorList:
      type: object
      description: The error shape returned for validation and not-found errors.
      properties:
        error_at:
          type: string
          format: date-time
          description: ISO 8601 timestamp of when the error occurred.
        errors:
          type: array
          items:
            type: object
            properties:
              identifier:
                type: string
                description: A machine-readable identifier for the error type.
              details:
                type: string
                description: A human-readable description of the error.
              meta:
                type: object
                description: Additional context. For validation errors, a map of field name to validation
                  messages.
  parameters:
    PageNumber:
      name: page[number]
      in: query
      required: false
      description: The page of results to return (1-based).
      schema:
        type: integer
        minimum: 1
        default: 1
    PageSize:
      name: page[size]
      in: query
      required: false
      description: The number of records per page.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
    ContactId:
      name: id
      in: path
      required: true
      description: The unique identifier of the contact.
      schema:
        type: string
      example: 9b1c2d3e-4f56-7890-abcd-ef1234567890