ShareShield

API v2 reference

Create, reveal and manage self-destructing secrets and files from scripts, CI and integrations.

Machine-readable spec: /api/v2/openapi.json (OpenAPI 3.1).

Authentication

Send an API key in the x-api-key header. Create keys under Dashboard → Profile → API keys. A key:

  • looks like shs_ + 64 hex characters and is shown once; ShareShield stores only its hash. The dashboard shows its prefix, e.g. shs_ab12cd34…;
  • is bound to one organization, the one that was active when it was created, and stops working if its owner leaves that organization;
  • may expire (1–365 days) and can be revoked at any time by its owner or an organization admin. Revoked and expired keys get 401.

Set two shell variables to use the examples below:

export SHARESHIELD_URL=https://app.shareshield.net   # your ShareShield address
export SHARESHIELD_API_KEY=shs_…

curl -sS "$SHARESHIELD_URL/api/v2/me" -H "x-api-key: $SHARESHIELD_API_KEY"

The web app's browser session also works for same-origin calls. Endpoints marked public need no credentials; auth optional endpoints accept anonymous callers, but a present and invalid key is always rejected.

Key scopes

What a key can do is the intersectionof its scopes and its owner's role in the organization. A scope never grants more than the role allows (a viewer's key can't create secrets even with secrets:create), and a missing scope returns 403 INSUFFICIENT_SCOPE. New keys default to every scope your role allows. Account, team, billing and key management are never available to API keys.

ScopeAllowsRequires role permission
secrets:createCreate self-destructing secrets and files in the organization.secrets:create
secrets:readReveal (and so consume a view of) a secret by its id.secrets:read
secrets:burnDestroy secrets the key owner created.secrets:create
secrets:listList metadata of the key owner's secrets (never their content).secrets:read
requests:createAsk someone to send you a secret.secrets:create
requests:readList the key owner's secret requests.secrets:read
audit:readRead the organization's audit log (owners and admins).audit:read

Owners and admins can use every scope; members every scope except audit:read; viewers secrets:read, secrets:list and requests:read.

Errors, pagination & versioning

  • Requests and responses are JSON. A request body must be sent as content-type: application/json (otherwise 415); unknown fields are rejected. The exceptions are file upload and fulfilling a request with a file, which are multipart/form-data; file downloads return raw bytes.
  • Expiry is expiryHours or expiryDays; expiryHourswins when both are sent, and with neither your default applies. Limits are your plan ∩ your organization's policy (see GET /options): going over a plan ceiling is 402 PAYMENT_REQUIRED (upgrading fixes it), over a policy ceiling 403 FORBIDDEN.
  • Every response carries X-API-Version: 2.
  • Errors always look like { "error": "…", "code": "VALIDATION_ERROR", "details": … }. code is stable; details is optional.
  • Lists return { "data": [ … ], "nextCursor": "…" | null }, newest first. Pass nextCursor back as ?cursor= to get the next page; limit is 1–100 (default 20).
  • API v1 is deprecated. v1 responses to API keys carry Deprecation: true, a Link to this page and, once scheduled, a Sunset date.

Error codes

Branch on code, not on the message. The same status can carry different codes (for example 403 FORBIDDEN vs 403 INSUFFICIENT_SCOPE, or 400 FILE_SECRET vs 400 VALIDATION_ERROR).

StatusCodeMeaning
400VALIDATION_ERRORBody, query, path parameter or options part failed validation (unknown fields included); details.issues lists each problem as { path, message }
400INVALID_CURSORThe cursor is not one a previous page returned. Start again without it
400BAD_REQUESTMalformed JSON or multipart body, missing file part, empty file, unknown or repeated form part
400FILE_SECRETReveal called on a file secret; no view was taken. Use POST /secrets/{id}/download (details.download)
400NOT_A_FILEDownload called on a text secret; no view was taken. Use POST /secrets/{id}/reveal (details.reveal)
400INVALID_CODERecipient code is wrong or expired, or no code is pending for the address (the same answer whether or not the address is a recipient). Request a new code
401UNAUTHORIZEDMissing, invalid, expired or revoked credentials; or an anonymous create that sends recipients (sign in to email recipients)
402PAYMENT_REQUIREDYour plan does not allow it: monthly quota used up, expiry or views above the plan ceiling, or files on a free, trial or past-due plan. Upgrading fixes it
403FORBIDDENYour role or your organization's policy does not allow it (expiry or views above the policy ceiling, passcode required, files disabled, a recipient outside the allowed domains: details.disallowedDomains), a wrong passcode, or a wrong burn token
403TWO_FACTOR_REQUIREDSession callers only: your organization requires two-factor authentication and you have not enrolled yet (details.setupPath). API keys are not affected
403CROSS_SITE_REQUESTSession callers only: a state-changing request came from another site. API keys are not affected
403INSUFFICIENT_SCOPEThe API key lacks the scope the endpoint needs (details.requiredScope)
403RECIPIENT_VERIFICATION_REQUIREDRecipients-only secret: sign in as a recipient, or get a recipientToken via POST /secrets/{id}/recipient-code and …/confirm and send it as X-Recipient-Token. No view was taken
404NOT_FOUNDNo such secret or request, or (rename, burn) a secret that is not yours
409CONFLICTThe request was already fulfilled
410GONEAlready viewed, burned (by its owner, its recipient or passcode lockout) or expired
413PAYLOAD_TOO_LARGEThe file is over your plan's file size limit (GET /options → maxFileSizeMb), or a JSON body is over 1 MB
415UNSUPPORTED_MEDIA_TYPEBody is not application/json (or not multipart/form-data for a file upload)
429TOO_MANY_REQUESTSRate limited; wait for the Retry-After header (seconds, also details.retryAfterSeconds)
429CODE_LOCKEDToo many wrong recipient codes: the code was discarded. Request a new one with POST /secrets/{id}/recipient-code. After 20 wrong codes in total for an address, codes are disabled for it on that secret (details.codesDisabled): the recipient signs in with that address instead
500INTERNAL_ERRORUnexpected server error (the message is generic)

