API Reference
The CredenShare API lets you work with shares and secure requests from your own code — CI pipelines, provisioning scripts, internal tools. It works on ciphertext you encrypt yourself, so the service stores your secret without ever being able to read it.
There are three resources: shares send a secret to somebody, secure requests collect one from them, and stats reports usage. The same account can also be driven by an AI assistant through the hosted MCP server.
Base URL
https://api.credenshare.io/v1
Every path on this page is relative to that base. There is no version header and no stage segment in the path.
A development host, https://api-dev.credenshare.io/v1, runs the same code if you want somewhere to rehearse against before pointing at production. The rest of this documentation uses the canonical base URL only.
Authentication
Send your API key as a bearer token:
curl https://api.credenshare.io/v1/shares \
-H "Authorization: Bearer crs_sk_live_<keyId>.<authSecret>"
A full credential has three dot-separated parts — crs_sk_live_<keyId>.<authSecret>.<custodySecret>. Only the first two are ever transmitted. The third part is a client-side secret used to derive your custody keypair; a credential that arrives with all three parts is refused outright and the key must be rotated.
Keys are minted in the dashboard under Account → Security, and the credential is displayed exactly once. There is no API route that creates, rotates or revokes keys: a leaked key that could mint more keys would be a far worse leak, so key management stays session-authenticated.
See Authentication for scopes, custody levels and the key lifecycle.
The encryption model — read this first
POST a password and get a URL back will fail on its first call.CredenShare shares are encrypted in the client. The content key is never sent to the server — it travels to the recipient in the URL fragment of the share link, and browsers do not transmit the fragment to the origin. That single fact shapes the whole API:
- You encrypt before you call.
datais your ciphertext, stored byte-for-byte.encryption_typemust be the literal stringe2ee-aes256-gcm; any other value, including the server-side encryption types used elsewhere in the product, is refused with a400:{ "success": false, "message": "The API accepts end-to-end encrypted content only. Encrypt client-side and send encryption_type=e2ee-aes256-gcm.", "error_code": 19 } - You must supply an
access_token. It is derived by your client from the content key. The server stores only a SHA-256 hash of it and cannot recompute it; the recipient's browser presents the same token when it opens the share. A create withoutaccess_tokenfails validation. - The create response has no
urlfield. It returns ashort_code. You assemble the link, because only you hold the key:https://crs.sh/<short_code>#<key material your client appends>
There is nothing the API could put in aurlfield that would open, so it does not offer one. - There is no endpoint that reads share content. A bearer key skips the proof-of-work and captcha challenges that guard the anonymous recipient page, so exposing recipient reads to a key would turn the API into an enumeration bypass. Reads through the API return metadata only.
- API-created shares are not readable from the dashboard unless you say so. Send an
item_key_wrapon create — a wrap of the content key to your key's custody public key — and the share becomes recoverable from your account later. This requires a key minted with custodyselfor higher; a wrap sent by a key with custodynoneis refused rather than ignored. Omit the wrap and the ciphertext is openable only by whoever holds the link. Thecustodyfield in the create response reports which happened.
Endpoints
| Method and path | What it does | Scope |
|---|---|---|
POST /v1/shares | Create a share from ciphertext. Returns a short code. | shares:write |
GET /v1/shares | List the shares this key created, newest first. Metadata only. | shares:read |
GET /v1/shares/{shortCode} | One share's metadata. Consumes no view and runs no passcode check. | shares:read |
DELETE /v1/shares/{shortCode} | Expire a share your account owns. | shares:write |
POST /v1/requests | Create a secure request. Returns a short code for a collect link. | requests:write |
GET /v1/requests | List the requests your account owns, newest first. Metadata only. | requests:read |
GET /v1/requests/{shortCode} | One request's metadata. | requests:read |
GET /v1/requests/{shortCode}/submissions | The submissions to one request, including ciphertext. | requests:read |
DELETE /v1/requests/{shortCode} | Expire an active request, or delete an already-expired one. | requests:write |
GET /v1/stats | Share counts and a 14-day view history. Figures only. | stats:read |
POST /v1/mcp | The hosted MCP server. JSON-RPC 2.0, not REST. | Per tool |
That is the whole surface. Full detail is on Shares, Secure requests, Stats and MCP server.
Two things follow from the table that are easy to miss:
GET /v1/requests/{shortCode}/submissionsis the one read that returns content. Everything else is metadata. It is not an exception to the encryption model — submissions come back sealed to a key you generated and we never received. See Read submissions.DELETE /v1/requests/{shortCode}is not idempotent. The first call expires; a second call on an already-expired request permanently deletes it, submissions included. The shareDELETEhas no such second behaviour. See Expire or delete a request.
Short codes are opaque. Do not parse them, and do not assume a length or character set.
Response shapes
Successes and errors are shaped differently on this surface. Handle both.
Successes are bare JSON objects. There is no data wrapper, no meta block and no request id.
{
"short_code": "…",
"expired_at": "2026-09-03T12:00:00Z",
"custody": "stored"
}
The list endpoint returns two keys:
{
"shares": [
{ "short_code": "…", "expired_at": "2026-09-03T12:00:00Z" }
],
"pagination": { "page": 1, "limit": 25, "total_pages": 4, "total": 87 }
}
DELETE is the one success that carries an envelope, and it returns 200 with a body — not 204:
{ "success": true, "message": "ok" }
Every expired_at is either an RFC 3339 string or JSON null.
Errors use a flat envelope with an integer error_code:
{
"success": false,
"message": "Validation failed",
"error_code": 19,
"additional_data": {
"encryption_type": "<why this field was rejected>"
}
}
Branch on error_code, not on message — messages are prose and may be reworded. additional_data is present on validation failures, where it maps each rejected JSON field name to a human-readable reason. See Errors and rate limits for the full code table.
Idempotency
POST /v1/shares requires an Idempotency-Key request header. It exists because a retried create that is not recognised as a retry mints a second copy of a secret.
curl -X POST https://api.credenshare.io/v1/shares \
-H "Authorization: Bearer crs_sk_live_<keyId>.<authSecret>" \
-H "Idempotency-Key: deploy-2026-09-03-db-password" \
-H "Content-Type: application/json" \
-d '{ ... }'
Choose the value yourself — anything non-blank. Reuse semantics:
| Situation | Result |
|---|---|
| Header missing or whitespace only | 400, error_code 104 |
| Key reused, request body byte-identical, first call finished | 200 with the first call's stored response body, replayed verbatim |
| Key reused, request body differs by even one byte | 409, error_code 105 |
| Key reused while the first call is still in flight | 409, error_code 106 |
Two details decide whether a retry is recognised:
- What is hashed is the raw request body, byte for byte — a SHA-256 of exactly the bytes you sent. A retry that re-serialises the same object with different key ordering, different whitespace, or a different float formatting hashes differently, and is refused as a reuse rather than replayed. To retry safely, send the identical byte string; build the body once, keep it, and resend it.
- Keys are scoped to the API key that used them, and retained for 24 hours. After 24 hours the same value is a fresh key. Two different API keys may use the same value without colliding.
Note that a successful replay returns 200, not the original 201. Treat both as success.
Plans
API access is a Business and Enterprise capability. On any other plan the entitlement resolves to off, and minting a key is refused with 403 and error_code 98, "API access requires a Business or Enterprise plan". An unset rate limit is treated as no access rather than as unlimited, so an older plan that predates the feature does not inherit it.
Request-rate limits, the plan share allowance and every error code are on Errors and rate limits.