seekrit
Docs/REST API

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 stored role is member (runtime credential) or admin (passes admin-gated routes, for headless provisioning). owner is 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 by seekrit login and 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)

MethodPathDescription
POST/v1/signupCreate 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.

MethodPathDescription
POST/v1/cli-loginOpen 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/pollAsk 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/:codeThe 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/approveAuthorize 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/denyDecline the request. Requires a browser session. Audits cli_session.denied.

Manage the resulting sessions under Identity.

Health

MethodPathDescription
GET/healthLiveness check (no auth).

Identity

MethodPathDescription
GET/v1/meThe current user and their organizations.
GET/v1/me/keysYour public key and passphrase-encrypted private key.
PUT/v1/me/keysUpload your keys (one-time key setup).
GET/v1/me/cli-sessionsDevices 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/:sessionIdRevoke 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/notificationsYour email notification preferences (all types, defaults applied).
PUT/v1/me/notificationsUpdate notification preferences. Body: { "prefs": { "<type>": true | false } } (partial). Returns the full merged map.

Organizations

MethodPathDescription
GET/v1/orgsList organizations you can access.
POST/v1/orgsCreate an organization (creator becomes owner).
GET/v1/orgs/:orgIdGet one organization and your role.
PATCH/v1/orgs/:orgIdRename an organization: { name } (display name only — the slug is fixed). (admin)
GET/v1/orgs/:orgId/membersList members (includes public keys, for granting).
GET/v1/orgs/:orgId/mfa-policyGet 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-policyRequire (or stop requiring) MFA for all members: { required }. (admin)
GET/v1/orgs/:orgId/invitesList pending invitations. (admin)
POST/v1/orgs/:orgId/invitesInvite 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/:inviteIdRevoke a pending invitation. (admin)

Applications & environments

