*Overview › Core concepts*

# Records and ids

> Every record has an opaque id that Filmo assigns. Keep your own ids in a mapping table on your side, and use a reference field only where the API has one.

## Ids are opaque

Identifiers are opaque strings. Never assume their format, length or ordering, and never build one. Pagination cursors are opaque in the same way. Human numbers such as `orderNumber` or `contactNumber` are for people: use `id` to address a record.

## Where your id can live

Only some resources have a writable field for a reference from your system. The table is generated from the spec.

| Field | Yes or no |
| --- | --- |
| `ContactDto.contactNumber` | No |
| `ContactDetailDto.contactNumber` | No |
| `ContactCreateBody.contactNumber` | Yes |
| `ContactUpdateBody.contactNumber` | Yes |
| `OrderDto.externalReference` | No |
| `OrderDetailDto.externalReference` | No |
| `ProjectDto.externalReference` | No |
| `ProjectDetailDto.externalReference` | No |
| `ProjectCreateBody.externalReference` | Yes |
| `StaffMemberDto.externalRef` | No |
| `StaffUpdateBody.externalRef` | Yes |
| `ProjectUpdateBody.externalReference` | Yes |
| `upsertExternalInvoice.originSystem` | Yes |
| `upsertExternalInvoice.originExternalId` | Yes |

| Resource | What exists today |
| --- | --- |
| Projects | `externalReference` on create and `externalReference` on update, up to 200 characters. |
| Staff | `externalRef`, set on update only; the create body has none. `null` clears it. |
| Invoices issued elsewhere | The pair `originSystem` and `originExternalId` in the path of `PUT /api/v1/invoices/external/{originSystem}/{originExternalId}` is the key of the mirror: repeating it updates the same mirror. |
| Orders | `externalReference` is returned, but the create and update bodies have no such field: you can read it but not write it through the API yet. |
| Contacts | No external reference field. `contactNumber` is a free reference, up to 50 characters, that you can write on create and on update. It is not unique: Filmo does not stop two contacts from sharing a number, so check what a lookup returns. |

## Looking up contacts

[`POST /api/v1/contacts/lookup`](https://docs.filmo.io/rest-api/endpoint-reference/contacts/look-up-contacts-by-external-reference) takes up to 200 references and compares each one, exactly and without any normalisation, with the `contactNumber` of the contacts, and with nothing else. It returns two lists: `notFound`, the references that match no contact, and `contacts`, the contacts that matched, each with `id`, `name` and `contactNumber`. It writes nothing, even though it is a POST.

The response does not pair a reference with a contact, and the order of `contacts` is not guaranteed. Pair them yourself by comparing each contact's `contactNumber` with your reference. Because `contactNumber` is not unique, several contacts may come back for one reference: decide what to do in that case instead of taking the first one. Use `notFound` to learn what to create, and keep the Filmo `id` in your own table once you have it.

## Keep your own mapping table

Store your external id and the Filmo `id` side by side in your own database. An `Idempotency-Key` only deduplicates retries for 24 hours; it does not replace that table ([Idempotency](https://docs.filmo.io/rest-api/guides/idempotency)). With the table you can update the right record, detect duplicates and decide what to delete. For the pattern in a CRM, see the [contact sync tutorial](https://docs.filmo.io/rest-api/tutorials/crm-contact-sync).

## Custom fields

The values of custom fields are read in the `customFields` map of a record, keyed by the identifier of the field definition. The definitions come from [`GET /api/v1/custom-fields`](https://docs.filmo.io/rest-api/endpoint-reference/custom-fields/list-custom-field-definitions), with the entity each applies to (`applyAt`), the type and the values a dropdown accepts. Values travel inside each resource; the definitions endpoint does not return them. v1 has no operation that writes a custom field: the create and update bodies have no `customFields` property, so set them in the app.

## Tags and notes

- Tags: [`GET /api/v1/tags`](https://docs.filmo.io/rest-api/endpoint-reference/tags/list-tags) returns the vocabulary with how many entities use each. [`POST /api/v1/tags/{tagId}/assignments`](https://docs.filmo.io/rest-api/endpoint-reference/tags/assign-a-tag-to-an-entity) attaches an existing tag to an entity. [`DELETE /api/v1/tags/{tagId}/assignments`](https://docs.filmo.io/rest-api/endpoint-reference/tags/remove-a-tag-from-an-entity) removes the link and does not delete the tag.
- Notes: [`POST /api/v1/notes`](https://docs.filmo.io/rest-api/endpoint-reference/notes/write-a-note-on-an-entity) writes a note on an entity, and [`GET /api/v1/notes`](https://docs.filmo.io/rest-api/endpoint-reference/notes/list-the-notes-of-an-entity) reads the notes of one entity, never all of them. Who wrote a note is not returned. Mentioning users notifies them in Filmo.

---

Canonical URL: https://docs.filmo.io/concepts/records-and-ids  
Last updated: 2026-10-09