Rate limits

Exceeding a limit returns 429 TOO_MANY_REQUESTS.

  • Anonymous secret creation: 2 per IP per UTC day.
  • Reveal, download, metadata and burn link: 30 attempts per IP per minute (shared).
  • Request creation: 20 per user per hour and 100 per organization per day.
  • Request fulfilment (text or file): 10 per IP per hour.
  • Recipient emails: 100 recipients per user per hour and 500 per organization per day (each recipient counts once).
  • Recipient codes: 10 per IP per hour and 10 per secret per hour. Confirming a code shares the reveal limit.

Secrets

post/api/v2/secretsauth optionalsecrets:create

Create a secret

Encrypts `content` and returns a one-time share link plus a recipient burn link. Anonymous callers are allowed (rate limited per IP); signed-in callers count against their plan quota. Expiry: send `expiryHours` or `expiryDays`. `expiryHours` wins when both are sent; with neither, your effective default applies (`GET /options`). Limits are plan ∩ organization policy: above a plan ceiling is 402, above a policy ceiling 403. Recipients: `recipients` (up to 10 addresses) emails each one the link; the response reports delivery per address in `recipients` and echoes `recipientsOnly`. With `recipientsOnly`, only a signed-in recipient or a caller holding a `recipientToken` (see `POST /secrets/{id}/recipient-code`) can open it. Signed-in callers only (anonymous: 401); your organization's recipient domain allow-list applies (403). Recipient emails count against per-user and per-organization rate limits (429).

Rate limit: Anonymous: 2 per IP per UTC day.

Request body application/json

content*stringmax 65536 chars
The secret text
labelstringmax 120 chars
Your private name for the secret (e.g. "Prod DB for Sam"), shown in your lists and owner emails, never to recipients. Signed-in callers and API keys only
expiryHoursinteger1–2160
Lifetime in hours (1-2160). Takes precedence over `expiryDays` when both are sent. Default: your organization's default (`GET /options` → `defaultExpiryHours`)
expiryDaysinteger1–90
Lifetime in days (1-90). Ignored when `expiryHours` is sent
maxViewsinteger1–100
Reveals allowed before the secret burns (1-100). Default: `GET /options` → `defaultMaxViews` (usually 1)
passwordstringmax 256 chars
Passcode the recipient must supply to reveal the secret
requireSignInbooleandefault false
Only signed-in ShareShield users (or API keys) can reveal
notifyOnOpenbooleandefault false
Email when the secret is opened
notifyEmailstring (email)max 254 chars
Where to send the open notification (default: your email)
notifyEmailsstring (email)[]
Up to 5 addresses to notify when the secret is opened (merged with `notifyEmail`, deduplicated). Sending any enables the notification
recipientsstring (email)[]
Email the share link to up to 10 addresses (deduplicated case-insensitively). Signed-in callers only (anonymous: 401). Your organization's recipient domain allow-list applies (403 `details.disallowedDomains`; see `GET /options` → `allowedRecipientDomains`). Delivery status per address is returned in `recipients`
recipientsOnlyboolean
Only the listed recipients can open it: signed in as a recipient, or with a `recipientToken` from `POST /secrets/{id}/recipient-code/confirm`. Needs at least one recipient. Default false

Response 201 Secret created

id*string
Public secret id as in the share URL, e.g. `k7mq-2vx-r9t` (10 characters, grouped)
url*string (uri)
Share link for the recipient
burnUrlstring (uri)
Burn link: lets the recipient destroy the secret without viewing it. Returned only here, at create; the token is not stored in clear and cannot be recovered
expiresAt*string (date-time)
ISO 8601 timestamp (UTC)
maxViews*integer-9007199254740991–9007199254740991
Reveals allowed (the effective value after defaults)
recipientsobject[]
Present only when `recipients` were sent: delivery status per address
email*string
Normalized (lower-cased) recipient address
sent*boolean
The link email was handed to the mail provider. false: sending failed; the secret was still created
recipientsOnlyboolean
Present only when `recipients` were sent

Errors

  • 401 Invalid, expired or revoked API key; or anonymous with `recipients` (UNAUTHORIZED)
  • 402 Plan quota reached, or expiry/maxViews above your plan ceiling (PAYMENT_REQUIRED)
  • 403 Missing scope `secrets:create` (INSUFFICIENT_SCOPE); expiry/maxViews above your organization's policy ceiling, a passcode the policy requires is missing, or a recipient outside the allowed domains (FORBIDDEN; `details.disallowedDomains`)
  • 415 Body is not JSON
  • 429 Anonymous daily limit reached, or recipient email limit reached

