# Data types

The API uses a few consistent formats. This page specifies them so you can parse and store values correctly.

## Identifiers

Ids are strings with a type prefix, at most 64 characters, made of letters, digits, `_` and `-` (URL-safe). They are case sensitive.

| Prefix | Object | Prefix | Object |
| --- | --- | --- | --- |
| `ws_` | Workspace | `rcp_` | Campaign recipient |
| `key_` | API key | `ct_` | Contact |
| `agent_` | Agent | `dnc_` | Do-not-call entry |
| `pn_` | Phone number | `we_` | Webhook endpoint |
| `call_` | Call | `evt_` | Event |
| `cmp_` | Campaign | `wd_` | Webhook delivery |
| `req_` | Request (the `X-Request-Id` header) | | |

- Treat ids as opaque. Store them as strings (up to 64 characters); do not parse them, derive meaning from them or build them yourself.
- An id that is malformed, has the wrong prefix for the path, belongs to another workspace or to the other mode answers [`resource_not_found`](/errors/resource_not_found): the same answer as an id that never existed.
- Ids are unique within their type. Keep the mode next to stored ids: a test-mode id means nothing to a live key.

## Timestamps

RFC 3339 in UTC with milliseconds, always ending in `Z`:

```text
2026-10-21T09:30:00.000Z
```

Send UTC timestamps in filters such as `created_after`. Durations are plain integers with the unit in the name: `duration_seconds` on a call, `start_ms` and `end_ms` (milliseconds from the start of the call) on transcript turns. Time zones appear as IANA names such as `Asia/Kolkata`: the workspace's `timezone`, and `details.timezone` of [`outside_calling_hours`](/errors/outside_calling_hours), which is the zone the calling window was evaluated in. Timestamps themselves are always UTC.

## Money

Amounts are objects with an integer in the currency's **minor unit** and an ISO 4217 code:

```json
{ "amount": 1250, "currency": "INR" }
```

- For INR the minor unit is the paisa: `1250` is ₹12.50. Never use floating-point numbers for amounts.
- Amounts in responses (a call's `cost`, a campaign's `spend`) are what your workspace is charged, never negative, and always `0` in test mode.
- Amounts you send (a campaign's `budget_cap`) must be at least 1 and in your workspace's currency, today `INR`.
- `currency` is an open value: today always `INR`, matching the workspace `currency`.

A call's `cost` is `null` until it is known and final once the call is `settled`.

## Phone numbers

E.164 strings: a `+`, the country code and the number, digits only, 8 to 15 digits in total:

```text
+12025550100
+12025550150
```

(Both are fictional test numbers. Use fictional numbers in your own tests and fixtures too.)

- An invalid number in a request answers [`invalid_phone_number`](/errors/invalid_phone_number) (400).
- Remember to URL-encode the `+` as `%2B` in query strings (see [Filtering](/fundamentals/filtering)).
- Numbers are returned in full, as stored, to the workspace that owns them.
- The do-not-call check and the contact lookups match numbers ignoring formatting.

## Booleans and null

Booleans are JSON `true` and `false`; in query strings, `true` and `false`. Every documented field is present in every response; a value that is not set or not known yet is `null` (for example `outcome` before a call ends).

## Metadata

Calls accept `metadata`, your own key-value data stored with the object and returned as sent:

- Up to 16 keys; keys up to 40 characters of letters, digits, `_`, `-` and `.`; values are strings up to 500 characters.
- Never sent to the person called and never used by the agent.
- Do not put secrets or sensitive personal data in it. Use it for your own ids (`{"crm_lead_id": "L-20931"}`), so you can match calls, events and webhook payloads back to your records.

## Variables and custom fields

- **Call variables** (`variables` on `POST /v1/calls`): values the agent can use in the conversation. Up to 50 keys; keys are lowercase letters, digits and `_`, starting with a letter, up to 64 characters; values are strings up to 500 characters.
- **Custom fields** on contacts (`custom_fields`): values for fields defined in the console. An unknown field name answers [`custom_field_not_found`](/errors/custom_field_not_found). Values are strings up to 500 characters; on update, `null` clears a field and omitted fields are left unchanged.

## Enums

Enum values are lowercase `snake_case` strings: call `status`, `outcome` and `end_reason`, campaign and recipient `status`, artifact `state`, readiness reason `code`, delivery `status` and so on.

**Enums are open**: new values can be added at any time without a new API version. Your code must:

- accept values it does not know, instead of failing to parse the response;
- treat an unknown value as "other", never as success (an unknown call `outcome` is not `completed`);
- log unknown values so you notice them and can update your handling.

Several enums include `unknown` today, meaning RelationsAI could not determine the value. It is never a success.
