Skip to main content

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 null clears 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_version matches 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 Conflict with problem type resource-conflict. Re-read the resource, reconcile, and retry.
  • lock_version is optional: omit it (or send null) to skip the check and perform a last-write-wins update.
  • Deletes never take a lock_version. DELETE requests 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 no lock_version and 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. /tenant and /billing/profile are singletons too but do expose lock_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.