Write Operations
Idempotency (Write Operations)
All write operations (POST, PUT, DELETE) require an Idempotency-Key header, except media upload (multipart bodies cannot be reliably hashed). This ensures safe retries—if a request is sent twice with the same key and payload, the second request returns the cached response without re-executing.
POST /api/v1/pages
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
Rules
- Max 256 characters
- Keys expire after 24 hours, then can be reused
- Same key + same body → cached response returned (with
X-Idempotency-Replay: trueheader) - Same key + different body →
409 CONFLICT - GET requests do not require idempotency keys
Example Response (Replayed)
HTTP/1.1 201 Created
X-Idempotency-Replay: true
Content-Type: application/json
{
"data": { ... }
}
Dry-Run Validation
All content write endpoints support ?dryRun=true query parameter. This validates the request without persisting data.
Valid Content (POST create):
{
"dryRun": true,
"valid": true,
"errors": [],
"warnings": [],
"wouldCreate": true
}
Valid Content (PUT update):
{
"dryRun": true,
"valid": true,
"errors": [],
"warnings": [],
"wouldUpdate": true
}
Validation Failed (POST create):
{
"dryRun": true,
"valid": false,
"errors": [
{
"path": "fields.title",
"message": "Required field",
"type": "required"
}
],
"warnings": [],
"wouldCreate": false
}
Validation Failed (PUT update):
{
"dryRun": true,
"valid": false,
"errors": [
{
"path": "fields.title",
"message": "Required field",
"type": "required"
}
],
"warnings": [],
"wouldUpdate": false
}
Valid, but rich text would be sanitized: warnings lists each rich-text value that would lose
markup outside the allow-list, and what it would lose. An invalid result
carries them too, which explains a required field that sanitizing would leave empty.
{
"dryRun": true,
"valid": true,
"errors": [],
"warnings": [
{
"field": "body",
"code": "RICH_TEXT_SANITIZED",
"message": "Removed disallowed markup from rich text: img[onerror]",
"removed": ["img[onerror]"]
}
],
"wouldCreate": true
}
Rich Text Is Sanitized on Write
Rich-text (wysiwyg) values are sanitized on the server before they are stored: anything outside
the allow-list is removed and the rest is saved. Content is validated
after it is sanitized, so the one way markup gets a write rejected is a required field that held
nothing else: it is empty once sanitized, and fails as a missing required field. When anything was
removed, the response carries the same warnings array beside data:
{
"data": { "…": "…" },
"warnings": [
{
"field": "sections[2].body",
"code": "RICH_TEXT_SANITIZED",
"message": "Removed disallowed markup from rich text: <script>",
"removed": ["<script>"]
}
]
}
warnings is omitted when nothing was removed.

