Save Config
Save a multi-field configuration for a team integration (e.g. Keystatic, WordPress).
Sensitive fields (github_token for Keystatic, app_password /
application_password for WordPress) per the
_SENSITIVE_EXTRA_DATA_KEYS map are split out of body.config
via team_integration_service.extract_sensitive_for_storage,
encrypted with AES-256-GCM, and persisted on
IntegrationCredential.access_token_encrypted. The remaining
non-sensitive config writes to extra_data (REPLACE semantics —
POST is the create / full-rewrite path, distinct from PATCH’s
shallow merge). See migration e8b3encsec01.
On row UPDATE, a blank sensitive value preserves the stored
encrypted secret (re-saving config without re-typing the token
is supported). On row CREATE without a sensitive value, the
encrypted column is left NULL; the verify_post_migration
invariant will flag the row if it’s also status='connected'.
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Body
Multi-field config payload.
config accepts Any values (not just str) so nested
structures like Keystatic's content_paths — a {task_type: path} dict — can round-trip through the API and JSONB column
without forcing the frontend to JSON.stringify them. The
pre-2026-06-02 shape was dict[str, str] which (a) made the
frontend serialise nested dicts to JSON strings and then (b) made
the publish-leg resolver crash on those strings with
AttributeError: 'str' object has no attribute 'get' — the
CMT publish loop incident.
The backend resolver (KeystaticAdapter._resolve_content_path)
still defensively handles JSON-stringified shapes for back-compat
with credentials saved before this widening (PR #1835). New saves
go in as native dicts via this widened schema.