Update Entities
How to update entities with deep merge semantics, relationship upserts, and CAS concurrency control.
Basic Update
PUT /entities/{id}
Content-Type: application/json
Authorization: Bearer <token>
{
"expect_tip": "bafyreig...",
"properties": {
"description": "Call me Ishmael. Some years ago—never mind how long precisely..."
}
}The expect_tip field is required and must match the entity's current tip CID. If another update occurred since you last read the entity, you'll get a 409 Conflict.
Properties: Deep Merge
Properties use deep merge semantics. Nested objects merge recursively rather than being replaced.
Example: Nested Merge
// Existing properties:
{ "metadata": { "author": "Melville", "year": 1851 } }
// Update request:
{
"expect_tip": "bafyreig...",
"properties": {
"metadata": { "genre": "Novel" }
}
}
// Result:
{ "metadata": { "author": "Melville", "year": 1851, "genre": "Novel" } }Merge Rules
| Source Type | Behavior |
|---|---|
| Plain object | Merges recursively with target |
| Array | Replaces target array (no array merging) |
| Primitive (string, number, boolean, null) | Replaces target value |
undefined | Skipped (keeps target value) |
Arrays Replace, Not Merge
Arrays have ambiguous merge semantics (append? replace items? dedupe?), so they always replace:
// Existing: { "tags": ["whale", "sea"] }
// Update: { "properties": { "tags": ["classic"] } }
// Result: { "tags": ["classic"] }Removing Properties
Use properties_remove to delete specific keys at any nesting depth.
Remove Top-Level Keys
{
"expect_tip": "bafyreig...",
"properties_remove": ["deprecated_field", "old_field"]
}Remove Nested Keys
Use a nested object structure to target deep keys:
{
"expect_tip": "bafyreig...",
"properties_remove": {
"settings": {
"notifications": ["email", "sms"]
}
}
}
// Before: { "settings": { "notifications": { "email": true, "sms": true, "push": true } } }
// After: { "settings": { "notifications": { "push": true } } }Dot Notation Not Supported
Important: You cannot use dot notation like ["settings.notifications.email"]. Use the nested object structure instead.
Add and Remove in Same Request
You can add and remove properties in the same update. Removals are applied after merges, so removals win on conflict:
{
"expect_tip": "bafyreig...",
"properties": { "new_field": "value" },
"properties_remove": ["old_field"]
}Relationships: Upsert Semantics
Relationships use upsert semantics. Adding a relationship that already exists (same predicate + peer) merges its properties rather than creating a duplicate.
Adding Relationships
{
"expect_tip": "bafyreig...",
"relationships_add": [
{
"predicate": "references",
"peer": "01KFNR0Z394A878Y5AQ63MQEM2",
"peer_type": "file",
"peer_label": "moby-dick.txt"
}
]
}Upsert: Updating Existing Relationships
If a relationship already exists, properties are deep-merged:
// Existing relationship:
{ "predicate": "editor", "peer": "01JUSER...", "properties": { "since": "2024-01" } }
// Update request:
{
"relationships_add": [{
"predicate": "editor",
"peer": "01JUSER...",
"properties": { "expires_at": "2025-12-31" }
}]
}
// Result:
{ "predicate": "editor", "peer": "01JUSER...", "properties": { "since": "2024-01", "expires_at": "2025-12-31" } }Updating peer_label and peer_type
When upserting, you can also update peer_label and peer_type:
{
"relationships_add": [{
"predicate": "author",
"peer": "01JUSER...",
"peer_label": "Herman Melville" // Updates the stored label
}]
}Removing Relationship Properties
Use properties_remove within a relationship add to remove specific properties:
{
"relationships_add": [{
"predicate": "editor",
"peer": "01JUSER...",
"properties_remove": ["temporary_flag"]
}]
}Removing Relationships
Remove Specific Relationship
Specify both predicate and peer:
{
"expect_tip": "bafyreig...",
"relationships_remove": [
{ "predicate": "references", "peer": "01KFNR0Z394A878Y5AQ63MQEM2" }
]
}Remove All Relationships with Predicate
Omit peer to remove all relationships with that predicate:
{
"expect_tip": "bafyreig...",
"relationships_remove": [
{ "predicate": "references" }
]
}This removes every references relationship, regardless of peer.
Order of Operations
Relationship changes are applied in this order:
- Removals first — specified relationships are removed
- Additions second — new/updated relationships are added
This means you can remove and re-add a relationship with different properties in one request:
{
"relationships_remove": [{ "predicate": "role", "peer": "01JUSER..." }],
"relationships_add": [{ "predicate": "role", "peer": "01JUSER...", "properties": { "level": "admin" } }]
}Request Fields Summary
| Field | Type | Description |
|---|---|---|
expect_tip | string | Required. Current tip CID for CAS guard |
properties | object | Properties to deep merge |
properties_remove | string[] or object | Properties to remove (nested structure supported) |
relationships_add | array | Relationships to add/upsert |
relationships_remove | array | Relationships to remove |
note | string | Version note for this update |
Conflict Resolution
When you receive a 409 Conflict:
{
"error": "CAS conflict",
"message": "Expected tip bafyreig... but found bafyreih...",
"status": 409
}Recovery pattern:
- Fetch the entity again:
GET /entities/{id} - Get the new
cidfrom the response - Reapply your changes to the fresh data
- Retry the update with the new CID as
expect_tip
For high-concurrency scenarios, use GET /entities/{id}/tip for a lightweight CID-only fetch.
Collection Membership Validation
When adding a collection relationship, the API validates that you have permission to add entities to that collection. You need {type}:create permission on the target collection.
{
"expect_tip": "bafyreig...",
"relationships_add": [
{ "predicate": "collection", "peer": "01KCOL...", "peer_type": "collection" }
]
}If you don't have permission, you'll receive a 403 Forbidden error.