> ## Documentation Index
> Fetch the complete documentation index at: https://docs.routera.io/llms.txt
> Use this file to discover all available pages before exploring further.

# API introduction

> Routera v2 REST API — authentication, base URL, and object response format

## Base URL

```
https://api.routera.io
```

All paths in this reference are relative to that base URL.

## Authentication

Integrations authenticate using the OAuth 2.0 **client credentials** grant.

### 1. Obtain an access token

`POST /oauth2/token` exchanges `client_id` and `client_secret` for a JWT. See [Obtain access token](/api-reference/auth/oauth2-token) for the full request format.

```http theme={null}
POST https://api.routera.io/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET
```

The response includes `access_token`, `expires_in`, and `token_type` (`Bearer`). There is no refresh token — request a new access token when it expires.

### 2. Call the API

Every other route requires the access token:

```http theme={null}
Authorization: Bearer <access_token>
```

The JWT must include a valid account context (set automatically for integrations linked to your account). Requests without it receive `401` with:

```json theme={null}
{ "message": "Unauthorized: missing account context", "error": true }
```

## Routes

### Authentication

| Route           | HTTP |
| --------------- | ---- |
| `/oauth2/token` | POST |

### Objects API

| Route                                                                                       | HTTP               |
| ------------------------------------------------------------------------------------------- | ------------------ |
| `/contacts`, `/companies`, `/deals`, `/tickets`                                             | GET, POST          |
| `/contacts/{contactId}`, `/companies/{companyId}`, `/deals/{dealId}`, `/tickets/{ticketId}` | GET, PATCH, DELETE |

### Routers API

| Route                 | HTTP |
| --------------------- | ---- |
| `/routers`            | GET  |
| `/routers/{routerId}` | GET  |

### Dispatch

| Route              | HTTP |
| ------------------ | ---- |
| `/dispatch/router` | POST |

## Object types

The object type is inferred from the path segment:

| Path segment | Object type | Default response fields                                                                     |
| ------------ | ----------- | ------------------------------------------------------------------------------------------- |
| `/contacts`  | contacts    | `id`, `first_name`, `last_name`, `email`, `contact_owner`, `external_id`, `archived`        |
| `/companies` | companies   | `id`, `company_name`, `description`, `industry`, `company_owner`, `external_id`, `archived` |
| `/deals`     | deals       | `id`, `deal_name`, `pipeline`, `deal_stage`, `deal_owner`, `external_id`, `archived`        |
| `/tickets`   | tickets     | `id`, `ticket_name`, `pipeline`, `ticket_owner`, `external_id`, `archived`                  |

Every response also includes `created_at` and `updated_at` (ISO 8601 UTC). When an object is archived, `archived_by` is added automatically.

## Response format

All object endpoints return a **flat** JSON object. Custom field values appear at the top level next to `id` and `external_id` (there is no nested `fields` object).

### Archived objects

| State    | `archived`           | `archived_by`                      |
| -------- | -------------------- | ---------------------------------- |
| Active   | `false`              | omitted                            |
| Archived | ISO timestamp string | user or API client id who archived |

`DELETE` archives the object; the record is retained with `archived` set to a timestamp.

### Default properties

When the `properties` query parameter is omitted, each type returns its default field set from the table above, plus `created_at` and `updated_at`. Use `?properties=phone` to request **additional** fields beyond the defaults (not a replacement list). Invalid or disallowed property names return `400`.

Supported on `GET` list, `GET` by id, and `PATCH` update responses.

### List pagination

`GET` on a collection returns:

```json theme={null}
{
  "data": [ /* objects */ ],
  "pagination": { "limit": 10, "offset": 0, "count": 1 }
}
```

Query parameters: `limit` (default 10, max 100), `offset` (default 0), `archived=true` to include archived rows.

## Write requests

`POST` (create) and `PATCH` (update) use the same JSON shape: a top-level `fields` object whose keys are field **internal names**.

```json theme={null}
{
  "fields": {
    "external_id": "ext-001",
    "first_name": "Ada",
    "last_name": "Lovelace",
    "email": "ada@example.com"
  }
}
```

