{"components":{"responses":{"BadRequest":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Invalid input"},"Gone":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Resource is no longer accessible. error.code discriminator: SECRET_BURNED, SECRET_EXPIRED, REQUEST_EXPIRED, REQUEST_FULFILLED, FULFILLMENT_BURNED, or LEGACY_API_SHAPE"},"OrgRequired":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Forbidden. error.code ORG_REQUIRED: the bearer is a CLI user session (bs_sess_...) whose user is not a member of an organization. The CLI requires an organization; create one in the dashboard, then retry. Chrome extension sessions and service keys never get this code. On POST /v1/secrets a 403 is also returned when notification options are requested without authentication."},"PortableBadRequest":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"INVALID_INPUT: consume body must be empty","headers":{"Cache-Control":{"schema":{"enum":["no-store"],"type":"string"}},"Pragma":{"schema":{"enum":["no-cache"],"type":"string"}}}},"PortableForbidden":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"ACCESS_DENIED: valid capability blocked by retrieve restrictions","headers":{"Cache-Control":{"schema":{"enum":["no-store"],"type":"string"}},"Pragma":{"schema":{"enum":["no-cache"],"type":"string"}}}},"PortableGone":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"FULFILLMENT_BURNED: generic consumed tombstone; no verifier or payload retained","headers":{"Cache-Control":{"schema":{"enum":["no-store"],"type":"string"}},"Pragma":{"schema":{"enum":["no-cache"],"type":"string"}}}},"PortableInternalError":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Internal error without credential or private material disclosure","headers":{"Cache-Control":{"schema":{"enum":["no-store"],"type":"string"}},"Pragma":{"schema":{"enum":["no-cache"],"type":"string"}}}},"PortableNotReady":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"REQUEST_NOT_READY: authorized request has no fulfillment yet","headers":{"Cache-Control":{"schema":{"enum":["no-store"],"type":"string"}},"Pragma":{"schema":{"enum":["no-cache"],"type":"string"}}}},"PortableRateLimited":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"RATE_LIMIT_EXCEEDED: bounded capability-IP rate limit","headers":{"Cache-Control":{"schema":{"enum":["no-store"],"type":"string"}},"Pragma":{"schema":{"enum":["no-cache"],"type":"string"}},"Retry-After":{"schema":{"type":"integer"}}}},"PortableUnauthorized":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"PORTABLE_UNAVAILABLE: missing or malformed capability bearer","headers":{"Cache-Control":{"schema":{"enum":["no-store"],"type":"string"}},"Pragma":{"schema":{"enum":["no-cache"],"type":"string"}}}},"PortableUnavailable":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"PORTABLE_UNAVAILABLE: invalid capability or unavailable/expired record","headers":{"Cache-Control":{"schema":{"enum":["no-store"],"type":"string"}},"Pragma":{"schema":{"enum":["no-cache"],"type":"string"}}}},"RateLimited":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Rate limit exceeded. Anonymous ~10/day/IP. Authenticated 100/day/key (standard) or 5000/day/key (partner).","headers":{"Retry-After":{"schema":{"type":"integer"}}}},"Unauthorized":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Missing or invalid authentication (unknown, revoked or expired service key or session). error.code SIGN_IN_METHOD_NOT_ALLOWED means the organization's sign-in policy no longer allows the method this session was created with; sign in again with an allowed method."}},"schemas":{"ConfirmBody":{"properties":{"id":{"description":"The secret id returned by POST /v1/secrets/upload-url.","type":"string"}},"required":["id"],"type":"object"},"ConfirmResponse":{"properties":{"id":{"type":"string"},"link_origin":{"description":"Origin for share links: the org's active custom domain, else https://burnedsecret.com","format":"uri","type":"string"},"status":{"enum":["active"],"type":"string"}},"type":"object"},"CreateRequestBody":{"properties":{"fields":{"description":"Phase 6 carry-over: up to 10 named input fields for the fulfiller","items":{"$ref":"#/components/schemas/RequestField"},"maxItems":10,"type":"array"},"notification_email":{"format":"email","type":"string"},"portable_view":{"$ref":"#/components/schemas/PortableViewCreate"},"prompt":{"maxLength":1024,"type":"string"},"public_key":{"description":"RSA-OAEP-SHA256 4096-bit SPKI DER base64url (no padding); ~736 bytes after decode","type":"string"},"request_id":{"$ref":"#/components/schemas/PortableRequestId"},"ttl":{"default":86400,"enum":[300,3600,86400,604800],"type":"integer"},"webhook_url":{"format":"uri","type":"string"}},"required":["prompt","public_key","request_id","portable_view"],"type":"object"},"CreateSecretBody":{"properties":{"ciphertext":{"description":"AES-256-GCM ciphertext+tag base64url (no padding)","type":"string"},"iv":{"description":"12-byte IV base64url","type":"string"},"max_views":{"default":1,"description":"Multi-view: the secret survives N reads and burns on the Nth (1..5, default 1 = burn on first read).","maximum":5,"minimum":1,"type":"integer"},"notification_email":{"description":"Auth-only","format":"email","type":"string"},"ttl":{"default":86400,"enum":[300,3600,86400,604800],"type":"integer"},"webhook_url":{"description":"Auth-only","format":"uri","type":"string"}},"required":["ciphertext","iv"],"type":"object"},"CreateSecretResponse":{"properties":{"expires_at":{"format":"date-time","type":"string"},"id":{"type":"string"},"link_origin":{"description":"Origin for share links: the org's active custom domain, else https://burnedsecret.com","format":"uri","type":"string"},"ttl":{"type":"integer"}},"type":"object"},"Error":{"properties":{"error":{"properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"integer"}},"type":"object"}},"type":"object"},"FileSecretBurnResponse":{"description":"Polymorphic GET response when the secret is a file. The client GETs the BSF2 blob from `download_url` and decrypts it locally with the master key from the share URL fragment.","properties":{"burned_at":{"format":"date-time","type":"string"},"download_url":{"description":"V4 signed GET URL for the BSF2 blob.","format":"uri","type":"string"},"id":{"type":"string"},"kind":{"enum":["file"],"type":"string"},"size_bytes":{"type":"integer"}},"type":"object"},"FulfillBody":{"properties":{"ciphertext":{"description":"AES-256-GCM ciphertext+tag base64url","type":"string"},"iv":{"type":"string"},"wrapped_key":{"description":"RSA-OAEP-SHA256 encryption of the raw 32-byte AES key bytes, base64url; ~512 bytes after decode for RSA-4096","type":"string"}},"required":["ciphertext","iv","wrapped_key"],"type":"object"},"FulfillmentEnvelope":{"properties":{"ciphertext":{"type":"string"},"fulfilled_at":{"format":"date-time","type":"string"},"id":{"type":"string"},"iv":{"type":"string"},"wrapped_key":{"type":"string"}},"type":"object"},"PortableRequestId":{"description":"Client-generated request id: canonical unpadded base64url of exactly 16 decoded bytes. Required on every request together with portable_view; collisions return 409 REQUEST_ID_CONFLICT.","example":"ICEiIyQlJicoKSorLC0uLw","maxLength":22,"minLength":22,"pattern":"^[A-Za-z0-9_-]{22}$","type":"string"},"PortableViewCreate":{"additionalProperties":false,"description":"Complete optional portable creation envelope. request_id and this object are all-or-neither. S is 32 random bytes and never uploaded. HKDF-SHA-256 uses salt UTF8('BurnedSecret/request-view/v1/salt') || 0x00 || UTF8(request_id); request_id is the canonical 22-character unpadded base64url encoding, not the decoded 16 random bytes. Independent info labels UTF8('BurnedSecret/request-view/v1/wrap') and UTF8('BurnedSecret/request-view/v1/retrieve') derive 32-byte K and T. AES-GCM AAD is UTF8('BurnedSecret/request-view/v1/pkcs8') || 0x00 || UTF8(request_id) || 0x00 || SHA-256(SPKI_DER). See https://burnedsecret.com/security#portable-request-view-v1.","properties":{"iv":{"description":"AES-GCM IV; exactly 12 decoded bytes.","example":"MDEyMzQ1Njc4OTo7","maxLength":16,"minLength":16,"pattern":"^[A-Za-z0-9_-]{16}$","type":"string"},"retrieval_token_hash":{"description":"SHA-256(raw 32-byte T), canonical unpadded base64url; exactly 32 decoded bytes. The hash is never accepted as a capability credential.","example":"hgS7nHUvWFyLetREvUTVC_qwBSmY44IsHx-0sH0gV1w","maxLength":43,"minLength":43,"pattern":"^[A-Za-z0-9_-]{43}$","type":"string"},"version":{"enum":[1],"type":"integer"},"wrapped_private_key":{"description":"AES-256-GCM ciphertext with the 16-byte tag appended, canonical unpadded base64url; 17..8192 decoded bytes. Plaintext is the local PKCS8 private key.","example":"a1_JJv-KpnMQwL62fxBUDX7","maxLength":10923,"minLength":23,"pattern":"^[A-Za-z0-9_-]+$","type":"string"}},"required":["version","wrapped_private_key","iv","retrieval_token_hash"],"type":"object"},"PortableViewEnvelope":{"additionalProperties":false,"description":"Public opaque wrapping envelope. It deliberately omits retrieval_token_hash and all fulfillment fields.","properties":{"iv":{"type":"string"},"version":{"enum":[1],"type":"integer"},"wrapped_private_key":{"type":"string"}},"required":["version","wrapped_private_key","iv"],"type":"object"},"PortableViewStatus":{"additionalProperties":false,"properties":{"expires_at":{"format":"date-time","type":"string"},"portable_view":{"$ref":"#/components/schemas/PortableViewEnvelope"},"public_key":{"description":"RSA-OAEP-SHA256 SPKI DER, canonical base64url.","type":"string"},"status":{"enum":["waiting","ready"],"type":"string"}},"required":["status","expires_at","public_key","portable_view"],"type":"object"},"RequestField":{"properties":{"name":{"type":"string"},"required":{"default":true,"type":"boolean"},"type":{"enum":["text","password","multiline"],"type":"string"}},"required":["name","type"],"type":"object"},"RequestMetadata":{"properties":{"expires_at":{"format":"date-time","type":"string"},"fields":{"description":"Present only when status='pending' and request had fields","items":{"$ref":"#/components/schemas/RequestField"},"type":"array"},"id":{"type":"string"},"prompt":{"description":"Present only when status='pending'","type":"string"},"public_key":{"description":"Present only when status='pending'","type":"string"},"status":{"enum":["pending","fulfilled"],"type":"string"}},"type":"object"},"SecretBurnNowResponse":{"description":"Result of the sender-initiated \"Burn now\". status is \"burned\" on a real or idempotent burn, \"expired\" if the secret was already gone. NO ciphertext or key material.","properties":{"id":{"type":"string"},"status":{"enum":["burned","expired"],"type":"string"}},"type":"object"},"SecretBurnResponse":{"properties":{"burned_at":{"format":"date-time","type":"string"},"ciphertext":{"type":"string"},"id":{"type":"string"},"iv":{"type":"string"}},"type":"object"},"SecretMetadataResponse":{"description":"Non-destructive lifecycle snapshot. Carries NO ciphertext, iv, download_url, or key material (zero-knowledge invariant).","properties":{"expires_at":{"format":"date-time","nullable":true,"type":"string"},"id":{"type":"string"},"status":{"enum":["active","burned","expired"],"type":"string"},"ttl":{"type":"integer"},"views_remaining":{"description":"Present ONLY for an active multi-view secret (max_views > 1): remaining reads before burn.","type":"integer"}},"type":"object"},"UploadUrlBody":{"properties":{"ciphertext_size_bytes":{"description":"Byte length of the BSF2 blob the client will PUT. Capped at 11 MiB (11534336) \u2014 the same edge cap GCS enforces via the signed x-goog-content-length-range.","maximum":11534336,"minimum":1,"type":"integer"},"max_views":{"default":1,"description":"Multi-view bound for the file secret (1..5).","maximum":5,"minimum":1,"type":"integer"},"ttl":{"default":86400,"description":"Seconds (NOT days).","enum":[300,3600,86400,604800],"type":"integer"}},"required":["ciphertext_size_bytes"],"type":"object"},"UploadUrlResponse":{"properties":{"id":{"type":"string"},"link_origin":{"description":"Origin for share links: the org's active custom domain, else https://burnedsecret.com","format":"uri","type":"string"},"required_headers":{"description":"Headers the client MUST send verbatim on the PUT. Includes the signed `x-goog-content-length-range` (`0,11534336`) and Content-Type `application/octet-stream`.","properties":{"Content-Type":{"type":"string"},"x-goog-content-length-range":{"type":"string"}},"type":"object"},"upload_expires_at":{"format":"date-time","type":"string"},"upload_url":{"description":"V4 signed PUT URL for the BSF2 blob.","format":"uri","type":"string"}},"type":"object"}},"securitySchemes":{"BearerApiKey":{"description":"Two opaque bearer credential kinds share this scheme. (1) Organization service key (starts with bs_): belongs to an organization, not to a person, and is what servers and integrations should use. 100 requests/day per key for tier=standard; 5000 for tier=partner. Service keys are created and revoked in the dashboard, and the /v1/keys management routes (dashboard-internal, not part of this spec) require an organization Owner or Admin. (2) User session (starts with bs_sess_): a revocable per-device session minted by `bs auth login` and the Chrome extension. It acts as the signed-in user under the per-user 100/day limit and can be revoked from the dashboard's Security page. A CLI session also needs the user to be a member of an organization (403 ORG_REQUIRED otherwise). Both kinds pass the SDKs' bs_ prefix check.","scheme":"bearer","type":"http"},"PortableViewBearer":{"bearerFormat":"base64url capability token","description":"request-specific, non-OAuth capability. Supply raw T as canonical unpadded base64url (exactly 32 decoded bytes). This is separate from account/API-key authentication and is not OAuth. The server stores SHA-256(T); that stored hash is not a credential. Never place T in a URL, body, log, trace, or notification.","scheme":"bearer","type":"http"}}},"externalDocs":{"description":"Human-readable API docs and SDK getting-started","url":"https://burnedsecret.com/docs/api"},"info":{"contact":{"url":"https://burnedsecret.com"},"description":"Zero-knowledge, burn-after-reading secrets service. All payloads are encrypted client-side; the server stores opaque ciphertext only. Optional passphrase protection is performed ENTIRELY client-side (BSF2 mode 1 / PBKDF2-SHA256 600k double-encryption) and is NOT a server parameter \u2014 the server never sees a passphrase, passphrase hash, derived key, or plaintext. Text and file secrets support an optional max_views (1..5) multi-view bound: the secret survives N reads and burns on the Nth. See https://burnedsecret.com/security for the crypto spec and https://burnedsecret.com/api/v1/test-vectors.json for interop test vectors.","title":"BurnedSecret API","version":"1.0.0"},"openapi":"3.0.3","paths":{"/v1/requests":{"post":{"description":"Generates a request URL the fulfiller can open. The caller supplies a public key (SPKI base64url); the fulfiller's client encrypts with this key. Every request also carries the client-generated request_id and the complete portable_view; a missing, null or partial pair is rejected with 400 and nothing is stored. The reply is read only through the private view link (GET /v1/requests/{requestId}/view, then POST /v1/requests/{requestId}/view/consume). The server cannot decrypt anything.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRequestBody"}}},"required":true},"responses":{"201":{"description":"Request created: {id, url, expires_at, ttl}. `url` is on the caller's org link origin: the org's active custom domain, else https://burnedsecret.com (Phase 23)."},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"$ref":"#/components/responses/OrgRequired"},"409":{"description":"REQUEST_ID_CONFLICT: the client-generated request_id is already in use; nothing was overwritten"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"BearerApiKey":[]},{}],"summary":"Create a secret request"}},"/v1/requests/{requestId}/fulfill":{"post":{"description":"Submit {ciphertext, iv, wrapped_key}. The fulfiller has encrypted content with a fresh AES key and wrapped that key with the requester's public key (RSA-OAEP-SHA256 4096). Server stores the envelope and fires a webhook (if configured).","parameters":[{"in":"path","name":"requestId","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FulfillBody"}}},"required":true},"responses":{"200":{"description":"Request fulfilled"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Request not found"},"410":{"$ref":"#/components/responses/Gone"}},"security":[{"BearerApiKey":[]},{}],"summary":"Fulfill a request with an opaque envelope"}},"/v1/requests/{requestId}/metadata":{"get":{"description":"Anonymous; the fulfiller's browser calls this to learn the prompt, public_key, and field schema. After fulfillment, prompt and public_key are omitted.","parameters":[{"in":"path","name":"requestId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequestMetadata"}}},"description":"Request metadata"},"404":{"description":"Request not found"},"410":{"$ref":"#/components/responses/Gone"}},"summary":"Public read of request metadata (prompt + public_key + fields)"}},"/v1/requests/{requestId}/view":{"get":{"description":"Capability-only status read. Send raw 32-byte retrieval token T as canonical unpadded base64url in Authorization: Bearer; never send the stored SHA-256(T), a query token, S, or K. The response contains waiting/ready status and the opaque wrapped PKCS8 envelope, but never the verifier or fulfillment. Polling does not consume the answer. Responses are never cacheable. See the canonical byte contract at https://burnedsecret.com/security#portable-request-view-v1 and the pinned vector at https://burnedsecret.com/api/v1/test-vectors.json.","parameters":[{"in":"path","name":"requestId","required":true,"schema":{"$ref":"#/components/schemas/PortableRequestId"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortableViewStatus"}}},"description":"Authorized non-consuming status and wrapped key","headers":{"Cache-Control":{"schema":{"enum":["no-store"],"type":"string"}},"Pragma":{"schema":{"enum":["no-cache"],"type":"string"}}}},"401":{"$ref":"#/components/responses/PortableUnauthorized"},"403":{"$ref":"#/components/responses/PortableForbidden"},"404":{"$ref":"#/components/responses/PortableUnavailable"},"410":{"$ref":"#/components/responses/PortableGone"},"429":{"$ref":"#/components/responses/PortableRateLimited"},"500":{"$ref":"#/components/responses/PortableInternalError"}},"security":[{"PortableViewBearer":[]}],"summary":"Read portable request status without consuming it"}},"/v1/requests/{requestId}/view/consume":{"post":{"description":"One-shot capability read after the client has locally derived K, unwrapped and imported the PKCS8 private key, and the user has explicitly chosen Reveal. The request body must be empty (zero bytes). The server atomically verifies T, expiry, access restrictions and unread state before returning the existing opaque fulfillment envelope. A lost successful response is unrecoverable; never retry this POST automatically.","parameters":[{"in":"path","name":"requestId","required":true,"schema":{"$ref":"#/components/schemas/PortableRequestId"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FulfillmentEnvelope"}}},"description":"Opaque fulfillment envelope; consumed atomically","headers":{"Cache-Control":{"schema":{"enum":["no-store"],"type":"string"}},"Pragma":{"schema":{"enum":["no-cache"],"type":"string"}}}},"400":{"$ref":"#/components/responses/PortableBadRequest"},"401":{"$ref":"#/components/responses/PortableUnauthorized"},"403":{"$ref":"#/components/responses/PortableForbidden"},"404":{"$ref":"#/components/responses/PortableUnavailable"},"409":{"$ref":"#/components/responses/PortableNotReady"},"410":{"$ref":"#/components/responses/PortableGone"},"429":{"$ref":"#/components/responses/PortableRateLimited"},"500":{"$ref":"#/components/responses/PortableInternalError"}},"security":[{"PortableViewBearer":[]}],"summary":"Explicitly consume a portable request fulfillment"}},"/v1/secrets":{"post":{"description":"The caller encrypts content client-side with AES-256-GCM, supplies the opaque {ciphertext, iv, ttl}, and constructs the share URL with the AES key in the #k= fragment. The server never sees plaintext or the AES key.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSecretBody"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSecretResponse"}}},"description":"Secret created"},"400":{"$ref":"#/components/responses/BadRequest"},"403":{"$ref":"#/components/responses/OrgRequired"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"BearerApiKey":[]},{}],"summary":"Create a new (already-encrypted) secret"}},"/v1/secrets/confirm":{"post":{"description":"Step 2 of the file-secret flow. After PUTting the BSF2 blob to the signed `upload_url`, the client calls this to flip the pending doc to active. The server verifies the blob exists and its size matches the declared `ciphertext_size_bytes` (a metadata read only \u2014 it never downloads the bytes). Idempotent: confirming an already-active doc returns 200.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmBody"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmResponse"}}},"description":"Upload confirmed; secret is now active"},"400":{"$ref":"#/components/responses/BadRequest"},"404":{"description":"Secret not found"},"409":{"description":"Blob missing, size mismatch, or doc not pending (error.code BLOB_MISSING / SIZE_MISMATCH / CONFLICT)"},"429":{"$ref":"#/components/responses/RateLimited"}},"security":[{"BearerApiKey":[]},{}],"summary":"Confirm a file-secret upload completed"}},"/v1/secrets/upload-url":{"post":{"description":"Step 1 of the file-secret flow. The client encrypts the file into a self-contained BSF2 blob client-side, then asks for a V4 signed PUT URL. The response includes `required_headers` the client MUST echo verbatim on the PUT \u2014 in particular the signed `x-goog-content-length-range` (`0,11534336`), which lets Cloud Storage enforce the ~11 MiB edge cap (10 MB content + crypto/envelope overhead). The server signs the URL and writes a pending metadata doc; it NEVER sees the blob bytes or any key. The client then PUTs the blob to `upload_url` and calls POST /v1/secrets/confirm.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadUrlBody"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadUrlResponse"}}},"description":"Signed upload URL + required headers"},"400":{"$ref":"#/components/responses/BadRequest"},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"description":"Upload signing temporarily unavailable (error.code UPLOAD_SIGNING_UNAVAILABLE)"}},"security":[{"BearerApiKey":[]},{}],"summary":"Request a signed upload URL for a zero-knowledge file secret"}},"/v1/secrets/{secretId}":{"get":{"description":"Returns the opaque secret payload; the caller decrypts client-side with the AES/master key from the share URL fragment. The response is polymorphic: a TEXT secret returns {id, ciphertext, iv, burned_at}; a FILE secret returns {id, kind:\"file\", download_url, size_bytes, burned_at} \u2014 the client GETs the BSF2 blob from `download_url` and decrypts it locally. With max_views > 1 the burn happens on the Nth read; earlier reads return the same payload.","parameters":[{"in":"path","name":"secretId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/SecretBurnResponse"},{"$ref":"#/components/schemas/FileSecretBurnResponse"}]}}},"description":"Opaque secret payload (text ciphertext blob OR file download URL)"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Secret not found"},"410":{"$ref":"#/components/responses/Gone"}},"summary":"Retrieve and atomically burn a secret (text or file)"}},"/v1/secrets/{secretId}/burn":{"post":{"description":"Destroys an unread secret on demand by flipping its status to burned \u2014 distinct from the burn-on-read GET. Idempotent: burning an already-burned secret returns 200 {status:\"burned\"}; burning an already-expired secret returns 200 {status:\"expired\"} (no error code \u2014 the client has one simple success contract). Fires the same webhook + burn-notification side-effects as a read-burn, exactly once, only on a real burn. The response carries NO ciphertext or key material. Requires a valid API key (any key \u2014 ID-based holder model, same posture as GET /v1/secrets/{id}).","parameters":[{"in":"path","name":"secretId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretBurnNowResponse"}}},"description":"Secret burned (or already burned/expired) \u2014 {id, status}"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Secret not found"},"410":{"$ref":"#/components/responses/Gone"}},"summary":"Sender-initiated burn of an unread secret (\"Burn now\")"}},"/v1/secrets/{secretId}/metadata":{"get":{"description":"Returns the secret's status (active / burned / expired), expiry, and ttl WITHOUT burning it \u2014 a subsequent GET /v1/secrets/{id} still succeeds. Used by integrations (e.g. the browser extension history view) to poll live status. An active multi-view secret also reports `views_remaining`. The response carries NO ciphertext, IV, download URL, or any key material (zero-knowledge invariant). Requires a valid API key (any key \u2014 ID-based holder model, same posture as GET /v1/secrets/{id}).","parameters":[{"in":"path","name":"secretId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretMetadataResponse"}}},"description":"Secret lifecycle metadata (non-destructive)"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Secret not found"},"410":{"$ref":"#/components/responses/Gone"}},"summary":"Non-destructive read of a secret's lifecycle status"}}},"security":[{"BearerApiKey":[]}],"servers":[{"description":"Production","url":"https://burnedsecret.com/api/v1"}]}