Example

curl -sS -X POST "$SHARESHIELD_URL/api/v2/secrets" \
  -H "x-api-key: $SHARESHIELD_API_KEY" \
  -H "content-type: application/json" \
  -d '{"content":"db-password: hunter2","expiryHours":4,"maxViews":1}'
post/api/v2/secrets/filesauth requiredsecrets:create

Create a file secret

Uploads a file as a one-time secret. The only v2 endpoint that takes `multipart/form-data`: a `file` part and an optional `options` part holding JSON with the same options as text create (no `content`; unknown fields are rejected). Needs an active paid plan (free, trial and past-due organizations get 402) and an organization policy that allows files (403). Files over the plan's limit (`GET /options` → `maxFileSizeMb`) get 413, checked from Content-Length before the body is read. Recipients: `recipients` (up to 10 addresses) emails each one the link; the response reports delivery per address in `recipients` and echoes `recipientsOnly`. With `recipientsOnly`, only a signed-in recipient or a caller holding a `recipientToken` (see `POST /secrets/{id}/recipient-code`) can open it. Signed-in callers only (anonymous: 401); your organization's recipient domain allow-list applies (403). Recipient emails count against per-user and per-organization rate limits (429).

Request body multipart/form-data

  • file *: the file (binary part).
  • options: optional JSON text with these fields:
labelstringmax 120 chars
Your private name for the secret (e.g. "Prod DB for Sam"), shown in your lists and owner emails, never to recipients. Signed-in callers and API keys only
expiryHoursinteger1–2160
Lifetime in hours (1-2160). Takes precedence over `expiryDays` when both are sent. Default: your organization's default (`GET /options` → `defaultExpiryHours`)
expiryDaysinteger1–90
Lifetime in days (1-90). Ignored when `expiryHours` is sent
maxViewsinteger1–100
Reveals allowed before the secret burns (1-100). Default: `GET /options` → `defaultMaxViews` (usually 1)
passwordstringmax 256 chars
Passcode the recipient must supply to reveal the secret
requireSignInbooleandefault false
Only signed-in ShareShield users (or API keys) can reveal
notifyOnOpenbooleandefault false
Email when the secret is opened
notifyEmailstring (email)max 254 chars
Where to send the open notification (default: your email)
notifyEmailsstring (email)[]
Up to 5 addresses to notify when the secret is opened (merged with `notifyEmail`, deduplicated). Sending any enables the notification
recipientsstring (email)[]
Email the share link to up to 10 addresses (deduplicated case-insensitively). Signed-in callers only (anonymous: 401). Your organization's recipient domain allow-list applies (403 `details.disallowedDomains`; see `GET /options` → `allowedRecipientDomains`). Delivery status per address is returned in `recipients`
recipientsOnlyboolean
Only the listed recipients can open it: signed in as a recipient, or with a `recipientToken` from `POST /secrets/{id}/recipient-code/confirm`. Needs at least one recipient. Default false

Response 201 File secret created

id*string
Public secret id as in the share URL, e.g. `k7mq-2vx-r9t` (10 characters, grouped)
url*string (uri)
Share link for the recipient
burnUrlstring (uri)
Burn link: lets the recipient destroy the secret without viewing it. Returned only here, at create; the token is not stored in clear and cannot be recovered
expiresAt*string (date-time)
ISO 8601 timestamp (UTC)
maxViews*integer-9007199254740991–9007199254740991
Reveals allowed (the effective value after defaults)
recipientsobject[]
Present only when `recipients` were sent: delivery status per address
email*string
Normalized (lower-cased) recipient address
sent*boolean
The link email was handed to the mail provider. false: sending failed; the secret was still created
recipientsOnlyboolean
Present only when `recipients` were sent
kind*"file"
fileName*string
The stored (sanitized) file name the recipient downloads
sizeBytes*integer-9007199254740991–9007199254740991
File size in bytes

Errors

  • 402 File sharing not on your plan, a trial, or billing inactive; plan quota reached; expiry/maxViews above your plan ceiling (PAYMENT_REQUIRED)
  • 403 Missing scope `secrets:create` (INSUFFICIENT_SCOPE); files disabled by your organization's policy, expiry/maxViews above its ceiling, or a recipient outside the allowed domains (FORBIDDEN; `details.disallowedDomains`)
  • 413 File over your plan's size limit (PAYLOAD_TOO_LARGE)
  • 415 Body is not multipart/form-data
  • 429 Recipient email limit reached

Example

curl -sS -X POST "$SHARESHIELD_URL/api/v2/secrets/files" \
  -H "x-api-key: $SHARESHIELD_API_KEY" \
  -F [email protected] \
  -F 'options={"expiryHours":24,"recipients":["[email protected]"],"recipientsOnly":true}'
get/api/v2/secretsauth requiredsecrets:list

List your secrets

Metadata of the secrets you created in this organization, newest first. Never includes content. Recipient addresses are not listed: `recipientCount` and `recipientsOnly` summarize them.

Query parameters

cursorstringmax 256 chars
Opaque cursor from a previous page’s `nextCursor`
limitinteger1–100default 20
Page size (1-100)
status"active" | "viewed" | "expired" | "burned"
active: can be revealed · viewed: opened at least once · expired · burned: destroyed by its owner, its recipient (burn link) or passcode lockout