On **create**, send the fields you want to set. On **update**, send only the fields you want to change.

`POST` responds with **201** and the created object.

### System-managed fields

These fields **cannot** be sent in `body.fields`. Attempts return **400** (for example `Field cannot be set: record_id`):

| Field                      | When set automatically                                                    |
| -------------------------- | ------------------------------------------------------------------------- |
| `record_id`                | Database on create — not set by the Objects API                           |
| `create_date`              | POST create                                                               |
| `last_modified_date`       | Every POST create and PATCH update                                        |
| `*_owner_assigned_date`    | When an owner field is included in the request                            |
| `created_at`, `updated_at` | Object-table timestamps; always in responses, never writable via `fields` |

These fields remain readable via `?properties=` on GET and PATCH responses.

### Field validation

Validation is **atomic**: if any field in `body.fields` fails, the entire create or update is rejected with **400** and nothing is written.

| `field_type_id` | Accepted input                                       | Stored value                                                               |
| --------------- | ---------------------------------------------------- | -------------------------------------------------------------------------- |
| `string`        | String (or coercible)                                | String (`email` lowercased; `website_url` stored as canonical host/domain) |
| `number`        | Finite number                                        | String                                                                     |
| `bool`          | `true`, `false`, `"true"`, `"false"`                 | `"true"` or `"false"`                                                      |
| `datetime`      | ISO 8601, RFC 2822, or numeric timestamp             | ISO 8601 UTC string                                                        |
| `enumeration`   | `internal_name` or display `name` (case-insensitive) | `internal_name` from DB field values                                       |
| `user`          | User UUID or email                                   | Email string                                                               |

Enumeration allowed values come from the database (`field.values` for that field), including options added in the app — not a fixed list in code.

### Required fields (POST create only)

| Object type | Requirement                                        |
| ----------- | -------------------------------------------------- |
| `contacts`  | At least one of `email`, `first_name`, `last_name` |
| `companies` | At least one of `company_name`, `website_url`      |
| `deals`     | `deal_name`, `deal_stage`, `pipeline`              |
| `tickets`   | `ticket_name`, `ticket_stage`, `pipeline`          |

`PATCH` does not require these fields, but every key in `fields` must exist on the object type and pass type validation.

### Duplicate prevention (POST / PATCH)

POST and PATCH reject duplicate **active** records (non-archived) within the same account and object type. Conflicts return **409** with `field` indicating which key collided.

| Object type | Unique keys checked (when present in `body.fields` or as `external_id`) |
| ----------- | ----------------------------------------------------------------------- |
| `contacts`  | `email`, `external_id`                                                  |
| `companies` | `website_url`, `external_id`                                            |
| `deals`     | `external_id`                                                           |
| `tickets`   | `external_id`                                                           |

Deals and tickets have no other business unique keys; duplicate `deal_name` / `ticket_name` values are allowed unless `external_id` is provided.

Example responses:

```json theme={null}
{
  "message": "A contact with this email already exists",
  "error": true,
  "field": "email"
}
```

```json theme={null}
{
  "message": "A deal with this external_id already exists",
  "error": true,
  "field": "external_id"
}
```

* Re-sending the same POST body with the same key does not create a second record.
* `website_url` variants such as `https://www.foo.com` and `foo.com` are treated as the same company.
* Archived records are ignored; a new active record may reuse a key from an archived one.
* Concurrent requests with the same key may still race without a database unique constraint (application-level check only).

## Routers

`GET /routers` returns a paginated list of routers for the authenticated account. `GET /routers/{routerId}` returns a single router with flattened settings, users, and teams. See [List routers](/api-reference/routers/list) and [Get router](/api-reference/routers/get).

## Dispatch

`POST /dispatch/router` runs router assignment for an object. The body must include `router_id` and `object_id`. See [Dispatch router](/api-reference/dispatch/router).

## Related guides

<Card title="Create records" icon="database" href="/kb/create-records">
  Product overview for records and fields in Routera.
</Card>
