Skip to main content
Two owner settings keep credentials out of your tool definitions and let a receiver check where a request came from. Secrets are values you store once for the workspace, such as the token a tool sends to your own system; Sempera never returns a value. Request signing adds a short-lived signed token, verifiable with public keys, to the requests Sempera sends in your name. Both are workspace settings. Writes need the owner role and the scope shown; a secret or key of another workspace answers 404 exactly like one that never existed. A write from the Sempera App also passes the CSRF check (Errors and limits).

Secrets

GET /v1/secrets takes q (a name search, up to 80 characters), type (all, static, refresh), status (all, active, paused) and sort (created_desc by default, created_asc, updated_desc, updated_asc); any other value is refused with 400 validation_failed. Other routes answer the secret itself. A secret has a name in CAPITALS_WITH_UNDERSCORES, 2 to 80 characters, starting with a letter, unique in the workspace. There are two types.
A Refresh secret is fetched when you create it, on .../refresh, and again, when a tool is about to use it, if its last attempt is older than the interval. The request goes out with a 10 second timeout, and the answer must be a 2xx JSON document. A failed fetch is recorded in refresh.last_error (a sentence that never contains the value or the answer) and keeps the last good value; creating a secret whose first fetch fails still succeeds, and the secret says why it has no value. A refresh needs the deployment’s encryption key, like every stored secret; without it writes answer 503 owner_setup_required. A secret reads id, object: "secret", name, key, type (Static or Refresh), status (Active or Paused), reference ({{secret.NAME}}), id_reference ({{secret.id.<id>}}), has_value, created_by, updated_by, created_at, updated_at and refresh, null for a Static secret, otherwise url (without its query string), method, header_names (never the values), interval_minutes, json_path, last_refreshed_at, last_attempt_at and last_error. PATCH takes name, value and status (active or paused), at least one. A value on a Refresh secret is refused, and a Refresh secret’s settings cannot be edited, only its name and status. .../refresh on a Static secret is 400, and on a paused one 409 conflict.

Using a secret in a tool

A tool names a secret by its name in auth ({"type": "bearer_reference", "secret": "ORDERS_TOKEN"}, or api_key_reference with a header), or by a template in its request URL path or query, a static header or a static body: {{secret.ORDERS_TOKEN}} or {{secret.id.<id>}} (never in the host). A tool your workspace owns resolves them from your workspace’s secrets only. A reference to an unknown secret, a paused secret or one with no value refuses the call before anything is sent, and the value is masked wherever it would appear in a result, including an answer that echoes it (Tools and integrations).

Request signing

When signing is on, the requests Sempera sends to your systems in your workspace’s name carry one more header holding a JWT: an owner-built tool call that reaches your system (a sandbox test or a live conversation), every outbound webhook delivery, every webhook action of an automation trigger (Automations) and a widget’s chat webhooks (Website widgets). It adds to X-Sempera-Signature (Webhooks); it never replaces it. These answer plain objects, not the compatibility envelope. GET /v1/signing answers object: "request_signing", workspace_id, algorithm (RS256), settings, keys, issuer, jwks_url, jwks_path, key_storage_configured and signed_requests (["tool_calls", "webhooks"]). A key reads id, object: "signing_key", kid, algorithm, status (active, previous or revoked), public_key_pem, created_at, retired_at, revoked_at and, in the list, published. A private key is never returned. The header value is the prefix followed by an RS256 token. Its header carries kid; its claims are iss (https://api.sempera.org/v1/workspaces/<workspace_id>), sub (the workspace), aud, iat, nbf, exp, a fresh jti, body_sha256 (the hex SHA-256 of the exact body bytes sent, of the empty string for no body) and your custom claims. To verify, read kid from the token, take that key from the JWKS, check the RS256 signature, iss, aud and exp, and compare body_sha256 with the SHA-256 of the raw body you received. A request without the header should fail that check: if the key cannot be read when a request is sent, the request goes out without the header rather than not at all. Rotating makes the new key the one that signs and keeps the previous key in the JWKS for 24 hours, so tokens already in flight still verify; at most three keys are published, and older ones are revoked. Revoking previous keys removes every earlier key from the JWKS at once, for a suspected compromise, and answers the settings view with revoked, the number withdrawn. A key is 2048-bit RSA and its private half is stored encrypted under the deployment’s key; without that key no signing key can be made. The JWKS needs no credential, answers {"keys": []} for a workspace without keys (or one that does not exist) and may be cached for 5 minutes.

Refusals