MethodPathDescription
GET/v1/orgs/:orgId/appsList applications.
POST/v1/orgs/:orgId/appsCreate an application. (admin)
GET/v1/orgs/:orgId/apps/:appIdGet an application and its environments.
PATCH/v1/orgs/:orgId/apps/:appIdRename an application: { name } (display name only — the slug is fixed). (admin)
DELETE/v1/orgs/:orgId/apps/:appIdDelete an application. (admin)
GET/v1/orgs/:orgId/apps/:appId/envsList environments.
POST/v1/orgs/:orgId/apps/:appId/envsCreate an environment (body includes the wrapped DEK). (admin)
GET/v1/orgs/:orgId/envs/:envIdGet an environment.
DELETE/v1/orgs/:orgId/envs/:envIdDelete an environment. (admin — or any grant-holder when it's a branch)
GET/v1/orgs/:orgId/envs/:envId/granteesThe 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.

MethodPathDescription
GET/v1/orgs/:orgId/envs/:envId/branchesList branches of an environment.
POST/v1/orgs/:orgId/envs/:envId/branchesFork this environment: { slug, name?, ttlSeconds?, wrappedDek, recoveryWrappedDek?, grants? }. envId is the parent.
GET/v1/orgs/:orgId/apps/:appId/branchesEvery branch in an application.
DELETE/v1/orgs/:orgId/envs/:branchIdTear 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.

MethodPathDescription
GET/v1/orgs/:orgId/groupsList groups.
POST/v1/orgs/:orgId/groupsCreate a group. (admin)
GET/v1/orgs/:orgId/groups/:groupIdGet a group and its environments.
PATCH/v1/orgs/:orgId/groups/:groupIdRename a group: { name } (display name only — the slug is fixed). (admin)
DELETE/v1/orgs/:orgId/groups/:groupIdDelete a group. (admin)
GET/v1/orgs/:orgId/groups/:groupId/envsList a group's environments.
POST/v1/orgs/:orgId/groups/:groupId/envsCreate a group environment (body includes the wrapped DEK). (admin)

Composition

Which groups an application environment pulls in, and their precedence.

MethodPathDescription
GET/v1/orgs/:orgId/envs/:envId/groupsList composed groups (slug, name, position).
POST/v1/orgs/:orgId/envs/:envId/groupsCompose a group { groupId, position? }. (admin)
DELETE/v1/orgs/:orgId/envs/:envId/groups/:groupIdRemove a composed group. (admin)

Resolve

MethodPathDescription
GET/v1/resolveThe 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.

note

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.

MethodPathDescription
GET/v1/orgs/:orgId/envs/:envId/secretsList secrets (with ciphertext).
PUT/v1/orgs/:orgId/envs/:envId/secrets/:nameCreate 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/restoreRoll back to an earlier version: { version }. Returns { secret, restoredFrom }.
DELETE/v1/orgs/:orgId/envs/:envId/secrets/:nameDelete a secret and its whole version history.
note

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

MethodPathDescription
GET/v1/orgs/:orgId/envs/:envId/keyThe calling principal's own wrapped DEK.
GET/v1/orgs/:orgId/envs/:envId/keysList all grants for the environment. (admin)
POST/v1/orgs/:orgId/envs/:envId/keysGrant a wrapped DEK to a principal. (admin)
DELETE/v1/orgs/:orgId/envs/:envId/keys/:grantIdRevoke 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.

MethodPathDescription
GET/v1/orgs/:orgId/kms/keysList keys the caller can see (admins: all org keys; members: keys they hold a grant for).
POST/v1/orgs/:orgId/kms/keysCreate 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/:keyIdKey metadata + every version (public keys for sign). (admin)
GET/v1/orgs/:orgId/kms/keys/:keyId/keyThe 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/publicPublished public keys of a sign key, per version. Grant-free within the org.
GET/v1/orgs/:orgId/kms/keys/:keyId/grantsGrantees, each with the versions they hold. (admin)
POST/v1/orgs/:orgId/kms/keys/:keyId/grantsGrant 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/rotateAdd a version and re-wrap for grantees: { publicKeyJwk? (sign only), grants: [{ principalType, principalId, wrappedKey }] }. (admin)
POST/v1/orgs/:orgId/kms/keys/:keyId/disableDisable a key — blocks all use (encrypt/decrypt/sign, new grants/rotations); grant rows retained. (admin)
DELETE/v1/orgs/:orgId/kms/keys/:keyIdSoft-delete a key — hides it from every listing and read path; row and versions retained for audit; name frees up for reuse. (admin)
note

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.

MethodPathDescription
GET/v1/orgs/:orgId/recoveryStatus: threshold, custodians, and environment coverage. (admin)
POST/v1/orgs/:orgId/recoveryEnable recovery: { recoveryPublicKeyJwk, threshold, shares[], grants[] }. (admin)
POST/v1/orgs/:orgId/recovery/rotateRotate to a fresh recovery key, custodian set, and env re-wraps. (admin)
DELETE/v1/orgs/:orgId/recoveryDisable recovery and delete every recovery grant. (admin)
POST/v1/orgs/:orgId/recovery/grantsBackfill recovery grants: { grants: [{ environmentId, wrappedDek }] }. (admin)
GET/v1/orgs/:orgId/recovery/shareThe caller's own wrapped recovery share (custodian only).
GET/v1/orgs/:orgId/recovery/env-keysEvery DEK wrapped to the recovery key, for the target to reconstruct. (admin)
GET/v1/orgs/:orgId/recovery/requestsList recovery ceremonies. (admin)
POST/v1/orgs/:orgId/recovery/requestsStart a ceremony: { targetPublicKeyJwk, targetType?, targetId?, reason? }. (admin)
GET/v1/orgs/:orgId/recovery/requests/:idA request plus its contributed (re-wrapped) shares.
POST/v1/orgs/:orgId/recovery/requests/:id/sharesA custodian contributes: { shareIndex, contributedShare }.
POST/v1/orgs/:orgId/recovery/requests/:id/completeTarget restores access: { principalType, principalId, grants[] }. (admin)
POST/v1/orgs/:orgId/recovery/requests/:id/cancelCancel an open request. (admin)
note

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

MethodPathDescription
GET/v1/orgs/:orgId/tokensList service tokens. (admin)
POST/v1/orgs/:orgId/tokensRegister 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/:tokenIdRevoke a token (idempotent). (admin)
DELETE/v1/orgs/:orgId/tokens/:tokenId/permanentDelete 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.

MethodPathDescription
GET/v1/orgs/:orgId/honey-tokensList 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-tokensRegister 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/:honeyTokenIdDelete 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).

