Skip to content

Create records through the API

Create a record for an object through the Henceforth data API and confirm it was written.

  • The target object and its fields are already applied to the database. See From metadata to database.
  • You know the object’s module key and object key (for example, core and Account).

All data API routes are derived from the module key and object key:

Method Path Operation
POST /v1/data/{moduleKey}/{objectKey} Create a record
GET /v1/data/{moduleKey}/{objectKey}/{id} Read a record by ID
PATCH /v1/data/{moduleKey}/{objectKey}/{id} Update a record (partial)
DELETE /v1/data/{moduleKey}/{objectKey}/{id} Delete a record
PUT /v1/data/{moduleKey}/{objectKey}/{fieldKey} Upsert on a unique field
POST /v1/data/query Execute a HOQS query

POST /v1/data/core/Account

Request body — every field you set must be defined on the object, and system fields (id, createdAt, updatedAt, objectType) must be omitted:

{
"name": "Acme Corp",
"industry": "Technology",
"active": true
}

A successful create returns 201 Created with the full record, including the system fields the runtime generated:

{
"id": "019502a4-b4e7-7f3a-8c1e-0a1b2c3d4e5f",
"objectType": "core.Account",
"createdAt": "2024-03-15T10:30:00.000Z",
"updatedAt": "2024-03-15T10:30:00.000Z",
"name": "Acme Corp",
"industry": "Technology",
"active": true
}

The response also includes a Location header with the URL of the created record.

3. For inherited objects, expect writes across the inheritance chain

Section titled “3. For inherited objects, expect writes across the inheritance chain”

If the object extends another object, the runtime inserts into every table in the chain, base to leaf, sharing one UUID. Creating an Individual record inserts into hf.object, then core.party, then core.individual. Abstract objects cannot be created directly — the API returns ABSTRACT_OBJECT (422) if you try. See Objects, records, and fields for the non-inherited case and why the record is read back through the view before it’s returned.

GET the Location header URL (or GET /v1/data/core/Account/{id} using the id from the response) and confirm the record comes back with the values you sent. See Read, update, and delete records through the API.

On every create and update, the runtime validates the request body against the compiled metadata:

  1. Unknown fields are rejected – UNKNOWN_FIELD (422) for any field name not defined in the metadata.
  2. System fields (id, createdAt, updatedAt) are read-only and rejected if present.
  3. Formula fields are read-only and rejected if present.
  4. Rollup fields are read-only and rejected if present.
  5. Type checking – each field value must match the expected JSON type for its scalar type (strings for text, booleans for bool, integers for int, and so on).
  6. Required fields must be present and non-null on create. On update, required fields cannot be set to null.
  7. Lookup values must be valid UUID strings referencing a record that exists in hf.object. For multi-target lookups, the referenced record’s object_type must be in the allowed set.
  8. Abstract objects cannot be instantiated.
  9. Custom rules – object-level rules: block expressions (declared in *_object.json5) are evaluated against the record. A rule that fails produces a RULE_FAILED field error, attributed to a pinned field, a single referenced field, or the whole record when the expression spans more than one field.

Every error response is an array of entries, each naming the subsystem (domain) and stable code that produced it:

{
"errors": [
{
"domain": "data",
"scope": "field",
"target": "email",
"code": "REQUIRED",
"message": "Field 'email' is required"
}
]
}

A request that fails several checks at once (e.g. two missing required fields) carries one entry per failure in the same array.

For the full list of codes this API can return, their HTTP statuses, and what each one means, see the generated error code reference – it is derived directly from the server’s own error types, so it never drifts from what the server actually sends.