Skip to content
CredenShare
API Reference

API Reference

The CredenShare REST API — end-to-end encrypted shares, secure requests and usage stats, driven from your own code.

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.

Working in Node, Python, Go or Rust? The official SDKs perform the encryption, derive the tokens and assemble the link for you, so you pass plaintext fields and get back a finished link. This reference is what you need if you are implementing the wire format yourself, or working in another language.

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

This API cannot accept a plaintext secret, and it cannot return a share link. Both are consequences of the encryption model, not gaps in the interface. Code written on the assumption that you can 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. data is your ciphertext, stored byte-for-byte. encryption_type must be the literal string e2ee-aes256-gcm; any other value, including the server-side encryption types used elsewhere in the product, is refused with a 400:
    {
      "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 without access_token fails validation.
  • The create response has no url field. It returns a short_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 a url field 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_wrap on 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 custody self or higher; a wrap sent by a key with custody none is refused rather than ignored. Omit the wrap and the ciphertext is openable only by whoever holds the link. The custody field in the create response reports which happened.

Endpoints

Method and pathWhat it doesScope
POST /v1/sharesCreate a share from ciphertext. Returns a short code.shares:write
GET /v1/sharesList 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/requestsCreate a secure request. Returns a short code for a collect link.requests:write
GET /v1/requestsList 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}/submissionsThe 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/statsShare counts and a 14-day view history. Figures only.stats:read
POST /v1/mcpThe 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}/submissions is 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 share DELETE has no such second behaviour. See Expire or delete a request.
Webhook endpoints are created and re-pointed in the dashboard, not through this API — an API key must not be able to redirect your event stream. The delivery contract, signing and event catalogue are documented under Webhooks.

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.

Authentication failures are the exception to both shapes: they are produced by the gateway before your request reaches the API, so they do not use the error envelope at all. Errors and rate limits shows exactly what you get.

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:

SituationResult
Header missing or whitespace only400, error_code 104
Key reused, request body byte-identical, first call finished200 with the first call's stored response body, replayed verbatim
Key reused, request body differs by even one byte409, error_code 105
Key reused while the first call is still in flight409, 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.