Response 200 A page of secrets

data*object[]
id*string
url*string (uri)
label*string | null
Your private name for the secret, if any
status*"active" | "viewed" | "expired" | "burned"
active: can be revealed · viewed: opened at least once · expired · burned: destroyed by its owner, its recipient (burn link) or passcode lockout
kind*"text" | "file" | null
text or file; null once the content has been destroyed (the payload, and with it the file name and size, is deleted)
fileNamestring
File secrets whose content still exists
sizeBytesinteger-9007199254740991–9007199254740991
File secrets whose content still exists
createdAt*string (date-time)
ISO 8601 timestamp (UTC)
expiresAt*string (date-time)
ISO 8601 timestamp (UTC)
maxViews*integer-9007199254740991–9007199254740991
viewsRemaining*integer-9007199254740991–9007199254740991
0 once burned, viewed for the last time or expired
isPasswordProtected*boolean
A passcode is set
requireSignIn*boolean
recipientsOnly*boolean
Only its email recipients can open it
recipientCount*integer-9007199254740991–9007199254740991
Email recipients the link was sent to (addresses are not listed)
openedAt*string (date-time) | null
burnedAt*string (date-time) | null
burnReason*"viewed" | "owner" | "recipient" | "passcode_lockout" | "expired" | null
fromRequest*boolean
Created by fulfilling one of your secret requests
nextCursor*string | null
Pass as `cursor` to fetch the next page; null on the last page

Errors

  • 400 Invalid cursor or filter

Example

curl -sS -X GET "$SHARESHIELD_URL/api/v2/secrets?limit=20" \
  -H "x-api-key: $SHARESHIELD_API_KEY"
get/api/v2/secrets/{id}auth optionalsecrets:read

Get secret metadata

What a recipient can know before revealing: text or file (with name and size), whether a passcode, sign-in or recipient verification is needed, views left and expiry. Does **not** consume a view and never returns content. Public (anyone holding the id); API keys need `secrets:read`.

Rate limit: 30 per IP per minute (shared with reveal and download).

Path parameters

  • id: Public secret id (the last segment of the share URL)

Response 200 Secret metadata

kind*"text" | "file"
text: reveal it · file: download it
fileNamestring
File secrets only
sizeBytesinteger-9007199254740991–9007199254740991
File secrets only
requiresPassword*boolean
A passcode must be sent to reveal/download
requiresAuth*boolean
Only signed-in users or API keys can reveal/download
requiresRecipientVerification*boolean
Recipients-only: reveal/download needs a signed-in recipient or a `recipientToken` (`POST /secrets/{id}/recipient-code`), otherwise 403 RECIPIENT_VERIFICATION_REQUIRED
viewsRemaining*integer-9007199254740991–9007199254740991
expiresAt*string (date-time)
ISO 8601 timestamp (UTC)

Errors

  • 404 No such secret
  • 410 Already viewed, burned or expired
  • 429 Too many attempts

Example

curl -sS -X GET "$SHARESHIELD_URL/api/v2/secrets/$ID" \
  -H "x-api-key: $SHARESHIELD_API_KEY"
post/api/v2/secrets/{id}/revealauth optionalsecrets:read

Reveal a secret

Decrypts a text secret and consumes one view; the last view burns it. Public (anyone holding the id); API keys need `secrets:read`. Responses are `Cache-Control: no-store`. The body is optional. File secrets return 400 `FILE_SECRET` without taking a view: download them instead. Recipients-only secrets (`GET /secrets/{id}` → `requiresRecipientVerification`) need a signed-in recipient or a `recipientToken`, as the `X-Recipient-Token` header or in the body.

Rate limit: 30 per IP per minute.

Path parameters

  • id: Public secret id (the last segment of the share URL)

Headers

  • X-Recipient-Token (optional): Recipients-only secrets: the recipientToken from POST /secrets/{id}/recipient-code/confirm (alternative to the body field; the header wins)

Request body application/json, optional

passwordstringmax 256 chars
Passcode the recipient must supply to reveal the secret
recipientTokenstringmax 1024 chars
Recipients-only secrets: the token from `POST /secrets/{id}/recipient-code/confirm`. May be sent as the `X-Recipient-Token` header instead (the header wins)

Response 200 The secret content

content*string
The decrypted secret text
viewsRemaining*integer-9007199254740991–9007199254740991
Views left after this one; 0 means the secret is now burned

Errors

  • 400 Validation failed (VALIDATION_ERROR), or the secret is a file (FILE_SECRET; `details.download` is the download path)
  • 403 Recipients-only secret without a signed-in recipient or valid `recipientToken` (RECIPIENT_VERIFICATION_REQUIRED; no view taken); password required or incorrect (FORBIDDEN; 5 wrong attempts burn the secret); or missing scope (INSUFFICIENT_SCOPE)
  • 404 No such secret
  • 410 Already viewed, burned or expired
  • 429 Too many attempts

Example

curl -sS -X POST "$SHARESHIELD_URL/api/v2/secrets/$ID/reveal" \
  -H "x-api-key: $SHARESHIELD_API_KEY" \
  -H "content-type: application/json" \
  -d '{"password":"correct horse"}'
post/api/v2/secrets/{id}/downloadauth optionalsecrets:read

Download a file secret

