> For the complete documentation index, see [llms.txt](https://docs.usepylon.com/pylon-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.usepylon.com/pylon-docs/developer/api/conventions.md).

# API conventions

How Pylon's API handles IDs, timestamps, pagination, search filters, updates, files, and slugs

These rules apply across Pylon's API. The [API reference](/pylon-docs/developer/api/api-reference.md) lists each endpoint's parameters; this page explains how they behave.

## Requests and responses

* Send request bodies as JSON with `Content-Type: application/json`. The one exception is [`POST /attachments`](/pylon-docs/developer/api/conventions.md#upload-a-file), which takes `multipart/form-data`.
* Successful responses put the result in `data` and include a `request_id`. List and search responses also include `pagination`.
* Unknown fields are ignored, not rejected. A misspelled field name is silently dropped, so check the field names in the reference.
* Sending `null` for an optional field is the same as leaving it out.

## IDs

Most endpoints take Pylon IDs. A few path parameters also accept another identifier:

| Path                                                            | Accepts                           |
| --------------------------------------------------------------- | --------------------------------- |
| `/issues/{id}` and its sub-paths                                | Issue ID or issue number          |
| `/accounts/{id}`, and account activities, highlights, and files | Account ID or account external ID |
| `/contacts/{id}`                                                | Contact ID or contact external ID |

See [External IDs](/pylon-docs/developer/api/external-ids.md) for attaching your own identifiers to accounts.

## Timestamps

* Every timestamp uses [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339), for example `2026-10-01T00:00:00Z` or `2026-10-01T09:00:00+02:00`. A timezone (`Z` or an offset) is required.
* Responses return timestamps in UTC to the second, for example `2026-10-01T14:03:27Z`.
* In query strings, encode `+` as `%2B` or use `Z`. An unencoded `+` is read as a space, and the request fails with `400`.

## Pagination

List and search endpoints use cursor pagination.

1. Send a `limit` (optional). Leave out `cursor` for the first page.
2. If the response has `pagination.has_next_page: true`, send `pagination.cursor` as `cursor` in the next request, with the same filter and other parameters.
3. Stop when `pagination` is missing or `has_next_page` is `false`.

`GET` endpoints take `cursor` and `limit` as query parameters. `POST .../search` endpoints take them in the JSON body. Treat cursors as opaque.

Limits vary by endpoint and are listed on each endpoint's `limit` parameter. Most search endpoints default to 100 results and accept up to 1,000. `GET /issues` defaults to and allows up to 20,000.

## Listing issues by time

[`GET /issues`](/pylon-docs/developer/api/api-reference/issues.md) requires a time window:

| Question                  | Answer                                              |
| ------------------------- | --------------------------------------------------- |
| Which field is filtered?  | The issue's `created_at`.                           |
| Are the bounds inclusive? | `start_time` is inclusive. `end_time` is exclusive. |
| Are both required?        | Yes. Leaving either out returns `400`.              |
| Maximum window            | 365 days between `start_time` and `end_time`.       |
| Order                     | Newest `created_at` first.                          |
| Page size                 | `limit` defaults to 20,000, the maximum.            |

```bash
curl "https://api.usepylon.com/issues?start_time=2026-09-01T00:00:00Z&end_time=2026-10-01T00:00:00Z" \
  -H "Authorization: Bearer $PYLON_API_TOKEN"
```

To filter on another timestamp, such as `updated_at`, `resolved_at`, or `latest_message_activity_at`, use [`POST /issues/search`](/pylon-docs/developer/api/api-reference/issues.md). Search has no maximum window:

```json
{
  "filter": {
    "field": "updated_at",
    "operator": "time_range",
    "values": ["2026-09-01T00:00:00Z", "2026-10-01T00:00:00Z"]
  }
}
```

## Search filters

Every `POST .../search` endpoint except feature requests takes a `filter` object with the same structure.

| Key          | Use                                                                |
| ------------ | ------------------------------------------------------------------ |
| `operator`   | Required. See the operators below.                                 |
| `field`      | The field to compare. Required unless `operator` is `and` or `or`. |
| `value`      | A single value, for operators that take one.                       |
| `values`     | An array, for operators that take several.                         |
| `subfilters` | An array of filters. Only used with `and` and `or`.                |

Combine conditions with `and` and `or`:

```json
{
  "filter": {
    "operator": "and",
    "subfilters": [
      { "field": "state", "operator": "in", "values": ["new", "waiting_on_you"] },
      {
        "operator": "or",
        "subfilters": [
          { "field": "tags", "operator": "contains", "value": "billing" },
          { "field": "priority", "operator": "equals", "value": "urgent" }
        ]
      }
    ]
  },
  "limit": 100
}
```

* A filter can be at most **3 levels deep**, counting the top-level filter. The example above uses all 3.
* In the example, `priority` is a custom field slug. Custom fields are filtered by slug, like built-in fields.
* `and` and `or` must have `subfilters` and can't have a `field` or `value`.
* Use `value` or `values` according to the operator. Sending the other one returns `400`.

### Operators

| Operator                                                                     | Takes                    | Matches when the field...                                        |
| ---------------------------------------------------------------------------- | ------------------------ | ---------------------------------------------------------------- |
| `equals` / `not_equals`                                                      | `value`                  | is / isn't exactly the value                                     |
| `in` / `not_in`                                                              | `values`                 | is / isn't one of the values                                     |
| `contains` / `does_not_contain`                                              | `value`                  | has / doesn't have the value (multi-value fields such as `tags`) |
| `includes_all_of`                                                            | `values`                 | has every value (multi-value fields)                             |
| `string_contains` / `string_does_not_contain`                                | `value`                  | contains / doesn't contain the text                              |
| `starts_with` / `ends_with`                                                  | `value`                  | starts / ends with the text                                      |
| `time_is_after` / `time_is_before`                                           | `value` (RFC 3339)       | is strictly after / before the time                              |
| `time_range`                                                                 | `values`: `[start, end]` | is between the two times, including both ends                    |
| `greater_than`, `greater_than_or_equals`, `less_than`, `less_than_or_equals` | `value`                  | compares numerically                                             |
| `is_set` / `is_unset`                                                        | nothing                  | has / doesn't have a value                                       |

### Allowed fields

Each search endpoint accepts its own fields, and each field allows only some operators. The full field-and-operator table is in the endpoint's description in the API reference. An unknown field or a disallowed operator returns `400` with the field or operator named in the message.

| Endpoint                             | Built-in fields                                                                                                                                                                                                                                                                                                                                         | Custom fields | `search_text` |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ------------- |
| `POST /issues/search`                | `created_at`, `updated_at`, `resolved_at`, `latest_message_activity_at`, `state`, `tags`, `title`, `body_html`, `account_id`, `requester_id`, `assignee_id`, `team_id`, `follower_user_id`, `follower_contact_id`, `ticket_form_id`, `issue_type`, `issue_is_issue_group`, `customer_portal_visible`, `reopened`, `slack_channel_id`, `workspace_email` | Yes           | Yes           |
| `POST /accounts/search`              | `id`, `name`, `domains`, `tags`, `external_ids`, `owner_id`                                                                                                                                                                                                                                                                                             | Yes           | Yes           |
| `POST /contacts/search`              | `id`, `email`, `name`, `account_id`                                                                                                                                                                                                                                                                                                                     | Yes           | Yes           |
| `POST /users/search`                 | `email` (`equals`, `in`, `not_in`)                                                                                                                                                                                                                                                                                                                      | No            | No            |
| `POST /custom-objects/{type}/search` | `name`, `account`, `created_at`, `updated_at`                                                                                                                                                                                                                                                                                                           | Yes           | No            |
| `POST /tasks/search`                 | `account_id`, `project_id`, `status`, `assignee_id`, `milestone_id`, `created_at`, `due_date`, `updated_at`                                                                                                                                                                                                                                             | Yes           | No            |
| `POST /projects/search`              | `account_id`, `status`, `owner_id`, `is_archived`, `created_at`, `updated_at`                                                                                                                                                                                                                                                                           | Yes           | No            |
| `POST /milestones/search`            | `project_id`, `account_id`, `due_date`, `created_at`, `updated_at`, `started_at`, `completed_at`                                                                                                                                                                                                                                                        | No            | No            |
| `POST /call-recordings/search`       | `account_id`, `source`, `title`, `start_time`                                                                                                                                                                                                                                                                                                           | No            | No            |
| `POST /surveys/search`               | `updated_at`                                                                                                                                                                                                                                                                                                                                            | No            | No            |
| `POST /audit-logs/search`            | `action`, `action_happened_at`, `object_id`                                                                                                                                                                                                                                                                                                             | No            | No            |

[`POST /feature-requests/search`](/pylon-docs/developer/api/api-reference/feature-requests.md) doesn't take a `filter`. It takes `query`, `request_statuses`, `account_ids`, and `limit`.

`search_text` runs a fuzzy text search. When you also send a `filter`, results must match both.

### Values to use

* **`state`:** `new`, `waiting_on_you`, `waiting_on_customer`, `on_hold`, `closed`, or a custom status slug.
* **`tags`:** the tag name.
* **Custom fields:** set `field` to the field's slug. For select and multi-select fields, use option slugs as values. See [Find slugs](/pylon-docs/developer/api/conventions.md#find-slugs).

Operators for a custom field depend on its type:

| Custom field type                       | Operators                                                                                                                                  |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Text, URL, Email, Select                | `equals`, `not_equals`, `in`, `not_in`, `string_contains`, `string_does_not_contain`, `starts_with`, `ends_with`, `is_set`, `is_unset`     |
| Number                                  | `equals`, `not_equals`, `in`, `not_in`, `greater_than`, `greater_than_or_equals`, `less_than`, `less_than_or_equals`, `is_set`, `is_unset` |
| Date, Datetime                          | `equals`, `not_equals`, `in`, `not_in`, `time_is_after`, `time_is_before`, `time_range`, `is_set`, `is_unset`                              |
| Multi-select                            | `contains`, `does_not_contain`, `in`, `not_in`, `includes_all_of`, `is_set`, `is_unset`                                                    |
| True/False, User, Contact, Relationship | `equals`, `not_equals`, `in`, `not_in`, `is_set`, `is_unset`                                                                               |

## Updating records

`PATCH` endpoints change only the fields you send.

* **Omitted fields don't change.** `null` counts as omitted.
* **Arrays replace the whole list.** Send the complete list you want, not just additions. Send `[]` to clear it.
* **Clear a reference with an empty string.** For example, `"assignee_id": ""` unassigns an issue.
* **`custom_fields` updates only the fields you list.** Other custom fields keep their values. For a multi-select field, `values` replaces the current selection. To clear a field, send its `slug` with no `value` or `values`.

| Endpoint                                            | Field                                     | Behavior                                                                            |
| --------------------------------------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------- |
| `PATCH /issues/{id}`                                | `tags`                                    | Replaces all tags on the issue.                                                     |
|                                                     | `assignee_id`, `team_id`, `requester_id`  | `""` removes the value.                                                             |
| `PATCH /accounts/{id}`                              | `channels`                                | Replaces the linked channels. Channels you leave out are unlinked from the account. |
|                                                     | `domains`, `primary_domain`               | Replaces the domains. `primary_domain` must be in `domains`.                        |
|                                                     | `tags`, `external_ids`                    | Replaces the list.                                                                  |
|                                                     | `subaccount_ids`                          | Replaces the subaccounts. Accounts you leave out stop being subaccounts.            |
|                                                     | `owner_id`                                | `""` removes the owner.                                                             |
| `PATCH /accounts` (bulk)                            | `tags`                                    | Set `tags_apply_mode` to `replace` (default), `append_only`, or `remove_only`.      |
| `PATCH /contacts/{id}`                              | `email`                                   | Replaces all of the contact's emails with this one.                                 |
|                                                     | `emails`, `phone_numbers`, `external_ids` | Replaces the list.                                                                  |
|                                                     | `account_id`                              | `""` removes the contact from its account.                                          |
| `PATCH /teams/{id}`                                 | `user_ids`                                | Replaces the team's members.                                                        |
| `PATCH /knowledge-bases/{id}/articles/{article_id}` | `tag_ids`                                 | Replaces the article's tags.                                                        |

### Updating knowledge base articles

* `body_html` saves a new **draft**. The published article doesn't change until you publish.
* To publish the new body in the same request, also send `"publish_updated_body_html": true` or `"is_published": true`.
* `"is_published": false` unpublishes the article.
* `title`, `is_unlisted`, `visibility_config`, `collection_id`, and `author_user_id` apply to the live article right away.
* To update a translation, add `language`. The same draft and publish rules apply.

## Files and attachments

### Upload a file

[`POST /attachments`](/pylon-docs/developer/api/api-reference/attachments.md) takes `multipart/form-data` with these fields:

| Field         | Required                    | Description                               |
| ------------- | --------------------------- | ----------------------------------------- |
| `file`        | One of `file` or `file_url` | The file to upload. One file per request. |
| `file_url`    | One of `file` or `file_url` | A URL Pylon downloads the file from.      |
| `description` | No                          | A description of the file.                |

```bash
curl https://api.usepylon.com/attachments \
  -H "Authorization: Bearer $PYLON_API_TOKEN" \
  -F "file=@screenshot.png" \
  -F "description=Error screenshot"
```

The response's `data` has the file's `id`, `name`, `url`, and `description`. Pass `url` in `attachment_urls` when you create an issue, reply, or internal note.

* Files up to 100 MB are supported. Larger files can be rejected with `413`; share those as a link instead. Email and chat channels may have smaller limits.
* Executables and script files are rejected with `400`.
* `attachment_urls` also accepts any publicly reachable URL. Pylon downloads the file and stores its own copy.

### Download a file

An issue's `attachment_urls` lists the files on its messages. Each message from [`GET /issues/{id}/messages`](/pylon-docs/developer/api/api-reference/messages.md) has its files in `file_urls`.

* Most of these are signed links. Download them with a plain `GET`, without an `Authorization` header.
* They don't expire, so you can store them. Anyone with a link can download the file, so treat links as sensitive.
* Use the URL exactly as returned. Changing it invalidates the signature.
* Some older attachments return the URL they were originally stored with.

## Find slugs

The API refers to custom fields, select options, and statuses by slug, not by label.

| Slug          | In Pylon                                                                                                                             | Through the API                                                                                                   |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
| Custom field  | The **API Slug** column on field settings pages, such as **Settings > Issue data > Fields** and **Settings > Account data > Fields** | [`GET /custom-fields?object_type=issue`](/pylon-docs/developer/api/api-reference/custom-fields.md) returns `slug` |
| Select option | **View and edit slugs** in the field's **…** menu                                                                                    | `select_metadata.options[].slug` in the same response                                                             |
| Issue status  | —                                                                                                                                    | [`GET /issue-statuses`](/pylon-docs/developer/api/api-reference/issues.md) returns `slug`                         |
| User status   | —                                                                                                                                    | [`GET /user-statuses`](/pylon-docs/developer/api/api-reference/users.md) returns `slug`                           |

`object_type` can be `account`, `issue`, `contact`, `task`, `project`, `meeting`, or `opportunity`. If you change a slug, update every integration that uses the old one.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.usepylon.com/pylon-docs/developer/api/conventions.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
