Skip to main content
Tally evolves a subject’s identity state through a deliberate two-step flow: first you propose a set of changes as an RFC 6902 JSON Patch, then you apply that proposal to atomically produce a new immutable snapshot. Separating proposal from application lets you inspect, audit, or even discard a pending change before it becomes permanent — and gives the platform the information it needs to guarantee that no two concurrent updates silently overwrite each other.

Propose an update

POST /v1/tenants/:tenant_id/entity-state-updates Submits a patch proposal against a specific snapshot of a subject. The server validates the patch operations and records the proposal with status proposed. No data is changed yet. Requires at least the tenant_proposer role in the target tenant, and your tenant must be the subject owner.

Path parameters

tenant_id
string
required
The ID of the tenant that owns the subject.

Body parameters

subject_id
string
required
The identifier of the subject to update.
subject_type
string
required
The kind of subject. Must be entity or individual.
base_snapshot_id
string (UUID)
required
The snapshot_id of the current snapshot you are patching from. This acts as an optimistic lock — if another update has been applied since you read this snapshot, the subsequent /apply call will return 409 Conflict.
base_snapshot_version
integer
required
The version number of the base snapshot. Must be an integer ≥ 1.
patch
array
required
An array of RFC 6902 patch operations to apply. Each operation is an object with the following fields:
request_id
string
An optional client-supplied idempotency key. Submitting the same request_id twice returns the original update_id without creating a duplicate record.
created_by
string
An optional free-form string identifying who initiated the proposal, for audit purposes.

Response

201 Created
{
  "update_id": "a1b2c3d4-0000-0000-0000-111122223333"
}
Save the returned update_id — you’ll need it to apply the update.

Example request

curl -X POST https://api.tally.so/v1/tenants/acme-corp/entity-state-updates \
  -H "Authorization: Bearer $TALLY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "subject_id": "ent_7f3a1b2c",
    "subject_type": "entity",
    "base_snapshot_id": "e9d8c7b6-1234-5678-abcd-000011112222",
    "base_snapshot_version": 4,
    "patch": [
      {
        "op": "replace",
        "path": "/attributes/entity_status",
        "value": "inactive"
      }
    ],
    "created_by": "ops-team@acme.com"
  }'

Apply an update

POST /v1/tenants/:tenant_id/entity-state-updates/:update_id/apply Applies a previously proposed update. The server re-validates that the base snapshot is still the latest for this subject, executes the patch operations, validates the resulting envelope, and writes a new immutable snapshot. This entire operation runs inside a serializable database transaction. Requires at least the tenant_editor role in the target tenant, and your tenant must be the subject owner.

Path parameters

tenant_id
string
required
The ID of the tenant that owns the subject.
update_id
string (UUID)
required
The update_id returned by the propose step.

Request body

No request body is needed.

Response

201 Created — the full snapshot envelope for the newly written snapshot.

Error responses

StatusCodeMeaning
404not_foundNo update exists with the given update_id.
409conflictThe update has already been applied, is not in proposed status, or the base snapshot is no longer the latest for this subject (stale base — re-propose).

How the new snapshot_id is derived

The new snapshot_id is deterministic — given the same inputs you will always get the same output:
snapshot_id = uuidv5(
  "{base_snapshot_id}:{canonical_json_patch}",
  SNAPSHOT_ID_NAMESPACE
)
The patch is serialized to canonical JSON (keys sorted, no extra whitespace) before hashing. This means you can compute the expected snapshot_id client-side before the apply call completes, and it provides a natural deduplication guarantee: re-applying the exact same patch to the same base snapshot is idempotent at the storage level.

Full curl sequence

curl -X POST https://api.tally.so/v1/tenants/acme-corp/entity-state-updates \
  -H "Authorization: Bearer $TALLY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "subject_id": "ent_7f3a1b2c",
    "subject_type": "entity",
    "base_snapshot_id": "e9d8c7b6-1234-5678-abcd-000011112222",
    "base_snapshot_version": 4,
    "patch": [
      {
        "op": "replace",
        "path": "/attributes/entity_status",
        "value": "inactive"
      }
    ]
  }'
# → { "update_id": "a1b2c3d4-0000-0000-0000-111122223333" }
Optimistic concurrency lock. The base_snapshot_id you supply at propose time must still be the current latest snapshot when you call /apply. If another update is applied to this subject between your propose and apply calls, the apply will return 409 Conflict with the message "Base snapshot is stale." You’ll need to re-read the subject’s latest snapshot and re-propose your changes on top of it. This prevents silent last-write-wins overwriting in multi-writer environments.