Returns the file bytes and consumes one view, like reveal (same optional `{ "password" }` body). Always `application/octet-stream` with `Content-Disposition: attachment`, `X-Content-Type-Options: nosniff` and `Cache-Control: no-store`; never rendered inline. Text secrets return 400 `NOT_A_FILE` without taking a view. Recipients-only secrets need a `recipientToken`, as for reveal.

Rate limit: 30 per IP per minute.

Path parameters

  • id: Public secret id (the last segment of the share URL)

Headers

  • X-Recipient-Token (optional): Recipients-only secrets: the recipientToken from POST /secrets/{id}/recipient-code/confirm (alternative to the body field; the header wins)

Request body application/json, optional

passwordstringmax 256 chars
Passcode the recipient must supply to reveal the secret
recipientTokenstringmax 1024 chars
Recipients-only secrets: the token from `POST /secrets/{id}/recipient-code/confirm`. May be sent as the `X-Recipient-Token` header instead (the header wins)

Response 200 The file

The raw file bytes, application/octet-stream. Headers:

  • Content-Disposition: `attachment; filename="…"; filename*=UTF-8''…` with the stored file name
  • X-Views-Remaining: Views left after this one; 0 means the secret is now burned
  • X-Content-Type-Options: `nosniff`
  • Cache-Control: `no-store`

Errors

  • 400 Validation failed (VALIDATION_ERROR), or the secret is text (NOT_A_FILE; `details.reveal` is the reveal path)
  • 403 Recipients-only secret without a signed-in recipient or valid `recipientToken` (RECIPIENT_VERIFICATION_REQUIRED; no view taken); password required or incorrect (FORBIDDEN; 5 wrong attempts burn the secret); or missing scope (INSUFFICIENT_SCOPE)
  • 404 No such secret
  • 410 Already viewed, burned or expired
  • 429 Too many attempts

Example

curl -sS -X POST "$SHARESHIELD_URL/api/v2/secrets/$ID/download" \
  -H "x-api-key: $SHARESHIELD_API_KEY" \
  -OJ
post/api/v2/secrets/{id}/burnpublic

Burn a secret with its burn link

Lets a recipient destroy a secret without viewing it, using the `token` from the `burnUrl` returned at create. No authentication: the token is the credential. Afterwards the secret reveals as 410.

Rate limit: 30 per IP per minute.

Path parameters

  • id: Public secret id (the last segment of the share URL)

Request body application/json

token*stringmax 256 chars
The `token` query parameter of the `burnUrl` returned at create

Response 204 Burned

No body.

Errors

  • 403 Wrong burn token (FORBIDDEN)
  • 404 No such secret
  • 410 Already viewed, burned or expired
  • 415 Body is not JSON
  • 429 Too many attempts

Example

curl -sS -X POST "$SHARESHIELD_URL/api/v2/secrets/$ID/burn" \
  -H "content-type: application/json" \
  -d '{"token":"TOKEN_FROM_BURN_URL"}'
patch/api/v2/secrets/{id}auth requiredsecrets:create

Rename a secret

Sets or clears your private name for a secret you own (`label`; null or empty clears it). Works at any point in its life, including after it was viewed or burned. The name is never shown to recipients.

Rate limit: 20 misses (404) per IP per hour, shared with every lookup by id.

Path parameters

  • id: Public secret id (the last segment of the share URL)

Request body application/json

label*string | null

Response 200 The new name

label*string | null

Errors

  • 404 No such secret, or not yours
  • 429 Too many ids that do not exist were tried

Example

curl -sS -X PATCH "$SHARESHIELD_URL/api/v2/secrets/$ID" \
  -H "x-api-key: $SHARESHIELD_API_KEY" \
  -H "content-type: application/json" \
  -d '{"label":"Prod DB for Sam"}'
delete/api/v2/secrets/{id}auth requiredsecrets:burn

Burn a secret

Destroys the content of a secret you own. Its metadata stays in your list with status `burned`.

Rate limit: 20 misses (404) per IP per hour, shared with every lookup by id.

Path parameters

  • id: Public secret id (the last segment of the share URL)

Response 204 Burned

No body.

Errors

  • 404 No such secret, or not yours
  • 429 Too many ids that do not exist were tried

Example

curl -sS -X DELETE "$SHARESHIELD_URL/api/v2/secrets/$ID" \
  -H "x-api-key: $SHARESHIELD_API_KEY"
get/api/v2/optionsauth optional

Your limits and defaults

The effective create limits and defaults for these credentials (plan ∩ organization policy; anonymous callers get the free plan). Use it to pre-validate expiry, views, file size and recipients (`maxRecipients`, `allowedRecipientDomains`): `limitedBy` says whether exceeding a ceiling is 402 (plan) or 403 (policy).

Response 200 Effective limits

