REST API
The API is a Cloudflare Worker. All application endpoints are under /v1 and require
authentication. Values are always ciphertext — the API neither encrypts nor decrypts.
Authentication
Send one of:
Authorization: Bearer <jwt>— a Stytch B2B session JWT (web sessions), or a machine (M2M) access token.Authorization: Basic base64(client_id:client_secret)— machine (M2M) client credentials. The API runs the OAuth 2.0 client-credentials exchange server-side and authorizes the caller as a machine client — the same credential used on the hosted metadata plane (mcp.seekrit.dev). It holds no key material and cannot decrypt; use it to bootstrap an admin service token, then switch to that token for anything that touches ciphertext.Authorization: Bearer skt_…— a service token. Its storedroleismember(runtime credential) oradmin(passes admin-gated routes, for headless provisioning).owneris never issued to a token. A service token is never a valid Basic credential — it carries a private key.Authorization: Bearer skc_…— a CLI session, created byseekrit loginand authorized in the dashboard. It authenticates as the user who authorized it, with that member's role in every org they belong to. It carries no key material, so decryption still needs that user's passphrase-unlocked private key.
Errors use { "error": { "code": "...", "message": "..." } } with a matching HTTP status
(400, 401, 403, 404, 409, 429, 500). Organizations you don't belong to return 404.
One code is recoverable rather than final: mfa_required (403) means the caller
must re-verify their second factor and retry (see CLI sign-in below).
Signup (agent self-service)
| Method | Path | Description |
|---|---|---|
POST | /v1/signup | Create an organization and a machine (M2M) client for it — the zero-human on-ramp for agents. No auth; rate-limited. Body: { "orgName", "orgSlug", "clientName"? } — orgName and orgSlug are required and should name the real project/company (not placeholders); orgSlug is lowercase alphanumeric + hyphens, disambiguated on collision. Returns { "org", "m2m": { "clientId", "clientSecret" } } — the secret is shown once. The org starts memberless; a human joins later via invite. |
Also reachable as POST https://mcp.seekrit.dev/signup (the hosted MCP server
proxies to this endpoint).
CLI sign-in (browser-approved)
What seekrit login drives. The CLI generates its own session token, registers
only the SHA-256 hash, and a human authorizes that hash in the dashboard — so the
credential never travels through the API, and an approval response carries nothing
secret.
| Method | Path | Description |
|---|---|---|
POST | /v1/cli-login | Open a sign-in request. No auth (the caller has no credential yet); rate-limited per IP. Body: { "sessionId": "skc_…", "tokenHash", "deviceLabel", "client"? } — deviceLabel/client are self-reported and shown to the human, never used for authorization. Returns { "code", "verifyUrl", "requestExpiresAt", "pollIntervalSeconds" }. Requests expire in 10 minutes. |
POST | /v1/cli-login/poll | Ask whether the request was authorized. No auth. Body: { "code" }. Returns { "status": "pending" | "approved" | "denied" | "expired" }, plus { "sessionId", "email", "expiresAt" } once approved. |
GET | /v1/cli-login/:code | The request's details, for the approval screen: { "request": { code, status, deviceLabel, client, ipAddress, createdAt, requestExpiresAt } }. Requires a browser session — a CLI session may not authorize another device. |
POST | /v1/cli-login/:code/approve | Authorize the device; the registered hash becomes a usable credential (90 days). Requires a browser session, and — for members with a second factor — that it was re-entered within the last 10 minutes, else 403 mfa_required. Returns { "session": { id, expiresAt } }. Audits cli_session.approved. |
POST | /v1/cli-login/:code/deny | Decline the request. Requires a browser session. Audits cli_session.denied. |
Manage the resulting sessions under Identity.
Health
| Method | Path | Description |
|---|---|---|
GET | /health | Liveness check (no auth). |
Identity
| Method | Path | Description |
|---|---|---|
GET | /v1/me | The current user and their organizations. |
GET | /v1/me/keys | Your public key and passphrase-encrypted private key. |
PUT | /v1/me/keys | Upload your keys (one-time key setup). |
GET | /v1/me/cli-sessions | Devices authorized with seekrit login: { "sessions": [...], "currentSessionId" }. Revoked and expired sessions stay listed (the ledger of what has signed in as you); currentSessionId is set when the caller is a CLI session. |
DELETE | /v1/me/cli-sessions/:sessionId | Revoke a CLI session — it stops authenticating on its next request. Allowed from a CLI session for its own id (that's seekrit logout). Audits cli_session.revoked. |
GET | /v1/me/notifications | Your email notification preferences (all types, defaults applied). |
PUT | /v1/me/notifications | Update notification preferences. Body: { "prefs": { "<type>": true | false } } (partial). Returns the full merged map. |
Organizations
| Method | Path | Description |
|---|---|---|
GET | /v1/orgs | List organizations you can access. |
POST | /v1/orgs | Create an organization (creator becomes owner). |
GET | /v1/orgs/:orgId | Get one organization and your role. |
PATCH | /v1/orgs/:orgId | Rename an organization: { name } (display name only — the slug is fixed). (admin) |
GET | /v1/orgs/:orgId/members | List members (includes public keys, for granting). |
GET | /v1/orgs/:orgId/mfa-policy | Get the org's second-factor policy: { required, configured }. configured is false when the identity provider isn't set up. (admin) |
PATCH | /v1/orgs/:orgId/mfa-policy | Require (or stop requiring) MFA for all members: { required }. (admin) |
GET | /v1/orgs/:orgId/invites | List pending invitations. (admin) |
POST | /v1/orgs/:orgId/invites | Invite someone by email { email, role? } (role defaults to member, admin allowed — never owner). Sends a magic-link invitation; they join at that role on first sign-in. (admin) |
DELETE | /v1/orgs/:orgId/invites/:inviteId | Revoke a pending invitation. (admin) |
Applications & environments
| Method | Path | Description |
|---|---|---|
GET | /v1/orgs/:orgId/apps | List applications. |
POST | /v1/orgs/:orgId/apps | Create an application. (admin) |
GET | /v1/orgs/:orgId/apps/:appId | Get an application and its environments. |
PATCH | /v1/orgs/:orgId/apps/:appId | Rename an application: { name } (display name only — the slug is fixed). (admin) |
DELETE | /v1/orgs/:orgId/apps/:appId | Delete an application. (admin) |
GET | /v1/orgs/:orgId/apps/:appId/envs | List environments. |
POST | /v1/orgs/:orgId/apps/:appId/envs | Create an environment (body includes the wrapped DEK). (admin) |
GET | /v1/orgs/:orgId/envs/:envId | Get an environment. |
DELETE | /v1/orgs/:orgId/envs/:envId | Delete an environment. (admin — or any grant-holder when it's a branch) |
GET | /v1/orgs/:orgId/envs/:envId/grantees | The public keys of this environment's grant-holders, for wrapping a new key to them. No key material is returned. |
An environment is owned by either an application or a group; applicationId
and groupId are both present on the row, with the unused one null.
Environment lists exclude branches — they're returned by the branch endpoints below.
Each environment returned by GET /v1/orgs/:orgId/apps/:appId and
GET /v1/orgs/:orgId/groups/:groupId also carries a canDecrypt boolean: whether
the calling principal holds a key grant for that environment. Secret names and
versions are visible to any member, but values only decrypt with a grant — so the
dashboard uses canDecrypt to lock the environments you can't read. It's computed
per request, not a stored field.
Branches
A branch is an ephemeral environment overlaying another:
an ordinary environment row with a parentEnvironmentId and an expiresAt. It
stores only the values that differ; the rest resolve from its parent at read
time, so creating one copies and re-encrypts nothing.
| Method | Path | Description |
|---|---|---|
GET | /v1/orgs/:orgId/envs/:envId/branches | List branches of an environment. |
POST | /v1/orgs/:orgId/envs/:envId/branches | Fork this environment: { slug, name?, ttlSeconds?, wrappedDek, recoveryWrappedDek?, grants? }. envId is the parent. |
GET | /v1/orgs/:orgId/apps/:appId/branches | Every branch in an application. |
DELETE | /v1/orgs/:orgId/envs/:branchId | Tear one down (the environment-delete route above). |
ttlSeconds omitted takes the default (7 days); pass an explicit null for no
expiry. The cap is 30 days. Expired branches are deleted automatically, along
with their overrides, key grants, and any service token bound to them.
grants wraps the branch's new DEK to principals that already hold a grant on
the parent (get their public keys from …/envs/:envId/grantees) — a grant naming
anyone else is rejected. Creating a branch requires a grant on the parent, not an
admin role: it conveys no access the caller doesn't already have. Parents must be
application environments and must not themselves be branches.
Groups
A group is a reusable, org-scoped secret bag. Its environments (matched by slug) reuse the same secret and key-grant endpoints above.
| Method | Path | Description |
|---|---|---|
GET | /v1/orgs/:orgId/groups | List groups. |
POST | /v1/orgs/:orgId/groups | Create a group. (admin) |
GET | /v1/orgs/:orgId/groups/:groupId | Get a group and its environments. |
PATCH | /v1/orgs/:orgId/groups/:groupId | Rename a group: { name } (display name only — the slug is fixed). (admin) |
DELETE | /v1/orgs/:orgId/groups/:groupId | Delete a group. (admin) |
GET | /v1/orgs/:orgId/groups/:groupId/envs | List a group's environments. |
POST | /v1/orgs/:orgId/groups/:groupId/envs | Create a group environment (body includes the wrapped DEK). (admin) |
Composition
Which groups an application environment pulls in, and their precedence.
| Method | Path | Description |
|---|---|---|
GET | /v1/orgs/:orgId/envs/:envId/groups | List composed groups (slug, name, position). |
POST | /v1/orgs/:orgId/envs/:envId/groups | Compose a group { groupId, position? }. (admin) |
DELETE | /v1/orgs/:orgId/envs/:envId/groups/:groupId | Remove a composed group. (admin) |
Resolve
| Method | Path | Description |
|---|---|---|
GET | /v1/resolve | The full layered environment for the calling principal. |
For a service token, the org + app + environment come from the token itself. For
a user session pass ?env=<envId>. Repeat ?with=<groupSlug>:<envSlug> to
resolve specific groups at a different slug. Add ?branch=<slug|id> to read an
ephemeral branch of that environment — a service token
stays bound to its own environment, so it can only name branches of that one.
Returns { scope, layers }, where layers are ordered lowest-precedence first
(composed groups → the app environment → the branch's overrides) and each carries
its environmentId, secret ciphertexts, and the DEK wrapped to the caller. A
branch resolve additionally reports scope.branchOf, and needs a grant on every
layer including the parent — so branch access never exceeds parent access. The client decrypts and merges; the server sees only
ciphertext. The language SDKs, CLI, and seekrit-run all
implement this same decrypt-and-merge against this endpoint.
Resolve is edge-cached per principal, so a fleet of identical containers or CI
jobs sharing one service token is served from cache without re-querying the
database. Responses reflect secret writes, key changes, and composition
changes within seconds (a write invalidates the cache; a short TTL is the
backstop). Because the cache key includes the calling principal, one caller
never receives another's wrapped DEK. Successful resolves are metered for
usage/billing but are not written to the audit log; a denied resolve is
audited (env.resolve_denied).
Secrets
Values are opaque ciphertext blobs produced by the client.
| Method | Path | Description |
|---|---|---|
GET | /v1/orgs/:orgId/envs/:envId/secrets | List secrets (with ciphertext). |
PUT | /v1/orgs/:orgId/envs/:envId/secrets/:name | Create or update a secret (appends a version). |
GET | /v1/orgs/:orgId/envs/:envId/secrets/:name/versions?limit= | The secret's history, newest first: { versions: [{ version, ciphertext, createdByType, createdById, restoredFromVersion, createdAt }], currentVersion }. limit defaults to 50, max 200. |
POST | /v1/orgs/:orgId/envs/:envId/secrets/:name/restore | Roll back to an earlier version: { version }. Returns { secret, restoredFrom }. |
DELETE | /v1/orgs/:orgId/envs/:envId/secrets/:name | Delete a secret and its whole version history. |
Restore is keyless. It replays a ciphertext the server already stores —
no decryption, no re-encryption — so a machine (M2M) credential can roll a
secret back even though it can never read the value. The replayed blob lands
as a new version (restoredFromVersion points at its source), so history
stays append-only and a rollback is itself undoable. 404 if the version
doesn't exist; 400 if the secret already holds that exact value.
Key grants
| Method | Path | Description |
|---|---|---|
GET | /v1/orgs/:orgId/envs/:envId/key | The calling principal's own wrapped DEK. |
GET | /v1/orgs/:orgId/envs/:envId/keys | List all grants for the environment. (admin) |
POST | /v1/orgs/:orgId/envs/:envId/keys | Grant a wrapped DEK to a principal. (admin) |
DELETE | /v1/orgs/:orgId/envs/:envId/keys/:grantId | Revoke a grant. (admin) |
KMS keys
Client-side managed keys for application-layer encryption and signing. Material
is generated by the client and stored only as wrapped grants; sign keys also
publish a public key per version. Management is (admin); using a key needs a
grant. See Managed keys.
| Method | Path | Description |
|---|---|---|
GET | /v1/orgs/:orgId/kms/keys | List keys the caller can see (admins: all org keys; members: keys they hold a grant for). |
POST | /v1/orgs/:orgId/kms/keys | Create a key: { name, purpose ('encrypt'|'sign'), spec, applicationId?, groupId?, publicKeyJwk? (sign only), grants: [{ principalType, principalId, wrappedKey }] }. At most one of applicationId/groupId. (admin) |
GET | /v1/orgs/:orgId/kms/keys/:keyId | Key metadata + every version (public keys for sign). (admin) |
GET | /v1/orgs/:orgId/kms/keys/:keyId/key | The caller's wrapped material across granted versions ({ purpose, spec, currentVersion, grants: [{ version, wrappedKey }] }). 403 if the key is disabled. |
GET | /v1/orgs/:orgId/kms/keys/:keyId/public | Published public keys of a sign key, per version. Grant-free within the org. |
GET | /v1/orgs/:orgId/kms/keys/:keyId/grants | Grantees, each with the versions they hold. (admin) |
POST | /v1/orgs/:orgId/kms/keys/:keyId/grants | Grant the current version: { principalType, principalId, wrappedKey }. (admin) |
DELETE | /v1/orgs/:orgId/kms/keys/:keyId/grants?principalType=&principalId= | Revoke a principal entirely (all versions). (admin) |
POST | /v1/orgs/:orgId/kms/keys/:keyId/rotate | Add a version and re-wrap for grantees: { publicKeyJwk? (sign only), grants: [{ principalType, principalId, wrappedKey }] }. (admin) |
POST | /v1/orgs/:orgId/kms/keys/:keyId/disable | Disable a key — blocks all use (encrypt/decrypt/sign, new grants/rotations); grant rows retained. (admin) |
DELETE | /v1/orgs/:orgId/kms/keys/:keyId | Soft-delete a key — hides it from every listing and read path; row and versions retained for audit; name frees up for reuse. (admin) |
Key material never reaches the server. For an encrypt key each wrappedKey
is the AES key wrapped to a principal (wd1.); for a sign key it is the
wrapped PKCS8 private key, and the public key is stored per version for
grant-free verification. Ciphertext (ce1.), wrapped data keys (dk1.), and
signatures (sg1.) are produced and consumed entirely client-side.
Recovery
Customer-controlled M-of-N recovery. The client generates and Shamir-splits the recovery key; these routes carry only the recovery public key, opaque wrapped shares, and ciphertext grants. Management is (admin); a custodian reads their own share and contributes to a ceremony. See Customer-controlled recovery.
| Method | Path | Description |
|---|---|---|
GET | /v1/orgs/:orgId/recovery | Status: threshold, custodians, and environment coverage. (admin) |
POST | /v1/orgs/:orgId/recovery | Enable recovery: { recoveryPublicKeyJwk, threshold, shares[], grants[] }. (admin) |
POST | /v1/orgs/:orgId/recovery/rotate | Rotate to a fresh recovery key, custodian set, and env re-wraps. (admin) |
DELETE | /v1/orgs/:orgId/recovery | Disable recovery and delete every recovery grant. (admin) |
POST | /v1/orgs/:orgId/recovery/grants | Backfill recovery grants: { grants: [{ environmentId, wrappedDek }] }. (admin) |
GET | /v1/orgs/:orgId/recovery/share | The caller's own wrapped recovery share (custodian only). |
GET | /v1/orgs/:orgId/recovery/env-keys | Every DEK wrapped to the recovery key, for the target to reconstruct. (admin) |
GET | /v1/orgs/:orgId/recovery/requests | List recovery ceremonies. (admin) |
POST | /v1/orgs/:orgId/recovery/requests | Start a ceremony: { targetPublicKeyJwk, targetType?, targetId?, reason? }. (admin) |
GET | /v1/orgs/:orgId/recovery/requests/:id | A request plus its contributed (re-wrapped) shares. |
POST | /v1/orgs/:orgId/recovery/requests/:id/shares | A custodian contributes: { shareIndex, contributedShare }. |
POST | /v1/orgs/:orgId/recovery/requests/:id/complete | Target restores access: { principalType, principalId, grants[] }. (admin) |
POST | /v1/orgs/:orgId/recovery/requests/:id/cancel | Cancel an open request. (admin) |
The server never sees a recovery share or the recovery key in the clear. Shares are wrapped to
each custodian (wd1.); during a ceremony each is unwrapped and re-wrapped to the target on the
custodian's own device; the recovery key is reconstructed only on the target's machine, from
ciphertext this API merely relays.
Service tokens
| Method | Path | Description |
|---|---|---|
GET | /v1/orgs/:orgId/tokens | List service tokens. (admin) |
POST | /v1/orgs/:orgId/tokens | Register a token (client sends hash + public key; role defaults to member, pass admin for a provisioning token; optional environmentId binds it to an app environment). (admin) |
DELETE | /v1/orgs/:orgId/tokens/:tokenId | Revoke a token (idempotent). (admin) |
DELETE | /v1/orgs/:orgId/tokens/:tokenId/permanent | Delete a token: drops the env/KMS grants wrapped to it and marks it deleted (soft delete — the row is retained for the audit trail but hidden from listings). Requires the token to already be revoked (409 otherwise). (admin) |
A service token may read or write secrets only for environments it holds a key grant for — its bound environment and the group slices composed into it. It cannot address other environments in the org.
Honey tokens
Decoy credentials that grant nothing and alert on use. All routes are (admin), and gated on the Honey tokens plan feature. See the guide.
| Method | Path | Description |
|---|---|---|
GET | /v1/orgs/:orgId/honey-tokens | List decoys with their trip counts and the time, source IP, and user agent of the last trip. Never returns the token hash. |
POST | /v1/orgs/:orgId/honey-tokens | Register a decoy (client sends tokenId + tokenHash, plus an optional placement note). Deliberately takes no publicKeyJwk: a decoy must not be wrappable, so nothing can be granted to it. |
DELETE | /v1/orgs/:orgId/honey-tokens/:honeyTokenId | Delete a decoy, which stops it alerting. A hard delete — there are no grants to clear, and recorded trips live on in the append-only audit log. |
There is no endpoint that trips a honey token, because tripping isn't an
operation: presenting one as Authorization: Bearer skt_… on any route
records the trip, alerts the org's admins, and returns the same
401 unknown service token an unregistered token receives. The responses are
identical by construction so a decoy can't be identified by probing.
Temporary access (leases)
Vault-style short-lived credentials. All routes are (admin).
| Method | Path | Description |
|---|---|---|
GET | /v1/orgs/:orgId/leases/broker-key | The org broker's public key. Wrap the admin credential to it before registering a target. |
GET | /v1/orgs/:orgId/leases/targets | List provisioning targets (never the wrapped admin secret's plaintext). |
POST | /v1/orgs/:orgId/leases/targets | Register a target. Postgres/MySQL/Redis: config = provider, executor, connection, accessLevel readonly/readwrite/custom (Postgres adds schema; MySQL adds userHost; Redis's connection takes an optional db index), or custom create/revoke templates. SSH: config = provider: "ssh", executor: "in_do", caPublicKey, optional allowedPrincipals/extensions/maxTtlSeconds/connection. AWS: config = provider: "aws", executor: "in_do", roleArn, region, optional externalId/sessionPolicy/maxTtlSeconds. GCP: config = provider: "gcp", executor: "in_do", serviceAccount, optional scopes/delegates/maxTtlSeconds. MongoDB: config = provider: "mongodb", executor: "in_do", connection, accessLevel readonly/readwrite/custom (custom adds a roles array of {role,db}), optional authSource/tls/maxTtlSeconds. wrappedAdminSecret is the admin credential (a pg/mysql/redis/mongodb connection string, the SSH CA private key, the AWS base IAM credential as JSON {accessKeyId,secretAccessKey}, or the GCP service-account key JSON) wrapped to the broker key. |
DELETE | /v1/orgs/:orgId/leases/targets/:targetId | Remove a target. |
GET | /v1/orgs/:orgId/leases | List the lease ledger (status, resource ref, provider, expiry — no secret material). |
POST | /v1/orgs/:orgId/leases | Mint a lease. The body is tagged by provider and must match the target. Postgres: { provider: "postgres", targetId, roleName, verifier, ttlSeconds } → returns connection (host/port/db/username), never the password. MySQL: { provider: "mysql", targetId, roleName, verifier, ttlSeconds } (verifier = mysql_native_password hash) → returns connection, never the password. Redis: { provider: "redis", targetId, roleName, verifier, ttlSeconds } (verifier = lowercase-hex SHA-256 digest; database in the response is the logical db index) → returns connection, never the password. SSH: { provider: "ssh", targetId, publicKey, principals[], ttlSeconds } → returns ssh (the signed certificate, principals, caPublicKey, expiry), never a private key. AWS: { provider: "aws", targetId, recipientPublicKey, ttlSeconds } (recipientPublicKey = an ephemeral P-256 JWK; ttlSeconds 900–43200) → returns aws (wrappedCredential — the STS credential wrapped to recipientPublicKey — plus region, roleArn, expiry), never a plaintext credential. GCP: { provider: "gcp", targetId, recipientPublicKey, ttlSeconds } (recipientPublicKey = an ephemeral P-256 JWK; ttlSeconds 60–43200) → returns gcp (wrappedCredential — the OAuth access token wrapped to recipientPublicKey — plus serviceAccount, scopes, expiry), never a plaintext token. MongoDB: { provider: "mongodb", targetId, recipientPublicKey, ttlSeconds } (recipientPublicKey = an ephemeral P-256 JWK) → returns mongodb (wrappedCredential — the created user's password wrapped to recipientPublicKey — plus host, port, database, authSource, expiry), never a plaintext credential. |
DELETE | /v1/orgs/:orgId/leases/:leaseId | Revoke a lease. Postgres/MySQL/Redis/MongoDB drop the role/user immediately; SSH, AWS, and GCP mark the ledger (the certificate / token stays valid until it expires). |
Postgres, MySQL, Redis, and SSH are tier 1 (verifier injection): the client
generates the secret half locally and sends only a verifier — a SCRAM
verifier (Postgres), a mysql_native_password hash (MySQL), a SHA-256
digest (Redis), or an SSH public key (SSH). None can authenticate on its
own, so the plaintext password / private key never reaches the API. AWS,
GCP, and MongoDB are tier 2: the target mints the credential (STS
AssumeRole; GCP generateAccessToken; a MongoDB createUser password
generated by the broker), so the broker wraps it to the consumer's ephemeral
public key (recipientPublicKey) and returns only that ciphertext —
decryptable solely on the requesting machine. The admin credential used to
provision (a pg/mysql/redis/mongodb admin connection string, the SSH CA private
key, the AWS base IAM credential, or the GCP service-account key) is wrapped to
the broker Durable Object's public key and is never seen in plaintext by the
control plane.
Secret rotation
Managed, scheduled replacement of a stored secret's value. All routes are (admin). No route returns a secret value; a rotated value is read back through the normal secret/resolve paths. See Secret rotation.
| Method | Path | Description |
|---|---|---|
GET | /v1/orgs/:orgId/rotation/rotator-key | The rotator public key (the org broker's) plus grantedEnvironmentIds — the environments that already hold a rotator grant. Wrap an environment's DEK to this key client-side before configuring rotation. |
GET | /v1/orgs/:orgId/rotation | List rotation policies: kind, cadence, status, last/next run, lastError. Never values. |
POST | /v1/orgs/:orgId/rotation | Configure (or replace) a secret's policy. { environmentId, secretName, config, intervalSeconds, targetId?, wrappedDek?, rotateNow? }. config is tagged by kind: { kind: "generated", length?, alphabet? } contacts nothing; { kind: "postgres"|"mysql"|"redis", username, passwordLength?, statements? } re-keys an existing account (mysql adds userHost) and requires a matching targetId from temporary access. intervalSeconds is 300–31536000. wrappedDek is the environment DEK wrapped to the rotator key — required unless that environment already holds a rotator grant, and impossible for the server to produce. rotateNow rotates once immediately and returns the new version. |
GET | /v1/orgs/:orgId/rotation/:rotationId | One policy. |
PATCH | /v1/orgs/:orgId/rotation/:rotationId | Change intervalSeconds, config (same kind only), or status (active/paused). Resuming clears the failure streak. |
POST | /v1/orgs/:orgId/rotation/:rotationId/rotate | Rotate now — the same path the scheduler uses. Returns { rotation, version, rotatedAt }. |
DELETE | /v1/orgs/:orgId/rotation/:rotationId | Remove the policy; the secret and its versions are untouched. rotatorRevoked reports whether the environment's rotator grant was dropped (it is, once nothing there rotates). |
Rotation is the one path where a server-side component can encrypt into an
environment — writing a new value requires the environment's data key. The
boundary is one ordinary key grant: an environment_keys row with
principal_type = rotator, holding the DEK wrapped to the per-org broker
Durable Object's public key, whose private half never leaves DO storage (so a
database dump is still only ciphertext). You create that grant from your own client —
the API cannot — it covers only the environments where you enabled rotation, it
is re-read on every rotation so revoking it halts rotation at once, and it is
dropped when the environment's last policy goes away. For the database kinds
the target still receives only a verifier, exactly as with temporary access.
Audit
| Method | Path | Description |
|---|---|---|
GET | /v1/orgs/:orgId/audit | Paginated audit entries. (admin) Query: cursor, limit, action, resourceType. |
Token creation is client-driven: the client generates the keypair and token string locally and submits only the token id, a SHA-256 hash, and the public key. The server never sees the token secret.
Plans & billing
Read the org's plan, current metered usage, and start self-serve billing. See the plans & billing guide.
| Method | Path | Description |
|---|---|---|
GET | /v1/orgs/:orgId/billing | The org's plan and entitlements. Returns { subscription, plan, promo, entitlements[], usage[], overrides[], enforced, manage }. Readable by any member. |
POST | /v1/orgs/:orgId/billing/checkout | Start a Checkout Session to upgrade. Body { family } (a plan family). Returns { url } to redirect the browser to. (admin) |
POST | /v1/orgs/:orgId/billing/portal | Open the Billing Portal to manage/update payment. Returns { url }. (admin) |
POST | /v1/orgs/:orgId/billing/cancel | Downgrade to the Free (default) plan: cancels any active paid subscription in the biller and reverts the org to Free immediately. Returns { ok: true }. (admin) |
POST | /v1/orgs/:orgId/billing/promo | Redeem a promo code, moving the org onto the plan the code grants. Body { code } (normalized server-side). Returns the same body as GET /billing. (admin) |
usage[] is the metered dimensions, each { key, metric, label, quantity, included, available }
— quantity is the current usage for this period, included is what the plan covers before overage
(null = unlimited), and available is false when a metric can't be measured (shown as unknown,
not zero). manage reports which self-serve actions this deployment supports:
{ checkout, checkoutFamilies[], portal }.
An organization can pay for a higher plan (checkout/portal), but it can't grant itself
one — comped plan assignment and per-org entitlement overrides are an operator-only surface, not
reachable by org admins. While enforced is false, plan limits are informational: every org has
full access and the usage numbers preview what a plan will include once enforcement is on.
Audit log export (OTLP)
Stream an organization's audit trail to your own SIEM/observability tooling. See the audit log export guide.
| Method | Path | Description |
|---|---|---|
GET | /v1/orgs/:orgId/log-sink | Get the org's OTLP log-sink config: endpoint, enabled, headerNames, and delivery status (lastSuccessAt, lastAttemptAt, lastError). Header values are never returned. (admin) |
PUT | /v1/orgs/:orgId/log-sink | Configure the sink: { endpoint, headers?, enabled? }. endpoint is the OTLP/HTTP logs URL; headers (e.g. { "Authorization": "Bearer …" }) is encrypted at rest — omit it to leave the stored value unchanged, pass {} to clear it. (admin) |
DELETE | /v1/orgs/:orgId/log-sink | Remove the sink. (admin) |
POST | /v1/orgs/:orgId/log-sink/test | Synchronously POST a synthetic record to the endpoint and return { ok, status, error }. (admin) |
Delivery is at-least-once and near-real-time: an every-minute sweep ships new audit rows to your endpoint as OTLP log records, advancing a per-org watermark only after the collector accepts a batch (a transient outage re-ships rather than drops). Records carry attribution and the audit log's redacted metadata only — never secret values, ciphertext, or credentials.
Agent access policy
Which upstream each agent may reach, with which credential, for which operations. The policy bundle is signed in the publishing admin's browser and is opaque to the API — see the agent access policy guide.
| Method | Path | Description |
|---|---|---|
GET | /v1/orgs/:orgId/agents | List agent identities: name, slug, bound environment, live policy version, and when a proxy last fetched it. (admin) |
POST | /v1/orgs/:orgId/agents | Create an identity: { name, slug, environmentId? }. Holds no key material and can never be granted any — it is a policy subject only. (admin) |
GET | /v1/orgs/:orgId/agents/:agentId | One identity, plus its bound environment's slug. (admin) |
PATCH | /v1/orgs/:orgId/agents/:agentId | Update { name?, enabled?, environmentId? }. Disabling is the revocation path: policy fetches start failing, and each proxy's current bundle expiry bounds the window. Audits agent.updated. (admin) |
DELETE | /v1/orgs/:orgId/agents/:agentId | Delete the identity and its published versions. Audits agent.deleted. (admin) |
GET | /v1/orgs/:orgId/agents/:agentId/policies | Published versions (newest first, 50 max), each with its signed bundle, signer thumbprint, expiry, and decoded rules. (admin) |
POST | /v1/orgs/:orgId/agents/:agentId/policies | Publish { bundle } — an ap1.<body>.<signature> envelope signed client-side. Requires a user session: an admin service token cannot publish, so an agent cannot widen its own policy. The bundle must be signed by the caller's own key, name this org/agent/slug, carry policy_version = current + 1 (else 409), and expire between 1 hour and 90 days out. Audits agent.policy_published with the signer and version. (admin) |
POST | /v1/orgs/:orgId/agents/:agentId/policies/:version/rollback | Republish an earlier version's bundle as a new version — a roll-forward, so nothing is re-signed and the restored bundle keeps its original expiry. Audits agent.policy_rolled_back. (admin) |
GET | /v1/orgs/:orgId/agents/signers/me | Your own signing thumbprint (RFC 7638), for the trust-anchor snippet: { "signer": { userId, thumbprint } | null }. (admin) |
GET | /v1/agents/:agentRef/policy | The proxy's read path. agentRef is the identity's id or slug, scoped to the caller's org. Returns { bundle, version, expiresAt, agent, signerThumbprint } with an ETag; send If-None-Match and a steady state costs a 304. Requires a service token (or a user session, for debugging) — a machine (M2M) credential is refused. 403 when the identity is disabled or the token is bound to a different environment; 404 when nothing is published yet. Not audited per fetch (it is a poll, like /v1/resolve); it records lastPolicyFetchAt, throttled to a minute. |
The bundle is stored and served verbatim: the API cannot forge one, because the signature is made in a browser with a key it never holds. A proxy verifies every bundle against signer thumbprints pinned in its own local file before acting on a rule, so withholding policy makes a proxy fail closed — and widening it is not something this API can do.
Third-party sync
Push an environment's resolved secrets to a platform that holds its own copy. See the third-party sync guide.
| Method | Path | Description |
|---|---|---|
GET | /v1/orgs/:orgId/sync/connections | List sync connections. Returns publicKeyJwk and status; never the wrapped credential. (admin) |
GET | /v1/orgs/:orgId/sync/connections/:connectionId/public-key | Mint (or return) the keypair for a connection id, before the connection exists. Wrap the destination credential and the environment DEKs to this key. (admin) |
POST | /v1/orgs/:orgId/sync/connections | Create a connection: { id?, name, config, wrappedCredential }. config is tagged by provider: { provider: "vercel", teamId? }, { provider: "cloudflare-workers" | "cloudflare-pages" | "cloudflare-secrets-store", accountId } (32 hex), { provider: "railway", tokenKind } (account | project, default account — it selects the auth header), { provider: "aws-secrets-manager" | "aws-parameter-store", region, accessKeyId }, { provider: "render" } (a Render API key is user-scoped — no account field), { provider: "fly" } (nothing to scope either: a Fly app name is globally unique, and the auth scheme is read from the token), { provider: "northflank" } / { provider: "digitalocean" } / { provider: "heroku" } (also empty — a Northflank token carries its own team scope, a DigitalOcean token its own account, and a Heroku token its user's access to every app they can reach), { provider: "netlify", accountId } (the team slug or account ID — Netlify keeps environment variables on the account, and a personal access token does not say which team to write), { provider: "bunnyshell" } (empty again — a Bunnyshell access token carries its user's access to every organization they belong to, and both variable collections name their parent by a globally unique ID), or { provider: "github-actions", baseUrl? } (empty for github.com too — a GitHub token addresses everything by owner/repo or org, which the destination names; baseUrl points the connection at a self-hosted GitHub Enterprise Server API root and must be https), { provider: "gcp-secret-manager", projectId } (the project ID or number whose Secret Manager to write — a service-account key names its own identity, but can be granted secrets in projects other than its own), or { provider: "langgraph-platform", region?, baseUrl?, tenantId? } (all three optional and all about where the control plane is: region is one of us (the default), eu, apac, aws-us, and matters because a LangSmith key minted in one region is refused by another with a bare 401; baseUrl points the connection at a self-hosted LangSmith control plane, must be https, and is rejected together with region; tenantId is the workspace UUID, needed only for an organization-scoped key, which reaches several workspaces). wrappedCredential is the destination's credential wrapped to the connection's public key — an API token, for AWS the secret access key alone (the access key id is an identifier and lives in config), and for GCP the whole service-account key JSON. Pass the same id you fetched the key for. (admin) |
POST | /v1/orgs/:orgId/sync/connections/:connectionId/verify | Test the stored credential against { destination }. Returns { ok: true } or 400 { ok: false, error }. (admin) |
DELETE | /v1/orgs/:orgId/sync/connections/:connectionId | Delete the connection, its bindings, and every sync key grant made to it, then destroy its keypair. (admin) |
GET | /v1/orgs/:orgId/sync/bindings | List bindings with their destination, filters, and last-run status. (admin) |
POST | /v1/orgs/:orgId/sync/bindings | Enable sync for one environment: { connectionId, environmentId, destination, nameTransform?, include?, exclude?, onDelete?, mode?, wrappedDeks, acknowledgedDecryption }. destination is tagged by provider and must match the connection: vercel { projectId, targets[], gitBranch? }; cloudflare-workers { scriptName }; cloudflare-pages { projectName, environments[] } (production/preview); cloudflare-secrets-store { storeId, scopes[] }; railway { projectId, environmentId, serviceId?, skipDeploys? } (all UUIDs; omit serviceId to write the environment's shared variables); aws-secrets-manager { layout: "secret-per-name" | "json-bundle", pathPrefix?, secretName?, kmsKeyId? } (secretName required for json-bundle); aws-parameter-store { path, type: "SecureString" | "String", tier, kmsKeyId? } (path starts and ends with /); render { kind: "service", serviceId } or { kind: "env-group", envGroupId }; fly { appName }; northflank { projectId, secretGroupId } (both are the slugs from their URLs); digitalocean { kind: "app", appId, scope? } or { kind: "component", appId, componentName, scope? } (appId is the App Platform app UUID; scope is RUN_TIME (default), BUILD_TIME, or RUN_AND_BUILD_TIME); heroku { app } (the app name or its UUID); netlify { siteId, contexts[], branch?, secret? } (siteId is the site's API ID — a UUID, since Netlify resolves no site names on this endpoint and an unresolved one writes to the whole team; contexts is one or more of production, deploy-preview, branch-deploy, branch, dev, with branch requiring branch; secret defaults to true and marks what seekrit creates as a write-only Netlify secret); bunnyshell { kind: "environment", environmentId, secret? } or { kind: "project", projectId, secret? } (IDs are Bunnyshell's own opaque strings, from bns environments list / bns projects list; a project binding's values are inherited by every environment created in it after the push, which is how ephemeral environments get seeded, and existing ones are untouched; secret defaults to true and sets isSecret on variables seekrit creates). github-actions { kind: "repo", owner, repo }, { kind: "environment", owner, repo, environment }, or { kind: "org", org, visibility, selectedRepositoryIds? } — the three scopes are three endpoints with three blast radii and three separate encryption keys; visibility is all | private | selected and has no default (all reaches every repository in the org, including ones added later), and selectedRepositoryIds is required, non-empty, and numeric for selected and rejected otherwise. gcp-secret-manager { layout: "secret-per-name" | "json-bundle", idPrefix?, secretId?, replication: "automatic" | "user-managed", locations?, kmsKeyName?, pruneVersions? } (secretId required for json-bundle; idPrefix takes only [A-Za-z0-9_-] — a Secret Manager ID has no slashes; locations required for user-managed replication, which is immutable once a secret exists; kmsKeyName is a full Cloud KMS resource name and, keys being regional, is accepted only with automatic replication or a single location; pruneVersions destroys the version each push supersedes, and only ever one seekrit itself wrote). langgraph-platform { deploymentId } (the deployment UUID — a deployment's secrets belong to the deployment, so there is nothing narrower to address, and every write creates a new revision, which rebuilds and rolls out the Agent Server). wrappedDeks is one { environmentId, wrappedDek } per environment the run reads — the target plus every composed group environment. acknowledgedDecryption must be true. (admin) |
PATCH | /v1/orgs/:orgId/sync/bindings/:bindingId | Update destination, filters, onDelete, mode, or enabled. Cannot move a binding to another environment or connection — delete and recreate, so a fresh grant and acknowledgment are required. (admin) |
DELETE | /v1/orgs/:orgId/sync/bindings/:bindingId | Delete the binding and revoke its environment's grant to that connection (unless another binding still needs it). (admin) |
POST | /v1/orgs/:orgId/sync/bindings/:bindingId/run | Push now, synchronously. Returns { run: { id, status, pushed[], deleted[], failures[], error } }. (admin) |
GET | /v1/orgs/:orgId/sync/runs | Run history (optionally ?bindingId=), newest first. Carries destination key names, counts, and error strings — never values. (admin) |
Sync is the one feature where seekrit's servers decrypt. It applies only to environments with a
sync key grant, and only for the destination that grant names. The grant is computed
client-side, so the API cannot create one on its own — POST /sync/bindings fails without
wrappedDeks. Every run writes a durable sync.run_succeeded / sync.run_failed audit row.