API documentation
A REST API for creating, viewing and managing secrets. All requests and responses use JSON, and all encryption and decryption happens on your side, so the plaintext never reaches our servers.
Overview
The base URL for all API endpoints is:
https://password.link/api
You can use the API to handle both encryption and decryption of the secret outside of our service, without needing to load any assets from us. It also allows you to use any kind of delivery method for the public password part (half of the password which is used to derive the encryption key for the secret from) and basically create any kind of custom configuration for viewing the secret.
For working end-to-end code, see the API examples.
Authentication
Every request must be authenticated with an API key, sent in the Authorization header:
Authorization: ApiKey public_key_abcd...
There are two kinds of API keys:
| Key type | Description |
|---|---|
| Public API key | Meant for creating a custom self-hosted view secret page. Can be used in public scripts. Only allows viewing and acknowledging secrets. |
| Private API key | Required for all other API actions. Must always be kept private. |
Both API keys contain the type of the key in the key itself so they can be easily distinguished.
Each endpoint below shows which key type it requires.
Errors
When a request fails, the response contains an error object:
{
"data": null,
"metadata": null,
"error": {
"message": "Ciphertext can't be blank",
"field": "ciphertext"
}
}
| Field | Description |
|---|---|
message |
The full error message. |
field |
The field the error is about, when the error is a validation error. |
The HTTP response status code also indicates the type of the error:
| Status code | Description |
|---|---|
403 Forbidden |
The request was not allowed, for example because of an invalid API key. |
404 Not Found |
The requested resource was not found. |
422 Unprocessable Content |
The request was understood but failed validation, for example because of a missing or invalid parameter. |
Encrypting secrets
Secrets are encrypted client-side before they are sent to the API. This section describes the password and ciphertext formats the service expects.
The password parts
The password which is used to encrypt the secret in AES must consist of two 18 character long strings. The actual encryption key will be the concatenation of these two strings ("private part" + "public part") and is derived from the password using PBKDF2.
Password: fkgjbnvlakwiejgutnFIGKEOTIRUBNAKQJRL
=> Private part: fkgjbnvlakwiejgutn
=> Public part: FIGKEOTIRUBNAKQJRL
The private part is sent to the API (Base64 encoded) and stored with the secret. The public part is never sent to the API, and is usually delivered in the link itself, so we can never decrypt the secret ourselves.
If you use the view secret page provided by us, you must also Base64 encode the public part. If you use a self-hosted view secret page, you can use the same method or create your own. Please see the API examples for reference implementations.
Ciphertext format
The ciphertext is a JSON object, encoded in Base64:
{
"cipher": "<Base64 encoded AES-GCM ciphertext>",
"iv": "<Base64 encoded 12-byte initialization vector>",
"salt": "<Base64 encoded 16-byte PBKDF2 salt>",
"iter": 10000
}
You must use the following settings for AES when encrypting the secret:
| Setting | Value |
|---|---|
| Mode | AES-GCM with a 128-bit authentication tag appended to the ciphertext (the Web Crypto default) |
| Key size | 256 bits |
| Key derivation | PBKDF2 with SHA-256 |
| PBKDF2 iterations | The value of the iter field. Allowed range 1000-1000000, we recommend 10000. |
| Initialization vector | 12 random bytes |
| Salt | 16 random bytes |
Legacy SJCL format (deprecated)
Ciphertexts created with the Stanford Javascript Crypto Library (SJCL) are still accepted: an SJCL compatible ciphertext JSON string encoded in Base64, using AES-GCM ("mode:gcm"), a 256 bit key ("ks:256") and 10000 PBKDF2 iterations ("iter:10000"). This format is deprecated and support for it will be removed in the future.
Decrypting secrets
The View Secret API returns the ciphertext exactly as it was stored. Secrets created in the web app use the Web Crypto format above. Secrets created by older API clients, the Chrome extension, or still-unopened links may use the legacy SJCL format.
A self-hosted view page must handle both. After Base64-decoding the ciphertext to JSON, check for a ct key: if it is present, decrypt with SJCL; otherwise decrypt with Web Crypto. See the View Secret example.
API clients that only create secrets do not need to change: SJCL ciphertexts are still accepted on create. Clients that decrypt secrets fetched from the API must add Web Crypto support, or they will fail on secrets created in the web app.
Create Secret
Creates an encrypted secret and returns its ID. The ciphertext must be created as described in Encrypting secrets. Returns 201 Created on success.
Request parameters
| Parameter | Type | Description |
|---|---|---|
ciphertextrequired |
string |
A Web Crypto compatible ciphertext JSON string of the secret, encoded in Base64. Legacy SJCL compatible ciphertexts are also accepted (deprecated). |
password_part_privaterequired |
string |
The private password part which was used to encrypt the secret, encoded in Base64. |
password_part_public |
string |
The public password part, encoded in Base64. Required only when using email_to so the API can email the hosted secret link. |
description |
string |
A description for the secret. Cannot be seen when viewing the secret. |
message |
string |
A message for the secret. Will be shown along the secret. |
expiration |
integer |
An expiration time for the secret, in hours. Possible values: 1-500. |
view_button |
boolean |
Show a view secret button instead of showing the secret immediately after opening the link. |
captcha |
boolean |
Show a simple CAPTCHA before showing the secret, mainly for blocking automated scanners. |
password |
string |
A password for the secret. |
max_views |
integer |
How many times the secret can be viewed. Possible values: 1-100. |
email_to |
string |
Comma-separated email recipients that should receive the created secret link. Requires password_part_public. |
otp_email_recipient |
string |
Email address that receives a one-time password. When set, the recipient must enter the emailed code before the secret can be viewed. |
otp_recipient |
string |
Accepted as an alias for otp_email_recipient. Prefer otp_email_recipient for new integrations. |
otp_sms_recipient |
string |
Phone number that receives a one-time password by SMS, in international format starting with + and no spaces. |
allowed_ips |
string |
Comma-separated IP addresses or CIDR ranges allowed to view the secret. |
allowed_locations |
array |
Array of ISO 3166-1 alpha-2 country codes allowed to view the secret. |
available_after |
datetime |
Date and time before which the secret cannot be viewed. |
secret_folder_id |
string |
ID of an existing secret folder owned by the API key user. |
reply_enabled |
boolean |
Enable a one-time reply from the recipient after the secret has been viewed. |
ack_required |
boolean |
Require recipient acknowledgement before the secret can be viewed. |
ack_mode |
string |
Acknowledgement mode. Use checkbox or phrase. |
ack_phrase |
string |
Phrase the recipient must type. Required when ack_required is true and ack_mode is phrase. |
ack_disclosure_text |
string |
Disclosure text shown to the recipient, up to 500 characters. |
ack_collect_name |
boolean |
collect the recipient's name with the acknowledgement. |
ack_name_required |
boolean |
Require the recipient's name. This also enables ack_collect_name. |
ack_collect_org |
boolean |
collect the recipient's organization with the acknowledgement. |
attachment |
object |
Metadata for one encrypted file attachment, see Attachments. Requires an active plan. |
attachment.file_name |
string |
The original file name shown to the recipient. Required when attachment is provided. |
attachment.file_type |
string |
The original MIME type, for example text/plain or application/pdf. Required when attachment is provided. |
attachment.file_size |
integer |
The original unencrypted file size in bytes. Required when attachment is provided. |
Team Secret Settings can disable, require, or enforce defaults for supported secret options. The API applies those team rules when creating a secret.
Example request
curl -H "Authorization: ApiKey private_key_abcd" \
-H "Content-Type: application/json" \
-X POST https://password.link/api/secrets \
-d '{
"ciphertext": "eyJjaXBoZXIiOiJRc3hxsN3...aXRlciI6MTAwMDB9",
"password_part_private": "ZmtnamJudmxha3dpZWpndXRu",
"description": "Database credentials",
"expiration": 24,
"view_button": true,
"max_views": 1
}'
Example response
{
"data": {
"id": "dT5g"
},
"metadata": {
"secrets_total": 70,
"secrets_usage": 25,
"secrets_allowance": 100,
"secret_requests_total": 10,
"secret_requests_allowance": 100
}
}
The link to the secret is formed by combining the hosted view page URL, the secret ID and the Base64 encoded public password part:
https://password.link/<id>/#<public password part, Base64>
Attachments
Uploading an attachment is a two-step process: first create the secret with attachment metadata (see Create Secret), then encrypt and upload the encrypted attachment payload to the returned upload URL.
The secret contents and attachment contents are encrypted separately. Both use Web Crypto compatible AES-GCM data, Base64 encoded, but the payload fields are encoded differently: the secret payload stores Base64 encoded fields and an iter field, while the attachment payload stores raw binary string fields.
Encrypting the attachment
Before encryption, represent the file as a data URL:
data:<mime-type>;base64,<base64 file bytes>
The attachment encryption password is the raw private password part concatenated with the raw public password part (no Base64 encoding).
Encrypt that data URL string with AES-GCM using PBKDF2-SHA256, 10000 iterations, a 256-bit derived key, a 16-byte random salt and a 12-byte random IV. Store the encrypted attachment payload as a JSON object, then Base64 encode the JSON string:
{
"cipher": "<binary string>",
"iv": "<binary string>",
"salt": "<binary string>"
}
Uploading the encrypted file
When an attachment is included, the create secret response contains an upload target in data.attachment.upload:
{
"data": {
"id": "dT5g",
"attachment": {
"file_name": "example.txt",
"upload": {
"url": "/api/secrets/dT5g/attachment",
"fields": {},
"metadata": {}
}
}
},
"metadata": {}
}
Upload the Base64 encoded encrypted JSON as text/plain using multipart/form-data to the returned upload.url. Add any returned upload.metadata or upload.fields key/value pairs to the form data before adding the encrypted file. The encrypted file field must be named file.
For Password.link upload URLs, include the same private API key in the Authorization: ApiKey <key> header. Direct storage upload URLs, such as pre-signed S3 URLs, should only receive the returned form fields and the encrypted file field.
Password.link-hosted upload URLs include an empty fields object. Direct storage upload URLs, such as pre-signed S3 URLs, return signing fields in metadata instead.
Attachment size limits depend on the account or team allowance, and the encrypted upload is larger than the original file because the file is converted to Base64 and encrypted.
View Secret
Fetches the ciphertext, private password part and other details of a secret. This endpoint can be used to create a custom self-hosted view secret page which does not load any scripts or other assets from our service. Returns 200 OK on success.
Successfully fetching a secret automatically marks it as viewed and so deletes the ciphertext and private password part from our database, making it impossible to view the secret again.
The ciphertext may be the Web Crypto format or the legacy SJCL format. Detect SJCL payloads by a ct key in the decoded JSON. See Decrypting secrets.
Example request
curl -H "Authorization: ApiKey public_key_abcd" \
https://password.link/api/secrets/dT5g
Example response
{
"data": {
"id": "dT5g",
"ciphertext": "eyJjaXBoZXIiOiJRc3hxsN3...aXRlciI6MTAwMDB9",
"password_part_private": "ZmtnamJudmxha3dpZWpndXRu",
"message": "Here are the credentials we talked about"
},
"metadata": null
}
Acknowledge Secret
Submits recipient acknowledgement for a secret that was created with ack_required, and returns the same secret data as View Secret when the acknowledgement is valid. Returns 200 OK on success.
If acknowledgement is required, GET /api/secrets/<id> returns 403 Forbidden with secret_status set to ack_required until this endpoint is used.
Invalid or incomplete acknowledgement payloads also return 403 Forbidden. They are not returned as validation errors.
Request parameters
| Parameter | Type | Description |
|---|---|---|
ack_confirmed |
boolean |
Set to true for checkbox acknowledgement. |
ack_phrase |
string |
Required for phrase acknowledgement, and must match the phrase configured for the secret. |
ack_signer_name |
string |
Recipient name, required when name collection is enabled and marked as required. |
ack_signer_org |
string |
Recipient organization, stored when organization collection is enabled. |
Example request
curl -H "Authorization: ApiKey public_key_abcd" \
-H "Content-Type: application/json" \
-X POST https://password.link/api/secrets/dT5g/acknowledge \
-d '{"ack_confirmed": true, "ack_signer_name": "Jane Doe"}'
List Secrets
Fetches a list of secrets. Does not contain the ciphertexts or private password parts, only IDs, descriptions and the like. Returns 200 OK on success.
The maximum amount of returned results for each query is limited to 50. You can control the offset with the optional offset query parameter, for example: /api/secrets?offset=50
Example request
curl -H "Authorization: ApiKey private_key_abcd" \
https://password.link/api/secrets
Example response
{
"data": [
{
"id": "dT5g",
"created_at": "2026-07-02T10:00:00.000Z",
"message": "Here are the credentials we talked about",
"description": "Database credentials",
"view_button": true,
"captcha": false,
"password": false,
"expiration": 24,
"expired": false,
"view_times": 1,
"max_views": 1,
"secret_folder_id": "5e976e6e-d205-4f58-8df9-5ac0048bc702",
"views": [
{
"viewed_at": "2026-07-02T12:34:56.000Z",
"viewed_by_ip": "192.0.2.10",
"viewed_by_user_agent": "Mozilla/5.0 ..."
}
]
}
],
"metadata": {
"secrets_total": 70,
"secrets_usage": 25,
"secrets_allowance": 100,
"secret_requests_total": 10,
"secret_requests_allowance": 100
}
}
Update Secret
Updates supported settings and metadata for an existing secret. Returns 204 No Content on success.
PATCH does not replace the encrypted secret content, re-send delivery emails, or create attachment upload metadata. Use Create Secret for ciphertext, password parts, link delivery, and attachments.
The following parameters can be updated, with the same meaning as in Create Secret:
description, message, expiration, max_views,
view_button, captcha, password,
otp_email_recipient, otp_recipient, otp_sms_recipient,
allowed_ips, allowed_locations, available_after,
secret_folder_id, reply_enabled,
ack_required, ack_mode, ack_phrase, ack_disclosure_text,
ack_collect_name, ack_name_required, ack_collect_org
ciphertext, password_part_private, password_part_public, email_to and attachment metadata are create-only. They are ignored by PATCH.
Example request
curl -H "Authorization: ApiKey private_key_abcd" \
-H "Content-Type: application/json" \
-X PATCH https://password.link/api/secrets/dT5g \
-d '{"description": "Updated description", "otp_email_recipient": "recipient@example.com"}'
Delete Secret
Deletes a secret. Returns 200 OK on success.
Example request
curl -H "Authorization: ApiKey private_key_abcd" \
-X DELETE https://password.link/api/secrets/dT5g
Example response
{
"data": null,
"metadata": {
"secrets_total": 69,
"secrets_usage": 24,
"secrets_allowance": 100,
"secret_requests_total": 10,
"secret_requests_allowance": 100
}
}
Create Secret Folder
Creates a secret folder. Returns 201 Created on success.
Request parameters
| Parameter | Type | Description |
|---|---|---|
namerequired |
string |
Folder name. |
description |
string |
Folder description. |
Example response
{
"data": {
"id": "5e976e6e-d205-4f58-8df9-5ac0048bc702"
},
"metadata": {
"secrets_total": 70,
"secrets_usage": 25,
"secrets_allowance": 100,
"secret_requests_total": 10,
"secret_requests_allowance": 100
}
}
List Secret Folders
Fetches a list of secret folders. Returns 200 OK on success.
Example response
{
"data": [
{
"id": "5e976e6e-d205-4f58-8df9-5ac0048bc702",
"name": "Clients",
"description": "Client credentials",
"created_at": "2026-07-02T10:00:00.000Z",
"updated_at": "2026-07-02T10:00:00.000Z"
}
],
"metadata": {
"secrets_total": 70,
"secrets_usage": 25,
"secrets_allowance": 100,
"secret_requests_total": 10,
"secret_requests_allowance": 100
}
}
Update Secret Folder
Updates the name and description of a secret folder. Accepts the same parameters as Create Secret Folder and returns 204 No Content on success.
Delete Secret Folder
Deletes a secret folder. Returns 200 OK on success.
Create Secret Request
Creates a new Secret Request. Returns 201 Created on success.
Request parameters
| Parameter | Type | Description |
|---|---|---|
description |
string |
Description for the Secret Request. |
message |
string |
Message for the Secret Request viewer. |
expiration |
integer |
Expiration time for the Secret Request, in hours. |
limit |
integer |
Usage limit for the Secret Request. |
send_request_to_email |
string |
Send the created Secret Request link to the given email address. |
send_request_to_sms |
string |
Send the created Secret Request link to the given phone number. |
send_to_email |
string |
Send the Secret link created using the Secret Request to the given email address. |
secret_description |
string |
Description for the Secret created using the Secret Request. |
secret_message |
string |
Message for the Secret created using the Secret Request. |
secret_expiration |
integer |
Expiration time for the Secret created using the Secret Request, in hours. |
secret_password |
string |
Password for the Secret created using the Secret Request. |
secret_max_views |
integer |
View limit for the Secret created using the Secret Request. |
template_id |
string |
ID of the request template to use. |
secret_allowed_emails |
string |
Comma-separated email addresses or domains allowed to open Secrets created with this Secret Request. |
secret_allowed_ips |
string |
Comma-separated IP addresses or CIDR ranges allowed to open Secrets created with this Secret Request. |
Example request
curl -H "Authorization: ApiKey private_key_abcd" \
-H "Content-Type: application/json" \
-X POST https://password.link/api/secret_requests \
-d '{"description": "Request for VPN credentials", "expiration": 48}'
Example response
{
"data": {
"id": "5e976e6e-d205-4f58-8df9-5ac0048bc702"
},
"metadata": {
"secrets_total": 70,
"secrets_usage": 25,
"secrets_allowance": 100,
"secret_requests_total": 10,
"secret_requests_allowance": 100
}
}
List Secret Requests
Fetches a list of Secret Requests. Returns 200 OK on success.
The maximum amount of returned results for each query is limited to 50. You can control the offset with the optional offset query parameter, for example: /api/secret_requests?offset=50
The list response does not include secret_allowed_emails or secret_allowed_ips.
Example request
curl -H "Authorization: ApiKey private_key_abcd" \
https://password.link/api/secret_requests
Example response
{
"data": [
{
"id": "5e976e6e-d205-4f58-8df9-5ac0048bc702",
"description": "Request for VPN credentials",
"message": "Please send me the VPN credentials",
"expiration": 48,
"limit": 1,
"send_request_to_email": "request-recipient@example.com",
"send_request_to_sms": "+358401234567",
"send_to_email": "example@example.com",
"secret_description": "VPN credentials",
"secret_message": null,
"secret_expiration": 24,
"secret_max_views": 1,
"secret_password": false,
"template_id": null
}
],
"metadata": {
"secrets_total": 70,
"secrets_usage": 25,
"secrets_allowance": 100,
"secret_requests_total": 10,
"secret_requests_allowance": 100
}
}
Delete Secret Request
Deletes a Secret Request. Returns 200 OK on success.
Example request
curl -H "Authorization: ApiKey private_key_abcd" \
-X DELETE https://password.link/api/secret_requests/5e976e6e-d205-4f58-8df9-5ac0048bc702