maxExpiryHours*integer-9007199254740991–9007199254740991
Longest allowed lifetime; above it create returns 402 (plan) or 403 (policy)
defaultExpiryHours*integer-9007199254740991–9007199254740991
Used when neither expiryHours nor expiryDays is sent
maxViews*integer-9007199254740991–9007199254740991
defaultMaxViews*integer-9007199254740991–9007199254740991
requirePasscode*boolean
The org policy requires a password on every secret (403 otherwise)
allowAnonymousViewers*boolean
false: every secret is created as sign-in only
filesAllowed*boolean
maxFileSizeMb*integer-9007199254740991–9007199254740991
0 when files are not allowed
filesBlockedReason"anonymous" | "plan" | "trial" | "billing_inactive" | "policy"
Why files are not allowed (anonymous: 401 · plan, trial, billing_inactive: 402 · policy: 403)
defaultNotifyOnOpen*boolean
allowSecretRequests*boolean
allowedRecipientDomains*string[]
Recipient domain allow-list from the org policy (subdomains match); empty means any domain. Others get 403 with `details.disallowedDomains`
maxRecipients*integer-9007199254740991–9007199254740991
Most `recipients` per secret; 0 for anonymous callers (who cannot add any)
limitedBy*object
Which side sets each ceiling: plan (exceeding it is 402) or policy (403)
maxExpiryHours*"plan" | "policy"
maxViews*"plan" | "policy"

Example

curl -sS -X GET "$SHARESHIELD_URL/api/v2/options" \
  -H "x-api-key: $SHARESHIELD_API_KEY"

Recipients

post/api/v2/secrets/{id}/recipient-codepublic

Email a recipient verification code

For recipients-only secrets: emails an 8-digit code (valid 10 minutes) to `email` if it is one of the secret’s recipients. No authentication. Always 202 for a live secret, whether or not the address is a recipient, so recipients cannot be enumerated. A new code replaces a pending one, except within 60 seconds of sending it (the request is accepted, but the code already sent stays the valid one). Exchange it at `POST /secrets/{id}/recipient-code/confirm`.

Rate limit: 10 per IP per hour and 10 per secret per hour.

Path parameters

  • id: Public secret id (the last segment of the share URL)

Request body application/json

email*string
The recipient address to send the 8-digit code to

Response 202 Accepted (a code is sent if the address is a recipient)

accepted*true
message*string
The same whether or not the address is a recipient

Errors

  • 404 No such secret
  • 410 Already viewed, burned or expired
  • 415 Body is not JSON
  • 429 Too many codes requested

Example

curl -sS -X POST "$SHARESHIELD_URL/api/v2/secrets/$ID/recipient-code" \
  -H "content-type: application/json" \
  -d '{"email":"[email protected]"}'
post/api/v2/secrets/{id}/recipient-code/confirmpublic

Exchange a recipient code for a token

Exchanges the emailed code for a `recipientToken` (valid 15 minutes, bound to this secret and address). Send it to reveal or download as the `X-Recipient-Token` header or body `recipientToken`. No authentication. A code works once; each wrong attempt uses one of its 5 attempts and the 5th discards it (429 `CODE_LOCKED`). After 20 wrong codes in total for an address, it can no longer be verified by code for this secret (429 `CODE_LOCKED` with `details.codesDisabled`); signing in with that address still opens the secret.

Rate limit: 30 per IP per minute (shared with reveal and download).

Path parameters

  • id: Public secret id (the last segment of the share URL)

Request body application/json

email*string
The address the code was sent to
code*string
The 8-digit code from the email

Response 200 Verified

recipientToken*string
Send as the `X-Recipient-Token` header (or body `recipientToken`) to reveal or download. Bound to this secret and address
expiresAt*string (date-time)
When the token stops working (15 minutes)

Errors

  • 400 Validation failed (VALIDATION_ERROR), or the code is wrong or expired (INVALID_CODE)
  • 404 No such secret
  • 410 Already viewed, burned or expired
  • 415 Body is not JSON
  • 429 Too many wrong codes, the code was discarded (CODE_LOCKED); or rate limited (TOO_MANY_REQUESTS)

Example

curl -sS -X POST "$SHARESHIELD_URL/api/v2/secrets/$ID/recipient-code/confirm" \
  -H "content-type: application/json" \
  -d '{"email":"[email protected]","code":"123456"}'

Requests

post/api/v2/requestsauth requiredrequests:create

Create a secret request

Creates a link someone can use to send you a secret. With `recipientEmail`, the link is emailed to them.

Rate limit: 20 per user per hour; 100 per organization per day.

Request body application/json

recipientEmailstring (email)max 254 chars
Email the request link to this address
messagestringmax 500 chars
Shown to the person fulfilling the request
expiryDaysinteger1–90default 3
How long the request link stays open (days)
notifyOnFulfillbooleandefault true
Email you the secret link when fulfilled

Response 201 Request created (`id`, `token`, `url`)

id*string
Request id (stable; appears in audit events)
token*string
Request token (as in the request URL)
url*string (uri)
Link to send to the person who holds the secret

Errors

  • 415 Body is not JSON
  • 429 Request limit reached

Example

curl -sS -X POST "$SHARESHIELD_URL/api/v2/requests" \
  -H "x-api-key: $SHARESHIELD_API_KEY" \
  -H "content-type: application/json" \
  -d '{"recipientEmail":"[email protected]","message":"Please send the staging DB password","expiryDays":3}'
get/api/v2/requestsauth requiredrequests:read

List your secret requests

Query parameters

cursorstringmax 256 chars
Opaque cursor from a previous page’s `nextCursor`
limitinteger1–100default 20
Page size (1-100)
status"open" | "fulfilled" | "expired"
open: waiting for a secret · fulfilled · expired (never fulfilled)

Response 200 A page of requests

