Publish the branding draft
POST /v1/branding:publish · permission branding:write
Takes a snapshot of every draft (environment, applications, organizations, texts) and makes it the live version for hosted pages. Before publishing, the effective theme of every scope is checked: required tokens, value ranges and WCAG contrast of each color pair. Any problem rejects the whole publish with 422 branding_validation_failed and an issues[] list naming every token to change (with the measured ratio, the threshold and a nearest passing suggestion for contrast). The last 50 versions are kept. Idempotency-Key is optional.
When the draft has custom CSS, it is also rendered on the hosted screens before anything is published (a render check that can take up to 25 seconds; use a client timeout of at least 30 seconds). CSS that breaks a protected element or adds an accessibility violation that the page does not have without it rejects the publish with 422 branding_validation_failed and a render_violations[] list. If the draft changes while it is being checked, nothing is published and you get 409 conflict; publish again.
Request
This operation takes no path, query or body fields. The environment comes from the credential.
Response
200 OK with the new version's metadata (the snapshot itself is not returned).
{
"version": 4,
"snapshot_sha256": "3f1c…",
"source": "publish",
"rolled_back_from": null,
"actor_type": "api_key",
"actor_id": "key_01jb…",
"published_at": "2026-10-05T09:31:00Z"
}
{
"type": "https://docs.keenauth.com/problems/branding_validation_failed",
"status": 422,
"code": "branding_validation_failed",
"issues": [
{"scope_type": "environment", "code": "brand_contrast_insufficient", "field": "light.color.primary_contrast",
"variant": "light", "pair": "primary_contrast_on_primary", "ratio": 2.1, "threshold": 4.5, "suggestion": "#ffffff"}
]
}
A render check rejection lists every finding: screen (for example password), locale (en, ar or ja), width (viewport width in CSS pixels, 320 or 1280), rule (protected_no_area, protected_not_visible, protected_transparent, protected_covered, protected_contrast, or an axe-core rule id such as color-contrast) and target (the protected element's name or an axe-core CSS selector). CSS values are never echoed. At most 100 findings are returned; render_violations_truncated is true when there were more.
{
"type": "https://docs.keenauth.com/errors/branding_validation_failed",
"status": 422,
"code": "branding_validation_failed",
"render_violations": [
{"screen": "recovery", "locale": "ar", "width": 320, "rule": "protected_covered", "target": "recovery_codes_hint"}
],
"render_violations_truncated": false
}
Errors
| HTTP | code | When |
|---|---|---|
| 403 | plan_feature_unavailable |
The draft has custom CSS or a page template and the plan does not include it (feature names which). |
| 422 | branding_validation_failed |
The draft cannot be published; see issues[]. The draft's custom CSS is sanitized again on publish; if it fails, the violations are in the violations[] extension (see PUT /v1/branding/css). If the custom CSS fails the render check, the findings are in render_violations[] and render_violations_truncated. |
| 409 | conflict |
The draft changed while its custom CSS was being checked; nothing was published. Publish again. |
| 401 | unauthenticated |
The credential is missing, invalid or revoked. |
| 403 | insufficient_permissions |
The credential lacks the required permission. |
| 429 | rate_limited |
The rate limit was exceeded; retry after the Retry-After period. |
| 503 | service_unavailable |
A dependency is temporarily unavailable (including the render check of a draft with custom CSS); retry after the Retry-After period. |
Example
curl -X POST https://api.eu.keenauth.com/v1/branding:publish \
-H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
-H "Idempotency-Key: $(uuidgen)"