SentraShare REST API
Programmatically create, retrieve and manage zero-knowledge secrets. All encryption happens client-side - the server only stores ciphertext it cannot read.
Generate your API key via POST /api/auth/api-key after logging in. API keys start with sk_ and are shown only once - store them securely.
Include your key in every request:
Authorization: Bearer sk_your_api_key_here
Zero-Knowledge note: SentraShare never sees your plaintext. Before calling POST /api/secrets, encrypt your payload client-side using XSalsa20-Poly1305 (libsodium / TweetNaCl). Pass the Base64-encoded ciphertext and nonce. The encryption key lives only in the share URL fragment (#key=…) and is never sent to the server.
The most common use-case: your application creates a secret via the API and sends the resulting share URL to a recipient. The recipient opens the URL in any browser - the page decrypts the secret client-side without the server ever seeing the plaintext.
Prerequisites: PHP 8.1+, ext-sodium (bundled since PHP 7.2),
and a valid API key (sk_…).
<?php // composer require ext-sodium (bundled in PHP 7.2+, nothing to install) $plaintext = 'My super secret message'; // what you want to protect $key = sodium_crypto_secretbox_keygen(); // 32 random bytes $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); // 24 random bytes $ciphertext = sodium_crypto_secretbox($plaintext, $nonce, $key);
$apiKey = 'sk_your_api_key_here';
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$baseUrl = $scheme . '://' . $_SERVER['HTTP_HOST'];
$payload = json_encode([
'ciphertext' => base64_encode($ciphertext),
'nonce' => base64_encode($nonce),
'ttl_hours' => 24, // secret expires after 24 hours
'max_views' => 1, // deleted after first view
]);
$ch = curl_init($baseUrl . '/api/secrets');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey,
],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
// $response['id'] contains the secret UUID
$secretId = $response['id'];
// The fragment (#key=...) is processed by the browser only -
// it is never transmitted to the server. This is what keeps
// the encryption truly zero-knowledge.
$shareUrl = $baseUrl . '/s/' . $secretId . '#key=' . base64_encode($key);
// Send $shareUrl to the recipient, e.g. via email:
// "Click here to view your secret: {$shareUrl}"
echo $shareUrl;
max_views: 1).
A passphrase adds a second factor: the recipient must enter it in the browser before the secret is revealed.
The server never stores the passphrase itself - only a bcrypt hash of HMAC-SHA256(passphrase, encryptionKey).
This means the passphrase cannot be brute-forced without also knowing the key.
In PHP you can compute the HMAC with the built-in hash_hmac() function and encode the raw binary result as Base64.
<?php
$plaintext = 'My super secret message';
$passphrase = 'correct horse battery staple'; // share this separately with the recipient
$key = sodium_crypto_secretbox_keygen(); // 32 random bytes
$nonce = random_bytes(12); // 12 bytes for AES-GCM
// Encrypt with AES-256-GCM
$ciphertext = openssl_encrypt(
$plaintext,
'aes-256-gcm',
$key,
OPENSSL_RAW_DATA,
$nonce,
$tag // 16-byte auth tag, appended to ciphertext
);
$ciphertextWithTag = $ciphertext . $tag;
// Build passphrase verifier: HMAC-SHA256(passphrase, key) - raw binary, then base64
$verifier = base64_encode(hash_hmac('sha256', $passphrase, $key, true));
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$baseUrl = $scheme . '://' . $_SERVER['HTTP_HOST'];
// POST to API
$payload = json_encode([
'ciphertext' => base64_encode($ciphertextWithTag),
'nonce' => base64_encode($nonce),
'ttl_hours' => 24,
'max_views' => 1,
'passphrase_verifier' => $verifier,
]);
$ch = curl_init($baseUrl . '/api/secrets');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer sk_your_api_key_here',
],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
// Build share URL - key in URL fragment, passphrase sent separately (e.g. by phone/SMS)
$keyB64Url = rtrim(strtr(base64_encode($key), '+/', '-_'), '=');
$shareUrl = $baseUrl . '/s/' . $response['id'] . '#key=' . $keyB64Url;
// Send $shareUrl to recipient via email
// Send $passphrase to recipient via a DIFFERENT channel (SMS, phone call, etc.)
echo "URL: {$shareUrl}
";
echo "Passphrase (send separately!): {$passphrase}
";
Files are encrypted as a single chunk (the entire file content). Before encryption, a small JSON metadata header
({"name":"…","size":…,"type":"…"} ) is prepended so the browser can reconstruct the
filename and MIME type when the recipient downloads.
The flow: create a secret with is_file: true → upload the encrypted chunk →
build the share URL. The recipient's browser downloads the chunk, decrypts it, strips the metadata header, and offers a download.
<?php
$filePath = '/path/to/document.pdf';
$fileName = basename($filePath);
$fileBytes = file_get_contents($filePath);
$mimeType = mime_content_type($filePath) ?: 'application/octet-stream';
$passphrase = 'optional-passphrase'; // set to '' or null to skip
$key = sodium_crypto_secretbox_keygen(); // 32 random bytes
$nonce = random_bytes(12); // 12 bytes for AES-GCM
// ── Step 1: Build payload (metadata header + raw file bytes) ─────────────────
// The browser uses this header to restore the filename and offer a download.
$meta = json_encode(['name' => $fileName, 'size' => strlen($fileBytes), 'type' => $mimeType]);
$combined = $meta . " " . $fileBytes; // null byte separates header from content
// ── Step 2: Encrypt with AES-256-GCM ─────────────────────────────────────────
$ciphertext = openssl_encrypt(
$combined,
'aes-256-gcm',
$key,
OPENSSL_RAW_DATA,
$nonce,
$tag
);
$ciphertextWithTag = $ciphertext . $tag; // tag is always 16 bytes
// ── Step 3: Build passphrase verifier (optional) ─────────────────────────────
$verifier = $passphrase
? base64_encode(hash_hmac('sha256', $passphrase, $key, true))
: null;
// ── Step 4: Create secret record with is_file = true ─────────────────────────
$apiKey = 'sk_your_api_key_here';
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$baseUrl = $scheme . '://' . $_SERVER['HTTP_HOST'];
$secretPayload = array_filter([
'ciphertext' => base64_encode($ciphertextWithTag),
'nonce' => base64_encode($nonce),
'ttl_hours' => 48,
'max_views' => 1,
'is_file' => true,
'passphrase_verifier' => $verifier,
]);
$ch = curl_init($baseUrl . '/api/secrets');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($secretPayload),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey,
],
]);
$secretData = json_decode(curl_exec($ch), true);
curl_close($ch);
$secretId = $secretData['id'];
// ── Step 5: Upload the encrypted chunk (chunk index 0) ────────────────────────
// For files larger than your memory limit, split into chunks and upload each
// with an incrementing chunk_index (0, 1, 2, …).
$ch = curl_init("{$baseUrl}/api/files/{$secretId}/0");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['ciphertext' => base64_encode($ciphertextWithTag)]),
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey,
],
]);
$chunkResult = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($chunkResult['stored'])) {
throw new RuntimeException('Chunk upload failed: ' . json_encode($chunkResult));
}
// ── Step 6: Build share URL ───────────────────────────────────────────────────
$keyB64Url = rtrim(strtr(base64_encode($key), '+/', '-_'), '=');
$shareUrl = "{$baseUrl}/s/{$secretId}#key={$keyB64Url}";
echo "Share URL: {$shareUrl}
";
if ($passphrase) {
echo "Passphrase (send via separate channel): {$passphrase}
";
}
All errors return a JSON object {"error": "message"} with an appropriate HTTP status code.
Rate limits are enforced per tenant per hour. The current limit for Enterprise is 500 creates/hour and 500 reads/minute. Check remaining quota with GET /api/rate-status. Exceeded limits return 429 Too Many Requests.
200 OK 201 Created 400 Bad Request - missing or malformed parameter 401 Unauthorized - missing or invalid API key 403 Forbidden - plan restriction or invalid token 404 Not Found - secret consumed, expired, or not found 422 Unprocessable - validation error 429 Too Many Requests - rate limit exceeded
Creates a new zero-knowledge secret. Encrypt your payload client-side before calling this endpoint.
| Parameter | Type | Description |
|---|---|---|
| ciphertextrequired | string | Base64-encoded encrypted payload (XSalsa20-Poly1305) |
| noncerequired | string | Base64-encoded 24-byte nonce used during encryption |
| ttl_hoursoptional | integer | Time-to-live in hours. Default: 24. Max: 720 (enterprise) |
| max_viewsoptional | integer | Maximum number of views before deletion. Default: 1. Max: 100 |
| passphrase_verifieroptional | string | HMAC-based passphrase verifier. Server stores bcrypt hash only. |
| is_fileoptional | boolean | Set true if secret references encrypted file chunks |
| policyoptional | object | Additional policy metadata (JSON object) |
POST /api/secrets
Authorization: Bearer sk_…
Content-Type: application/json
{
"ciphertext": "base64encodedCiphertext==",
"nonce": "base64encodedNonce==",
"ttl_hours": 48,
"max_views": 1
}
{
"id": "uuid-of-secret",
"delete_token": "hex-token-for-deletion",
"expires_at": "2026-03-11T12:00:00+00:00"
}
delete_token to revoke it later.Atomically fetches and consumes a secret. Once all views are consumed the secret is permanently deleted. For passphrase-protected secrets use POST /api/secrets/:id with the verifier in the body.
| Parameter | Type | Description |
|---|---|---|
| idrequired | string (path) | Secret UUID |
| passphrase_verifieroptional | string (query) | Required if secret is passphrase-protected |
{
"ciphertext": "base64encodedCiphertext==",
"nonce": "base64encodedNonce==",
"is_file": false,
"policy": {}
}
Permanently deletes a secret before it expires or is consumed. Requires the delete_token returned at creation time.
| Parameter | Type | Description |
|---|---|---|
| delete_tokenrequired | string (body) | Token returned by POST /api/secrets |
{"deleted": true}Returns the current rate limit status for your tenant.
{
"limit": 500,
"used": 12,
"remaining": 488,
"reset_at": 1741518000,
"plan": "enterprise"
}
Uploads a single encrypted chunk of a file. Split large files into chunks, encrypt each client-side, upload sequentially starting from index 0. The secret must be created first with is_file: true.
| Parameter | Type | Description |
|---|---|---|
| ciphertextrequired | string | Base64-encoded encrypted chunk data |
{"stored": true}{"ciphertext": "base64…"} - decrypt client-sideRequires an active browser session (logged in as Enterprise user). Generates a new sk_… API key - shown only once. Calling again rotates the key.
| Parameter | Type | Description |
|---|---|---|
| actionoptional | string | generate (default) or revoke |
{
"api_key": "sk_4a7f1b3c…",
"note": "Store this key securely - it will not be shown again."
}
Ready to use the API?
API access is available on the Enterprise plan. Upgrade to get full programmatic access, higher rate limits, and Swagger UI for interactive testing.
View Enterprise Plan Login