data*object[]
token*string
url*string (uri)
status*"open" | "fulfilled" | "expired"
open: waiting for a secret · fulfilled · expired (never fulfilled)
recipientEmail*string | null
message*string | null
createdAt*string (date-time)
ISO 8601 timestamp (UTC)
expiresAt*string (date-time)
ISO 8601 timestamp (UTC)
fulfilledAt*string (date-time) | null
nextCursor*string | null
Pass as `cursor` to fetch the next page; null on the last page

Errors

  • 400 Invalid cursor or filter

Example

curl -sS -X GET "$SHARESHIELD_URL/api/v2/requests?limit=20" \
  -H "x-api-key: $SHARESHIELD_API_KEY"
get/api/v2/requests/{token}public

Get a request (public)

What the person fulfilling a request sees, including what the requester's organization accepts: `requirePasscode` (send a `password`), `filesAllowed` and `maxFileSizeMb` (for `fulfil-file`). No authentication.

Rate limit: 20 misses (404) per IP per hour, shared with every lookup by id.

Path parameters

  • token: Request token (the last segment of the request URL)

Response 200 Request details

requesterName*string
requesterEmail*string
message*string | null
createdAt*string (date-time)
ISO 8601 timestamp (UTC)
expiresAt*string (date-time)
ISO 8601 timestamp (UTC)
requirePasscode*boolean
The requester's organization requires a `password` on the response (403 otherwise)
filesAllowed*boolean
The response can be a file (`POST /requests/{token}/fulfil-file`)
maxFileSizeMb*integer-9007199254740991–9007199254740991
Largest file the requester accepts; 0 when files are not allowed

Errors

  • 404 No such request
  • 409 Already fulfilled
  • 410 Expired
  • 429 Too many links that do not exist were tried

Example

curl -sS -X GET "$SHARESHIELD_URL/api/v2/requests/$TOKEN"
post/api/v2/requests/{token}/fulfilpublic

Fulfil a request (public)

Creates a secret owned by the requester and emails them its link. No authentication.

Rate limit: 10 per IP per hour.

Path parameters

  • token: Request token (the last segment of the request URL)

Request body application/json

content*stringmax 65536 chars
The secret text
expiryDaysinteger1–90default 1
Lifetime of the created secret (days)
passwordstringmax 256 chars
Passcode the recipient must supply to reveal the secret

Response 201 Fulfilled

fulfilled*true
message*string

Errors

  • 402 Requester's plan quota reached
  • 403 A passcode the requester's organization requires is missing: send `password` (FORBIDDEN; `GET /requests/{token}` → `requirePasscode`)
  • 404 No such request
  • 409 Already fulfilled
  • 410 Expired
  • 415 Body is not JSON
  • 429 Too many fulfilments

Example

curl -sS -X POST "$SHARESHIELD_URL/api/v2/requests/$TOKEN/fulfil" \
  -H "content-type: application/json" \
  -d '{"content":"staging-db: s3cr3t","expiryDays":1}'
post/api/v2/requests/{token}/fulfil-filepublic

Fulfil a request with a file (public)

Like `fulfil`, but the response is a file: `multipart/form-data` with a `file` part and an optional `options` part holding JSON `{ expiryDays?, password? }` (unknown fields are rejected). No authentication. Gated on the **requester's** organization (the fulfiller is anonymous): an active paid plan (free, trial and past-due get 402), a policy that allows files (403) and its file size limit (413; `GET /requests/{token}` → `maxFileSizeMb`). Eligibility is checked before the body is read.

Rate limit: 10 per IP per hour (shared with fulfil).

Path parameters

  • token: Request token (the last segment of the request URL)

Request body multipart/form-data

  • file *: the file (binary part).
  • options: optional JSON text with these fields:
expiryDaysinteger1–90default 1
Lifetime of the created secret (days)
passwordstringmax 256 chars
Passcode the recipient must supply to reveal the secret

Response 201 Fulfilled

fulfilled*true
message*string

Errors

  • 402 Files not on the requester's plan, a trial, or billing inactive; or the requester's quota is reached (PAYMENT_REQUIRED)
  • 403 Files disabled by the requester's policy, or a passcode it requires is missing (FORBIDDEN)
  • 404 No such request
  • 409 Already fulfilled
  • 410 Expired
  • 413 File over the requester's size limit (PAYLOAD_TOO_LARGE)
  • 415 Body is not multipart/form-data
  • 429 Too many fulfilments

Example

curl -sS -X POST "$SHARESHIELD_URL/api/v2/requests/$TOKEN/fulfil-file" \
  -F [email protected] \
  -F 'options={"expiryDays":1}'

Account

get/api/v2/meauth required

Who am I

The caller, their organization, and the scopes and permissions usable with these credentials.

Response 200 The caller

id*string
email*string
displayName*string
role*string
Org role: owner, admin, member or viewer
organization*object
id*string
name*string
via*"session" | "apiKey"
apiKey*object | null
The calling key; null for session callers
id*string
scopes*"secrets:create" | "secrets:read" | "secrets:burn" | "secrets:list" | "requests:create" | "requests:read" | "audit:read"[]
scopes*"secrets:create" | "secrets:read" | "secrets:burn" | "secrets:list" | "requests:create" | "requests:read" | "audit:read"[]
Scopes usable right now (role ∩ key scopes)
permissions*string[]
Effective role permissions as resource:action strings

