# Data formats

How the API writes dates, times, durations, money, ids, enums and empty values, and the forms it accepts back.

This page lists the formats the API uses on the wire. Send values back in the same forms: the API checks them strictly and does not guess.

A worklog row from `GET /worklogs` shows most of them at once:

```jsonc
{
  "id": 9021,
  "date": "2026-06-15",
  "minutes": 90,
  "description": "Sprint review with Acme",
  "work_item_id": 812,
  "is_overtime": false,
  "created_at": "2026-06-15T07:30:00Z",
  "updated_at": "2026-06-15T07:30:00Z"
  // 15 more fields: project_id, project, budget_id, budget, task_id, task, work_item, user_id, user, is_not_billable, exceeded_budget, reviewed_by, invoice_id, sales_invoice_row_id, links
}
```

## Field names

Every key is `snake_case`.

- A relation usually comes as a foreign key named `{relation}_id`, next to the relation itself: `sprint_id` and `sprint`. Three do not: a Drive item's `drive_folder` uses `folder_id`, a Drive folder's `parent_folder` uses `parent_id`, and a contact's `role` uses `contact_role_id`.
- A counter is an integer named `{relation}_count`, such as `comments_count`.
- Booleans are JSON `true` and `false`. Most are named `is_…`, but not all: `auto_renew` and `active` are booleans too. Go by the type in the schema, not by the name.

## Calendar dates

A calendar date is `YYYY-MM-DD`, such as `"2026-06-15"`. It has no time and no time zone. A worklog's `date` and a work item's `due_date` are calendar dates.

A time of day on its own, as in a user's `working_hours`, is `HH:MM`, such as `"08:30"`, also without a zone.

## Date-times

A date-time is UTC, to the second, with a `Z`: `"2026-06-15T07:30:00Z"`. Responses always use this form.

Send date-times in exactly this form too, in request bodies and in filters. An offset such as `+02:00` or fractions of a second are refused:

| Where        | What you get                                                                                                                                 |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| Request body | `422` [validation-failed](https://developers.plnnrly.com/en/docs/api/v2/guides/errors/#validation-failed), code `date_format`                |
| Filter       | `400` [invalid-query-parameter](https://developers.plnnrly.com/en/docs/api/v2/guides/errors/#invalid-query-parameter), code `invalid_format` |

Convert to UTC before you send. A meeting at 09:30 in Amsterdam on 15 June 2026 is `2026-06-15T07:30:00Z`, because Amsterdam is two hours ahead of UTC in summer.

To show a date-time in a person's own zone, read the `timezone` field on their [user](https://developers.plnnrly.com/en/docs/api/v2/schemas/user/) record, such as `Europe/Amsterdam`. When it is `null`, the organisation's default zone applies.

## Durations

A duration is a whole number of minutes, as an integer. One and a half hours is `90`.

The field name does not always say so. `minutes` and `logged_minutes` do, but a work item's `estimation` and a budget's `budget` are minutes as well. The field description in the schema states the unit.

One exception: on a sales invoice row that bills budget time, `quantity` can hold hours, as a decimal string like any amount. The row's `billable_minutes` holds that time in minutes.

## Money, percentages and coordinates

| Kind                          | Form                                       | Example                  |
| ----------------------------- | ------------------------------------------ | ------------------------ |
| Money                         | A decimal string with two decimals         | `"1250.00"`              |
| Percentage                    | A whole number from 0 to 100               | `21` means 21%, not 0.21 |
| Coordinates in a response     | A decimal string with up to seven decimals | `"52.3675734"`           |
| Coordinates in a request body | A JSON number                              | `52.3675734`             |

Money is a string so that no rounding happens on the way. Send amounts as strings with at most two decimals. There is no currency field.

A sales invoice row, for example:

```jsonc
{
  "quantity": "2.50",
  "unit_price": "95.00",
  "vat_rate": 21,
  "vat_amount": "49.88",
  "total_amount": "237.50",
  "billable_minutes": 150
  // 18 more fields: id, sales_invoice_id, sales_invoice, subscription_item_id, budget_id, exact_item_id, exact_vat_code, exact_cost_center, exact_gl_account, name, description, hourly_rate, is_aggregated_price, sort_order, created_at, updated_at, deleted_at, links
}
```

## Identifiers

Every `id` is an integer, unique among records of the same resource in your organisation: project `12` and work item `12` are different records.

A work item also has an `issue_key`, a string such as `"ACM-12"`. Every work item path accepts either form, so `/work-items/ACM-12` and `/work-items/812` are the same record. Request bodies and filters take the numeric id, such as `work_item_id` on a worklog. The one exception is `after_work_item_id` on the [move action](https://developers.plnnrly.com/en/docs/api/v2/work-items/move/), which takes either. Store the numeric id as your key: an `issue_key` can be `null`.

## Absent, null and empty

These three mean different things:

| What you see        | Meaning                                                                                                                                                                         |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The field is absent | You did not ask for it. Included relations, on-demand fields and detail-only fields appear only when you request them.                                                          |
| `null`              | There is no value, such as `end_date` on a project with no planned end. A few on-demand fields are also `null` when your access does not cover them; their description says so. |
| `[]`                | The list exists and is empty.                                                                                                                                                   |

See [Views and includes](https://developers.plnnrly.com/en/docs/api/v2/guides/views-and-includes/) for how to request what is absent.

## Enum values

Enum values are sent and returned exactly as the schema lists them. They are not always `snake_case`: a project's status can be `on-hold`, and `on_hold` is refused.

Most enums can gain values in a later release. The OpenAPI document marks each of them with `x-extensible-enum: true`, in 200 places. Treat a value you do not recognise as "unknown, show it as is", never as an error.

A value that is not in the list is `422` with code `in` in a request body, and `400` with code `invalid_format` in a filter.

## Rich text

| Field                                                                       | Format                                                        |
| --------------------------------------------------------------------------- | ------------------------------------------------------------- |
| Work item `description`, comment `body`, note `content`, and similar fields | HTML from the plnnrly editor, sanitised before it is returned |
| Drive document `content`                                                    | GitHub-Flavored Markdown                                      |
| Drive document `content_html`                                               | The same document as sanitised HTML, on request               |

Markdown cannot hold text colours, highlights, alignment or @mentions, so writing a document back as Markdown drops them. Read `content_html` first when a document may contain them. The [drive document](https://developers.plnnrly.com/en/docs/api/v2/drive-documents/get/) operation explains the details.

## Links back to plnnrly

Projects and work items carry a `web_url`: an absolute URL that opens the record in plnnrly, on your organisation's own address. It is stable, so you can store it and show it as a link. It is read-only.

## Language

Data comes back exactly as people entered it. Nothing is translated.

Problem documents and warnings are always in English, whatever language a user has chosen in plnnrly. Their `type`, `code` and enum values are identifiers: switch on those, not on the English text.

## Related

- [How the data fits together](https://developers.plnnrly.com/en/docs/api/v2/guides/data-model/): which resource holds what.
- [Filtering and sorting](https://developers.plnnrly.com/en/docs/api/v2/guides/filtering-and-sorting/): the same formats in filter values.
- [Errors and retries](https://developers.plnnrly.com/en/docs/api/v2/guides/errors/): what a refused value looks like.