MethodPathDescription
GET/v1/orgs/:orgId/leases/broker-keyThe org broker's public key. Wrap the admin credential to it before registering a target.
GET/v1/orgs/:orgId/leases/targetsList provisioning targets (never the wrapped admin secret's plaintext).
POST/v1/orgs/:orgId/leases/targetsRegister 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/:targetIdRemove a target.
GET/v1/orgs/:orgId/leasesList the lease ledger (status, resource ref, provider, expiry — no secret material).
POST/v1/orgs/:orgId/leasesMint 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/:leaseIdRevoke 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).
note

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.

MethodPathDescription
GET/v1/orgs/:orgId/rotation/rotator-keyThe 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/rotationList rotation policies: kind, cadence, status, last/next run, lastError. Never values.
POST/v1/orgs/:orgId/rotationConfigure (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/:rotationIdOne policy.
PATCH/v1/orgs/:orgId/rotation/:rotationIdChange intervalSeconds, config (same kind only), or status (active/paused). Resuming clears the failure streak.
POST/v1/orgs/:orgId/rotation/:rotationId/rotateRotate now — the same path the scheduler uses. Returns { rotation, version, rotatedAt }.
DELETE/v1/orgs/:orgId/rotation/:rotationIdRemove 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).
note

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

MethodPathDescription
GET/v1/orgs/:orgId/auditPaginated audit entries. (admin) Query: cursor, limit, action, resourceType.
note

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.

MethodPathDescription
GET/v1/orgs/:orgId/billingThe org's plan and entitlements. Returns { subscription, plan, promo, entitlements[], usage[], overrides[], enforced, manage }. Readable by any member.
POST/v1/orgs/:orgId/billing/checkoutStart a Checkout Session to upgrade. Body { family } (a plan family). Returns { url } to redirect the browser to. (admin)
POST/v1/orgs/:orgId/billing/portalOpen the Billing Portal to manage/update payment. Returns { url }. (admin)
POST/v1/orgs/:orgId/billing/cancelDowngrade 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/promoRedeem 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 }.

note

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.

MethodPathDescription
GET/v1/orgs/:orgId/log-sinkGet 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-sinkConfigure 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-sinkRemove the sink. (admin)
POST/v1/orgs/:orgId/log-sink/testSynchronously POST a synthetic record to the endpoint and return { ok, status, error }. (admin)
note

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.

MethodPathDescription
GET/v1/orgs/:orgId/agentsList agent identities: name, slug, bound environment, live policy version, and when a proxy last fetched it. (admin)
POST/v1/orgs/:orgId/agentsCreate 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/:agentIdOne identity, plus its bound environment's slug. (admin)
PATCH/v1/orgs/:orgId/agents/:agentIdUpdate { 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/:agentIdDelete the identity and its published versions. Audits agent.deleted. (admin)
GET/v1/orgs/:orgId/agents/:agentId/policiesPublished versions (newest first, 50 max), each with its signed bundle, signer thumbprint, expiry, and decoded rules. (admin)
POST/v1/orgs/:orgId/agents/:agentId/policiesPublish { 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/rollbackRepublish 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/meYour own signing thumbprint (RFC 7638), for the trust-anchor snippet: { "signer": { userId, thumbprint } | null }. (admin)
GET/v1/agents/:agentRef/policyThe 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.
caution

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.

MethodPathDescription
GET/v1/orgs/:orgId/sync/connectionsList sync connections. Returns publicKeyJwk and status; never the wrapped credential. (admin)
GET/v1/orgs/:orgId/sync/connections/:connectionId/public-keyMint (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/connectionsCreate 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/verifyTest the stored credential against { destination }. Returns { ok: true } or 400 { ok: false, error }. (admin)
DELETE/v1/orgs/:orgId/sync/connections/:connectionIdDelete the connection, its bindings, and every sync key grant made to it, then destroy its keypair. (admin)
GET/v1/orgs/:orgId/sync/bindingsList bindings with their destination, filters, and last-run status. (admin)
POST/v1/orgs/:orgId/sync/bindingsEnable 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/:bindingIdUpdate 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/:bindingIdDelete 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/runPush now, synchronously. Returns { run: { id, status, pushed[], deleted[], failures[], error } }. (admin)
GET/v1/orgs/:orgId/sync/runsRun history (optionally ?bindingId=), newest first. Carries destination key names, counts, and error strings — never values. (admin)
caution

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.