Creating & Updating Resources
This page describes the write model for the REST API: how creates, partial updates, full replacements, field clearing, and concurrency control behave. It is the contract every write endpoint follows.
:::note Target contract
This describes the intended write contract for /api/v2. Most resources already follow it;
a few older endpoints are still being aligned. When an endpoint's behaviour differs from what is
described here, the behaviour described here is the direction of travel β file it as a bug.
:::
Creating resources (POST)β
Create a resource by POSTing to its collection. On success the API returns 201 Created with
the full created resource in the body:
curl -X POST "https://api.tourfold.com/api/v2/areas" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Vienna North",
"color": "#3366FF",
"description": "Alpine Facility Services area north of the Danube"
}'
{
"id": "a1b2c3d4-e5f6-4a1b-8c9d-123456789abc",
"name": "Vienna North",
"color": "#3366FF",
"description": "Alpine Facility Services area north of the Danube",
"lock_version": 0
}
Every 201 Created response also includes an absolute Location header containing the canonical
URL of the new resource:
Location: https://api.tourfold.com/api/v2/areas/a1b2c3d4-e5f6-4a1b-8c9d-123456789abc
For a batch create that produces several resources, Location identifies the collection because
there is no single canonical resource URL.
Partial updates (PATCH)β
Updates are PATCH with partial (merge) semantics β you send only the fields you want to
change. This follows RFC 7386 JSON Merge Patch:
- A field omitted from the request body is left unchanged.
- For clearable optional fields, an explicit
nullclears the stored value (see below). - Validation and uniqueness checks run only for the fields you send.
Send Content-Type: application/merge-patch+json. The same body is also accepted as
application/json (an alias with identical omit/null semantics) so generic HTTP clients
keep working.
# Rename the area, leave color and description untouched
curl -X PATCH "https://api.tourfold.com/api/v2/areas/a1b2c3d4-e5f6-4a1b-8c9d-123456789abc" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/merge-patch+json" \
-d '{ "name": "Vienna North & Central" }'
Clearing an optional field (null)β
For fields that are optional and clearable, send an explicit null to remove the stored
value. Omitting the field leaves it unchanged; only an explicit null clears it.
# Clear the description, keep everything else
curl -X PATCH "https://api.tourfold.com/api/v2/areas/a1b2c3d4-e5f6-4a1b-8c9d-123456789abc" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/merge-patch+json" \
-d '{ "description": null }'
Fields that are required cannot be cleared this way. Sending null for a required field (or
for a field that is required for a dependent operation, such as an invoice-relevant billing field)
returns 422 Unprocessable Entity with a problem+json body, rather than silently ignoring it.
Clearing takes an explicit null β an empty string does not clear a field and is not accepted.
{"description": ""} returns 422 with field-required for description; use
{"description": null} to clear it, or omit the key to leave it unchanged. The same applies on a
create: omit the key rather than sending "".
Full replacement (PUT)β
PUT is used only where whole-set, declarative replacement is the contract β not as a general
update verb. Where a PUT endpoint exists, it replaces the entire target set: the body you send
becomes the new state, and anything absent from the body is removed. An empty array clears the set.
# Replace a user's skill set entirely β sending [] removes all skills
curl -X PUT "https://api.tourfold.com/api/v2/users/{user_id}/skills" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"skill_ids": [
"00000000-0000-4000-8000-000000005101",
"00000000-0000-4000-8000-000000005102"
]
}'
To clear Jonas's whole skill set, replace it with an empty set:
curl -X PUT "https://api.tourfold.com/api/v2/users/{user_id}/skills" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "skill_ids": [] }'
If you want to change one attribute of a resource, use PATCH on the resource. Reach for PUT
only when the endpoint is explicitly a set-replacement (e.g. tags, skills) or a declarative schema
replacement.
Immutable fieldsβ
Some fields are fixed at creation and cannot change on update (for example an SMS gateway's
type). Sending a value that contradicts the stored one is rejected with 422, not silently
ignored, so you never believe a change took effect when it did not.
Optimistic concurrency (lock_version)β
Most writable resources expose a lock_version integer that increments on each successful write.
To guard against lost updates when several clients edit the same resource, include the
lock_version you last read in your write body:
curl -X PATCH "https://api.tourfold.com/api/v2/areas/a1b2c3d4-e5f6-4a1b-8c9d-123456789abc" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "name": "Vienna North & Central", "lock_version": 3 }'
- If the supplied
lock_versionmatches the stored version, the write proceeds and the version increments. - If it does not match (someone else wrote in the meantime), the API returns
409 Conflictwith problem typeresource-conflict. Re-read the resource, reconcile, and retry. lock_versionis optional: omit it (or sendnull) to skip the check and perform a last-write-wins update.- Deletes never take a
lock_version.DELETErequests carry no body; a delete always applies to the current stored version unconditionally. - Exception β per-tenant settings singletons: a few small workspace-configuration endpoints
(
/document-management/settings,/mcp/settings,/ai-assistant/settings, and/geoservices/settings) have nolock_versionand always apply last-write-wins. Each is a single row per workspace edited by one administrator through a purpose-built toggle, so there is no concurrent lost-update scenario to guard./tenantand/billing/profileare singletons too but do exposelock_version, because they are compound records edited by multiple admins.
Success status and response shapeβ
Create and update endpoints return the full resource after the write, so you always see the
resulting state (including the new lock_version) without a follow-up GET. A synchronous
operation that returns a representation uses 200 OK (or 201 Created for a create).
An operation that completes successfully and intentionally has no response representation
returns 204 No Content with an empty body. This is the usual contract for deletes and bodyless
actions such as unassign, remove, or disable. A delete that deliberately returns the deleted
resource or an updated aggregate instead uses 200 OK; 204 is not applied mechanically to every
DELETE.
Work accepted for asynchronous processing uses 202 Accepted. Incoming webhook endpoints follow
the sender's acknowledgement contractβtypically an explicit 200 or 202βrather than being
changed to 204 solely for consistency.
Responses omit fields that have no value β so a field you just cleared with null comes back
absent, not as "field": null. Treat a missing key as "not set". See
Basics β Optional fields and null for the full convention,
and Errors for the problem+json error format.
Field type mismatchesβ
A request field whose value has the wrong JSON type or shape β a string where a number is
expected, 1.5 for an integer field, an object where an array is expected β is rejected with
422 Unprocessable Content, problem type field-type-invalid, and an errors[].pointer
identifying the offending field (e.g. #/lock_version, #/tag_ids/0). This is the same
errors[] shape as every other field validation, so you can bind the message to the input
without special-casing. A body that is not valid JSON at all (a syntax error, a truncated
payload) is a 400 Bad Request instead.