Example

curl -sS -X GET "$SHARESHIELD_URL/api/v2/me" \
  -H "x-api-key: $SHARESHIELD_API_KEY"

Meta

get/api/v2/openapi.jsonpublic

This OpenAPI document

Response 200 OpenAPI 3.1 document

No body.

Example

curl -sS -X GET "$SHARESHIELD_URL/api/v2/openapi.json"

Walkthrough: send and reveal a secret

# 1. Create (returns {"id","url","burnUrl","expiresAt","maxViews"})
ID=$(curl -sS -X POST "$SHARESHIELD_URL/api/v2/secrets" \
  -H "x-api-key: $SHARESHIELD_API_KEY" -H "content-type: application/json" \
  -d '{"content":"db-password: hunter2","expiryHours":4}' | jq -r .id)

# 2. Reveal once (needs the secrets:read scope when using a key)
curl -sS -X POST "$SHARESHIELD_URL/api/v2/secrets/$ID/reveal" -H "x-api-key: $SHARESHIELD_API_KEY"

# 3. A second reveal returns 410 GONE
curl -sS -X POST "$SHARESHIELD_URL/api/v2/secrets/$ID/reveal" -H "x-api-key: $SHARESHIELD_API_KEY"

Walkthrough: share a file

Files need an active paid plan and an organization policy that allows them; check GET /options → filesAllowed and maxFileSizeMb.

# 1. Upload (multipart; returns {"id","url","burnUrl","expiresAt","maxViews","kind":"file","fileName","sizeBytes"})
ID=$(curl -sS -X POST "$SHARESHIELD_URL/api/v2/secrets/files" \
  -H "x-api-key: $SHARESHIELD_API_KEY" \
  -F [email protected] \
  -F 'options={"expiryHours":24}' | jq -r .id)

# 2. Check it without using a view
curl -sS "$SHARESHIELD_URL/api/v2/secrets/$ID"

# 3. Download once (saved under its original name; X-Views-Remaining says what is left)
curl -sS -X POST "$SHARESHIELD_URL/api/v2/secrets/$ID/download" \
  -H "x-api-key: $SHARESHIELD_API_KEY" -OJ -D -

Walkthrough: email recipients and recipients-only access

Send recipients to email the link (signed-in callers only; see GET /options → maxRecipients and allowedRecipientDomains). With recipientsOnly, a reveal without a signed-in recipient or a recipient token returns 403 RECIPIENT_VERIFICATION_REQUIRED and takes no view. The recipient proves their address with an emailed 8-digit code.

# 1. Sender: create and email it (the response reports delivery per address)
ID=$(curl -sS -X POST "$SHARESHIELD_URL/api/v2/secrets"   -H "x-api-key: $SHARESHIELD_API_KEY" -H "content-type: application/json"   -d '{"content":"db-password: hunter2","recipients":["[email protected]"],"recipientsOnly":true}'   | tee /dev/stderr | jq -r .id)
# → {"id":"…","url":"…","recipients":[{"email":"[email protected]","sent":true}],"recipientsOnly":true,…}

# 2. Recipient: does it need verification? (no view used)
curl -sS "$SHARESHIELD_URL/api/v2/secrets/$ID"          # "requiresRecipientVerification": true

# 3. Recipient: ask for a code. Always 202, recipient or not.
curl -sS -X POST "$SHARESHIELD_URL/api/v2/secrets/$ID/recipient-code"   -H "content-type: application/json" -d '{"email":"[email protected]"}'

# 4. Recipient: exchange the emailed code for a token (valid 15 minutes)
TOKEN=$(curl -sS -X POST "$SHARESHIELD_URL/api/v2/secrets/$ID/recipient-code/confirm"   -H "content-type: application/json" -d '{"email":"[email protected]","code":"123456"}' | jq -r .recipientToken)

# 5. Recipient: reveal with the token (or send {"recipientToken": "…"} in the body)
curl -sS -X POST "$SHARESHIELD_URL/api/v2/secrets/$ID/reveal" -H "X-Recipient-Token: $TOKEN"

A wrong code is 400 INVALID_CODE; the fifth wrong code discards it (429 CODE_LOCKED), and a new one must be requested.

Walkthrough: burn link

Every create returns a burnUrl (…/secret/ID/burn?token=…) for the recipient to destroy the secret without viewing it, for example when it reached the wrong person. It is returned only once.

BURN_URL=$(curl -sS -X POST "$SHARESHIELD_URL/api/v2/secrets" \
  -H "x-api-key: $SHARESHIELD_API_KEY" -H "content-type: application/json" \
  -d '{"content":"wrong-recipient"}' | jq -r .burnUrl)
ID=$(echo "$BURN_URL" | sed -E 's#.*/secret/([^/]+)/burn.*#\1#')
TOKEN=$(echo "$BURN_URL" | sed -E 's#.*token=##')

# No credentials needed: the token is the credential. 204 No Content.
curl -sS -X POST "$SHARESHIELD_URL/api/v2/secrets/$ID/burn" \
  -H "content-type: application/json" -d "{\"token\":\"$TOKEN\"}"

# Any reveal afterwards returns 410 GONE
curl -sS -X POST "$SHARESHIELD_URL/api/v2/secrets/$ID/reveal"