POST /api/zsc/set
Create or replace a secret. The plaintext value is encrypted in
memory (AES-256-GCM via zeqEncrypt), then upserted into
zsc_secrets. If a row with name already exists, value_enc and
value_iv are replaced and updated_at is touched — every call
rotates the IV.
Auth
Admin cookie (zeq_admin) required.
Request
curl -H "Cookie: zeq_admin=$ADMIN_JWT" \
-H "Content-Type: application/json" \
-X POST https://YOUR-FRAMEWORK/api/zsc/set \
-d '{
"name": "STRIPE_SECRET_KEY",
"value": "sk_live_...",
"purposeTag": "payments",
"boundZid": "ZEQ-FOUNDATION",
"permissions": ["ZEQ07111111111"],
"expiresZeqond": 2289676900
}'
Body fields
| Field | Required | Type | Notes |
|---|---|---|---|
name | yes | string | Vault key. Validate against your naming convention before sending. |
value | yes | string | Plaintext. Up to 65,536 bytes. The server never logs this. |
purposeTag | no | string | null | Free-form analytics label. |
boundZid | no | string | Owning ZID. Default: "ZEQ-SYS". |
permissions | no | string[] | Additional ZIDs allowed to read. Filtered to strings server-side. |
expiresZeqond | no | bigint | Auto-rotation target. If absent, the secret will be picked up by the rotation daemon at the next default cadence (~18.6 h). |
Response · 200 OK
{ "ok": true, "name": "STRIPE_SECRET_KEY" }
The response is intentionally terse — the plaintext is never echoed
back, not even as a confirmation. If you need to verify the write,
follow up with GET /api/zsc/info/:name.
Errors
| Status | error | Cause |
|---|---|---|
400 | name required | Empty/missing name. |
400 | value must be a string | Wrong type. |
400 | value cannot be empty | Zero-length string. Use DELETE instead. |
400 | value too large (>64KB) | Hit the 65,536-byte ceiling. |
401 | unauthorized | Admin cookie missing/invalid. |
500 | INTERNAL_ERROR | DB unreachable or encryption error. |
Audit row
A successful set emits one audit_log row:
transition_type = "secret_set"
actor_zid = <the admin's ZID from the JWT>
payload_json = { name, purpose_tag, bound_zid, permissions, expires_zeqond, purpose: "created" | "updated" }
proof_digest = SHA-256(name | actor_zid | transition_id | "set")
If the row already existed, payload_json.purpose is "updated".
First-time write is "created". See ZSC Audit Trail.
Related
POST /api/zsc/rotate/:name— same encrypted value, fresh IV (a true rotation without changing plaintext)POST /api/zsc/grant/:name— narrow permission updatepulse > context set— CLI equivalent