Skip to main content
Records are written in the web app, or with an API key that has the Write and publish records scope (records:write). Every write is a revision, attributed to the person or key that made it, so you can see it and undo it in the record’s history. In the web app, editors and every role above can create, edit and delete records; viewers can’t.

Create a record

In the web app, choose New record on a collection’s page, fill in the fields and choose Create, or Create and publish from its menu. Through the API, POST /v1/collections/{collection_id}/records with the field values in data, keyed by field key. With an API key, the collection’s slug works in place of its ID:
The API answers 201 with the record, unpublished, at revision 1; publish it when it’s ready. Each field type takes its own kind of value, never converted from another type ("5" is not an integer):
  • Relation: the ID of a record in the field’s target collection (rec_…).
  • Asset: the ID of an uploaded asset of the project (ast_…), of a type the field accepts. Upload the file first.
  • Rich text: Markdown, or a ProseMirror document ({"type": "doc", "content": [...]}), which can show the project’s images. See Rich text.
  • Multiple: a list of distinct values of the field’s type, such as the tags above.
Content modelling has every type’s value and rules. A required field needs a value: null, "" and [] count as none. Relation and asset IDs are checked when they are newly written: the record must exist in the target collection and the asset must be uploaded and not deleted (invalid_reference). IDs a record already holds are left alone, so a record whose related record was deleted later can still be saved. A value that doesn’t pass is a 422, with a detail for each field, keyed by its field, such as required, unknown_field or taken (another record has this value in a unique field). See Errors and retries.

Update a record

In the web app, open a record, change it and choose Save. Through the API, PATCH /v1/records/{record_id} changes only the fields in data. Each key you send replaces that field’s value, null clears it, and the fields you leave out keep their values:
  • A multiple field’s list is replaced as a whole. To add a tag, send the full list.
  • A required field can’t be cleared (422, required).
  • Published records change at once. An edit to a live record reaches your site straight away; to hold changes back, write them to a draft instead.
  • A pending draft doesn’t block the update. Merging the draft later replaces the fields the draft changes.
  • A deleted record can’t be updated (409, detail code deleted): restore it first.
A renamed field’s old key keeps working in data for six months (Changing a collection).

Sync content without duplicates

To copy content in from another system and run the same job again later, create with upsert_on: the key of a unique field, such as a slug or a SKU, whose value data includes. If a record already has that value, the API updates it as PATCH would and answers 200; otherwise it creates one and answers 201.
The field must hold one value per record and be unique: a text field with the slug format or unique on, or a unique integer field (Formats and unique values). Any other field is a 400 (not_unique), and data without a value for it is a 422. A deleted record doesn’t count: its unique values are free, so the upsert creates a new record. Upserting a record doesn’t publish it, and an updated record keeps its publication.

Concurrent edits

Without a check, the last write wins: an update can overwrite a change someone made after you read the record, though both stay in its history. To write only if nobody changed the record since you read it, send its revision, the ETag of the response you read it from, as If-Match. A stale write is refused with 412 and nothing is written:
With upsert_on, If-Match applies to the update, and there being no record to update is a 412 too. In the SDKs, pass the record you read as ifMatch (if_match): see Concurrent edits. The CLI takes --if-revision. Errors and retries has what to do with a 412, and how to retry any write safely with an Idempotency-Key.

Say why with a change note

Send Nohead-Change-Note with a write to keep a short note in the revision it makes, such as the ticket or the reason for an import. The SDKs take it as changeNote (change_note), and the CLI as --note. Change notes and attribution has the format.

Change many records at once

In the web app, select records on a collection’s page and open the menu that counts them, such as 2 records selected: Publish, Unpublish, Set field… or Delete…. With Deleted chosen in the status filter, the menu has Restore. Records that fail stay selected. Through the API, POST /v1/collections/{collection_id}/record-batches publishes, unpublishes, deletes, restores or updates up to 100 records of a collection. action is publish, unpublish, delete, restore or update; record_ids lists the records; for update, data holds the fields to set on every one of them (null clears one):
Each record is changed as if by its own request: one revision for each change it makes, with its own activity log entry, webhook and search update. Records that fail are reported without stopping the others: one that doesn’t exist, values that don’t pass validation, a plan limit, or, for publish, a live record with a pending draft. The response says what happened to each record, in request order:
changed is false for a record that was already in that state, such as publishing a published record: it gets no revision. The request as a whole is refused, changing nothing, when action is unknown, record_ids isn’t 1 to 100 distinct IDs, or an update has no data (422). Batches take a change note and an Idempotency-Key, but not If-Match.
nohead records bulk also selects the records for you, with --status, --filter, --deleted or --every, and sends them 100 at a time. It prints how many records changed and the error for each one that failed, and exits with 1 if any did.

Delete and restore

Deleting a record keeps its values and history for 30 days, until its purge_after, and then removes it for good. Until then it can be restored. A deleted record is gone from lists and from your site, and its unique values are free for other records. Deleting also cancels its schedule, and a pending draft can’t be merged until the record is restored. In the web app, open the record, choose Delete and confirm with Delete record. To bring it back, choose Deleted in the collection’s status filter, open the record and choose Restore.
That’s DELETE /v1/records/{record_id} and POST /v1/records/{record_id}/restore. Deleting a deleted record, or restoring one that isn’t deleted, changes nothing. A restore is refused with 409 (taken, with that record’s record_id) when another record took one of its unique values meanwhile, and with 402 when the organization’s plan has no room for another record. A restored record comes back as it was, published or not, but its schedule doesn’t come back. To list deleted records, add filter[deleted]=true to a list (Keep a copy in sync).

Import from the command line

The CLI writes records from flags and files. --set key=value sets a text value and --set key:=json any JSON value; --data takes a JSON object and --data-file reads one from a file (- for stdin). They combine, --set winning:
nohead records import <collection> <file> creates a record for each line of a JSON Lines file: either a plain object of field values, or a record as nohead records export writes it. With --publish, exported records that were published are published again. Each line is its own request, so a line that fails is reported with its line number and the rest go on; the command then exits with 1. Each line also gets an idempotency key derived from the file, so running an interrupted import again within 24 hours skips the lines already done.
The import creates new records, with new IDs and a history of their own. Relation and asset IDs are written as they are, so they must exist in the project you import into. To copy a whole project, schema, assets and relations included, use nohead export and nohead import: see the CLI. Every endpoint is in the API reference.