ShareShield
Create, reveal and manage self-destructing secrets and files from scripts, CI and integrations.
Machine-readable spec: /api/v2/openapi.json (OpenAPI 3.1).
Send an API key in the x-api-key header. Create keys under Dashboard → Profile → API keys. A key:
shs_ + 64 hex characters and is shown once; ShareShield stores only its hash. The dashboard shows its prefix, e.g. shs_ab12cd34…;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.
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.
| Scope | Allows | Requires role permission |
|---|---|---|
secrets:create | Create self-destructing secrets and files in the organization. | secrets:create |
secrets:read | Reveal (and so consume a view of) a secret by its id. | secrets:read |
secrets:burn | Destroy secrets the key owner created. | secrets:create |
secrets:list | List metadata of the key owner's secrets (never their content). | secrets:read |
requests:create | Ask someone to send you a secret. | secrets:create |
requests:read | List the key owner's secret requests. | secrets:read |
audit:read | Read 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.
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.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.X-API-Version: 2.{ "error": "…", "code": "VALIDATION_ERROR", "details": … }. code is stable; details is optional.{ "data": [ … ], "nextCursor": "…" | null }, newest first. Pass nextCursor back as ?cursor= to get the next page; limit is 1–100 (default 20).Deprecation: true, a Link to this page and, once scheduled, a Sunset date.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).
| Status | Code | Meaning |
|---|---|---|
400 | VALIDATION_ERROR | Body, query, path parameter or options part failed validation (unknown fields included); details.issues lists each problem as { path, message } |
400 | INVALID_CURSOR | The cursor is not one a previous page returned. Start again without it |
400 | BAD_REQUEST | Malformed JSON or multipart body, missing file part, empty file, unknown or repeated form part |
400 | FILE_SECRET | Reveal called on a file secret; no view was taken. Use POST /secrets/{id}/download (details.download) |
400 | NOT_A_FILE | Download called on a text secret; no view was taken. Use POST /secrets/{id}/reveal (details.reveal) |
400 | INVALID_CODE | Recipient 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 |
401 | UNAUTHORIZED | Missing, invalid, expired or revoked credentials; or an anonymous create that sends recipients (sign in to email recipients) |
402 | PAYMENT_REQUIRED | Your 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 |
403 | FORBIDDEN | Your 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 |
403 | TWO_FACTOR_REQUIRED | Session callers only: your organization requires two-factor authentication and you have not enrolled yet (details.setupPath). API keys are not affected |
403 | CROSS_SITE_REQUEST | Session callers only: a state-changing request came from another site. API keys are not affected |
403 | INSUFFICIENT_SCOPE | The API key lacks the scope the endpoint needs (details.requiredScope) |
403 | RECIPIENT_VERIFICATION_REQUIRED | Recipients-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 |
404 | NOT_FOUND | No such secret or request, or (rename, burn) a secret that is not yours |
409 | CONFLICT | The request was already fulfilled |
410 | GONE | Already viewed, burned (by its owner, its recipient or passcode lockout) or expired |
413 | PAYLOAD_TOO_LARGE | The file is over your plan's file size limit (GET /options → maxFileSizeMb), or a JSON body is over 1 MB |
415 | UNSUPPORTED_MEDIA_TYPE | Body is not application/json (or not multipart/form-data for a file upload) |
429 | TOO_MANY_REQUESTS | Rate limited; wait for the Retry-After header (seconds, also details.retryAfterSeconds) |
429 | CODE_LOCKED | Too 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 |
500 | INTERNAL_ERROR | Unexpected server error (the message is generic) |
Exceeding a limit returns 429 TOO_MANY_REQUESTS.
/api/v2/secretsauth optionalsecrets:createEncrypts `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.
content* | stringmax 65536 chars The secret text |
label | stringmax 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 |
expiryHours | integer1–2160 Lifetime in hours (1-2160). Takes precedence over `expiryDays` when both are sent. Default: your organization's default (`GET /options` → `defaultExpiryHours`) |
expiryDays | integer1–90 Lifetime in days (1-90). Ignored when `expiryHours` is sent |
maxViews | integer1–100 Reveals allowed before the secret burns (1-100). Default: `GET /options` → `defaultMaxViews` (usually 1) |
password | stringmax 256 chars Passcode the recipient must supply to reveal the secret |
requireSignIn | booleandefault false Only signed-in ShareShield users (or API keys) can reveal |
notifyOnOpen | booleandefault false Email when the secret is opened |
notifyEmail | string (email)max 254 chars Where to send the open notification (default: your email) |
notifyEmails | string (email)[] Up to 5 addresses to notify when the secret is opened (merged with `notifyEmail`, deduplicated). Sending any enables the notification |
recipients | string (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` |
recipientsOnly | boolean 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 |
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 | ||||
burnUrl | string (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) | ||||
recipients | object[] Present only when `recipients` were sent: delivery status per address
| ||||
recipientsOnly | boolean Present only when `recipients` were sent |
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 JSON429 Anonymous daily limit reached, or recipient email limit reachedcurl -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}'/api/v2/secrets/filesauth requiredsecrets:createUploads 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).
file *: the file (binary part).options: optional JSON text with these fields:label | stringmax 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 |
expiryHours | integer1–2160 Lifetime in hours (1-2160). Takes precedence over `expiryDays` when both are sent. Default: your organization's default (`GET /options` → `defaultExpiryHours`) |
expiryDays | integer1–90 Lifetime in days (1-90). Ignored when `expiryHours` is sent |
maxViews | integer1–100 Reveals allowed before the secret burns (1-100). Default: `GET /options` → `defaultMaxViews` (usually 1) |
password | stringmax 256 chars Passcode the recipient must supply to reveal the secret |
requireSignIn | booleandefault false Only signed-in ShareShield users (or API keys) can reveal |
notifyOnOpen | booleandefault false Email when the secret is opened |
notifyEmail | string (email)max 254 chars Where to send the open notification (default: your email) |
notifyEmails | string (email)[] Up to 5 addresses to notify when the secret is opened (merged with `notifyEmail`, deduplicated). Sending any enables the notification |
recipients | string (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` |
recipientsOnly | boolean 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 |
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 | ||||
burnUrl | string (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) | ||||
recipients | object[] Present only when `recipients` were sent: delivery status per address
| ||||
recipientsOnly | boolean 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 |
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-data429 Recipient email limit reachedcurl -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}'/api/v2/secretsauth requiredsecrets:listMetadata of the secrets you created in this organization, newest first. Never includes content. Recipient addresses are not listed: `recipientCount` and `recipientsOnly` summarize them.
cursor | stringmax 256 chars Opaque cursor from a previous page’s `nextCursor` |
limit | integer1–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 |
data* | object[]
| ||||||||||||||||||||||||||||||||||||||
nextCursor* | string | null Pass as `cursor` to fetch the next page; null on the last page |
400 Invalid cursor or filtercurl -sS -X GET "$SHARESHIELD_URL/api/v2/secrets?limit=20" \
-H "x-api-key: $SHARESHIELD_API_KEY"/api/v2/secrets/{id}auth optionalsecrets:readWhat 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).
id: Public secret id (the last segment of the share URL)kind* | "text" | "file" text: reveal it · file: download it |
fileName | string File secrets only |
sizeBytes | integer-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) |
404 No such secret410 Already viewed, burned or expired429 Too many attemptscurl -sS -X GET "$SHARESHIELD_URL/api/v2/secrets/$ID" \
-H "x-api-key: $SHARESHIELD_API_KEY"/api/v2/secrets/{id}/revealauth optionalsecrets:readDecrypts 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.
id: Public secret id (the last segment of the share URL)X-Recipient-Token (optional): Recipients-only secrets: the recipientToken from POST /secrets/{id}/recipient-code/confirm (alternative to the body field; the header wins)password | stringmax 256 chars Passcode the recipient must supply to reveal the secret |
recipientToken | stringmax 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) |
content* | string The decrypted secret text |
viewsRemaining* | integer-9007199254740991–9007199254740991 Views left after this one; 0 means the secret is now burned |
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 secret410 Already viewed, burned or expired429 Too many attemptscurl -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"}'/api/v2/secrets/{id}/downloadauth optionalsecrets:readReturns 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.
id: Public secret id (the last segment of the share URL)X-Recipient-Token (optional): Recipients-only secrets: the recipientToken from POST /secrets/{id}/recipient-code/confirm (alternative to the body field; the header wins)password | stringmax 256 chars Passcode the recipient must supply to reveal the secret |
recipientToken | stringmax 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) |
The raw file bytes, application/octet-stream. Headers:
Content-Disposition: `attachment; filename="…"; filename*=UTF-8''…` with the stored file nameX-Views-Remaining: Views left after this one; 0 means the secret is now burnedX-Content-Type-Options: `nosniff`Cache-Control: `no-store`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 secret410 Already viewed, burned or expired429 Too many attemptscurl -sS -X POST "$SHARESHIELD_URL/api/v2/secrets/$ID/download" \
-H "x-api-key: $SHARESHIELD_API_KEY" \
-OJ/api/v2/secrets/{id}/burnpublicLets 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.
id: Public secret id (the last segment of the share URL)token* | stringmax 256 chars The `token` query parameter of the `burnUrl` returned at create |
No body.
403 Wrong burn token (FORBIDDEN)404 No such secret410 Already viewed, burned or expired415 Body is not JSON429 Too many attemptscurl -sS -X POST "$SHARESHIELD_URL/api/v2/secrets/$ID/burn" \
-H "content-type: application/json" \
-d '{"token":"TOKEN_FROM_BURN_URL"}'/api/v2/secrets/{id}auth requiredsecrets:createSets 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.
id: Public secret id (the last segment of the share URL)label* | string | null |
label* | string | null |
404 No such secret, or not yours429 Too many ids that do not exist were triedcurl -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"}'/api/v2/secrets/{id}auth requiredsecrets:burnDestroys 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.
id: Public secret id (the last segment of the share URL)No body.
404 No such secret, or not yours429 Too many ids that do not exist were triedcurl -sS -X DELETE "$SHARESHIELD_URL/api/v2/secrets/$ID" \
-H "x-api-key: $SHARESHIELD_API_KEY"/api/v2/optionsauth optionalThe 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).
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)
|
curl -sS -X GET "$SHARESHIELD_URL/api/v2/options" \
-H "x-api-key: $SHARESHIELD_API_KEY"/api/v2/secrets/{id}/recipient-codepublicFor 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.
id: Public secret id (the last segment of the share URL)email* | string The recipient address to send the 8-digit code to |
accepted* | true |
message* | string The same whether or not the address is a recipient |
404 No such secret410 Already viewed, burned or expired415 Body is not JSON429 Too many codes requestedcurl -sS -X POST "$SHARESHIELD_URL/api/v2/secrets/$ID/recipient-code" \
-H "content-type: application/json" \
-d '{"email":"[email protected]"}'/api/v2/secrets/{id}/recipient-code/confirmpublicExchanges 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).
id: Public secret id (the last segment of the share URL)email* | string The address the code was sent to |
code* | string The 8-digit code from the email |
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) |
400 Validation failed (VALIDATION_ERROR), or the code is wrong or expired (INVALID_CODE)404 No such secret410 Already viewed, burned or expired415 Body is not JSON429 Too many wrong codes, the code was discarded (CODE_LOCKED); or rate limited (TOO_MANY_REQUESTS)curl -sS -X POST "$SHARESHIELD_URL/api/v2/secrets/$ID/recipient-code/confirm" \
-H "content-type: application/json" \
-d '{"email":"[email protected]","code":"123456"}'/api/v2/requestsauth requiredrequests:createCreates 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.
recipientEmail | string (email)max 254 chars Email the request link to this address |
message | stringmax 500 chars Shown to the person fulfilling the request |
expiryDays | integer1–90default 3 How long the request link stays open (days) |
notifyOnFulfill | booleandefault true Email you the secret link when fulfilled |
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 |
415 Body is not JSON429 Request limit reachedcurl -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}'/api/v2/requestsauth requiredrequests:readcursor | stringmax 256 chars Opaque cursor from a previous page’s `nextCursor` |
limit | integer1–100default 20 Page size (1-100) |
status | "open" | "fulfilled" | "expired" open: waiting for a secret · fulfilled · expired (never fulfilled) |
data* | object[]
| ||||||||||||||||
nextCursor* | string | null Pass as `cursor` to fetch the next page; null on the last page |
400 Invalid cursor or filtercurl -sS -X GET "$SHARESHIELD_URL/api/v2/requests?limit=20" \
-H "x-api-key: $SHARESHIELD_API_KEY"/api/v2/requests/{token}publicWhat 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.
token: Request token (the last segment of the request URL)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 |
404 No such request409 Already fulfilled410 Expired429 Too many links that do not exist were triedcurl -sS -X GET "$SHARESHIELD_URL/api/v2/requests/$TOKEN"/api/v2/requests/{token}/fulfilpublicCreates a secret owned by the requester and emails them its link. No authentication.
Rate limit: 10 per IP per hour.
token: Request token (the last segment of the request URL)content* | stringmax 65536 chars The secret text |
expiryDays | integer1–90default 1 Lifetime of the created secret (days) |
password | stringmax 256 chars Passcode the recipient must supply to reveal the secret |
fulfilled* | true |
message* | string |
402 Requester's plan quota reached403 A passcode the requester's organization requires is missing: send `password` (FORBIDDEN; `GET /requests/{token}` → `requirePasscode`)404 No such request409 Already fulfilled410 Expired415 Body is not JSON429 Too many fulfilmentscurl -sS -X POST "$SHARESHIELD_URL/api/v2/requests/$TOKEN/fulfil" \
-H "content-type: application/json" \
-d '{"content":"staging-db: s3cr3t","expiryDays":1}'/api/v2/requests/{token}/fulfil-filepublicLike `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).
token: Request token (the last segment of the request URL)file *: the file (binary part).options: optional JSON text with these fields:expiryDays | integer1–90default 1 Lifetime of the created secret (days) |
password | stringmax 256 chars Passcode the recipient must supply to reveal the secret |
fulfilled* | true |
message* | string |
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 request409 Already fulfilled410 Expired413 File over the requester's size limit (PAYLOAD_TOO_LARGE)415 Body is not multipart/form-data429 Too many fulfilmentscurl -sS -X POST "$SHARESHIELD_URL/api/v2/requests/$TOKEN/fulfil-file" \
-F [email protected] \
-F 'options={"expiryDays":1}'/api/v2/meauth requiredThe caller, their organization, and the scopes and permissions usable with these credentials.
id* | string | ||||
email* | string | ||||
displayName* | string | ||||
role* | string Org role: owner, admin, member or viewer | ||||
organization* | object
| ||||
via* | "session" | "apiKey" | ||||
apiKey* | object | null The calling key; null for session callers
| ||||
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 |
curl -sS -X GET "$SHARESHIELD_URL/api/v2/me" \
-H "x-api-key: $SHARESHIELD_API_KEY"/api/v2/openapi.jsonpublicNo body.
curl -sS -X GET "$SHARESHIELD_URL/api/v2/openapi.json"# 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"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 -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.
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"