Skip to content
CredenShare
API Reference

Shares

The end-to-end encrypted share resource, and the client-side work a create requires.

A share is a piece of content that your client encrypts before it reaches CredenShare. The API stores and serves the ciphertext you hand it, and never holds the key that opens it.

That one fact shapes every endpoint on this resource. It is also why creating a share is more work than a single request, so read this page before the endpoint pages.

The API does not accept plaintext. encryption_type has exactly one accepted value, e2ee-aes256-gcm, and there is no flag, opt-in or per-account exception. A server cannot encrypt what it never sees.

The end-to-end flow

Four of these five steps happen in your code. The official SDKs do all five — reach for this section when you are implementing them yourself.

A create is a five-step operation, and only one of those steps is a request to us.

These are the primitives you need:

PrimitiveParameters
HashSHA-256
KDFHKDF-SHA-256 (RFC 5869), extract-and-expand
AEADAES-256-GCM, 96-bit IV, 128-bit tag

HKDF-SHA256(ikm, salt, info, len) below takes info as UTF-8 bytes. Where a salt is shown as "", pass a zero-length byte string. Do not substitute a non-zero placeholder — that changes the derived key and your ciphertext will not decrypt.

1. Generate a content key

The content key is 32 random bytes from a cryptographically secure source. It is the only thing that can decrypt the share, and CredenShare never receives it.

2. Build the field array

The plaintext is a JSON array of field objects:

[
  { "key": "Database password", "value": "s3cr3t", "type": "password" },
  { "key": "Host", "value": "db.internal.example", "type": "text" }
]
MemberRequiredNotes
keyyesThe field's visible label — what the recipient reads.
valueyesThe field's content.
typeyesOne of text, password, date, multiline, markdown, source_code. Decides how the recipient's page renders it: password is masked behind a reveal, source_code is syntax-highlighted, markdown is rendered.
selectedProgrammingLanguagenoLanguage hint for source_code.
filenamenoFor source_code and markdown, offers the recipient a download using this name.
The label member is key. It is not label, name or title — those are ignored silently. A share built with the wrong member name still encrypts, still posts, still decrypts and still renders, with every field label blank and nothing anywhere reporting an error.

3. Encrypt

salt       = 16 random bytes
iv         = 12 random bytes
key        = HKDF-SHA256(contentKey, salt, "content", 32)
ciphertext = AES-256-GCM(key, iv, utf8(JSON.stringify(fields)))

data       = base64(salt || iv || ciphertext+tag)

data uses standard base64 with padding: it travels in a JSON body, never in a URL. AES-GCM output is ciphertext || tag in a single buffer; if your AEAD returns them separately, concatenate in that order.

If the share has a passcode, the info string becomes content|<passcode> instead of content. The passcode is mixed into info, never into the salt.

4. Derive the access token

access_token = base64url(HKDF-SHA256(contentKey, "", "access", 32))

The salt is empty on purpose, so the recipient's browser can reproduce this value from the link fragment alone with nothing stored. CredenShare stores only a hash of the token, and HKDF's domain separation means the access output tells us nothing about the content output.

base64url here is URL-safe base64 with no padding.

5. POST the ciphertext, then assemble the link yourself

Send the create request with an Idempotency-Key header. You get back a short code:

{
  "short_code": "a1b2c3d4",
  "expired_at": "2026-09-01T00:00:00Z",
  "custody": "none"
}

The recipient link is the short code plus the content key in the URL fragment:

https://crs.sh/{short_code}#1{base64url(contentKey)}

The fragment is bare: a single version character 1, then the base64url key, with no k= prefix. Browsers never transmit a fragment to a server, which is how the key reaches the recipient without reaching us.

You are the only party who can build a working link. If you lose the content key between step 1 and step 5, nobody can open that share — including you and including CredenShare. There is no recovery; create the share again.

Why the response contains no URL

The create response returns a short code, not a link, and this is deliberate rather than an omission.

A working link contains the content key. CredenShare has never had that key, so there is nothing we could put in a url field that would actually open the share. Returning a keyless link would be worse than returning none: it would look correct and fail at the moment the recipient used it.

The same reasoning removes several things you might expect elsewhere on this resource. No endpoint returns share content, and no endpoint returns anything from which a link could be reconstructed.

Endpoints

MethodPathScopePurpose
POST/v1/sharesshares:writeCreate a share from ciphertext you produced
GET/v1/sharesshares:readList your shares, metadata only
GET/v1/shares/{shortCode}shares:readRetrieve one share's metadata
DELETE/v1/shares/{shortCode}shares:writeExpire a share now

Scopes are matched by exact string. There is no hierarchy: shares:write does not imply shares:read, so a key that both creates and lists needs both scopes.

Base URL

https://api.credenshare.io/v1

Every path on this page is relative to that base.

Authentication

Every request carries a bearer credential:

Authorization: Bearer crs_sk_live_<keyId>.<authSecret>

An API credential issued to you has three dot-separated parts:

crs_sk_live_<keyId>.<authSecret>.<custodySecret>

Send only the first two. The third part derives your custody keypair locally and must never be transmitted — a request carrying all three is refused outright, and that credential should be treated as disclosed and rotated.

API access is a Business and Enterprise capability. On a plan without it, minting a key is refused with error_code 98 and the message API access requires a Business or Enterprise plan.

Keys are minted in the dashboard, under Security on your account page. There is no API endpoint that creates, lists or revokes keys: a leaked key that could mint more keys would be a far worse leak than one that cannot.

See Authentication for scopes, custody levels and rate limits in full.

Reading content back

There is no content-read endpoint on this API, for your own shares or anyone else's.

The recipient read path is anonymous and protected by proof-of-work and a captcha. Bearer authentication skips both, so exposing that path to an API key would turn the API into a way to enumerate short codes. A key reads only metadata, and only for what your account owns.

If you want an API-created share to be readable later from your dashboard rather than only from the link, send an item_key_wrap on the create.

Response and error shape

Successful responses are bare JSON objects — there is no data wrapper and no meta block.

Errors use one envelope, with an integer error_code:

{
  "success": false,
  "message": "Validation failed",
  "error_code": 19,
  "additional_data": {
    "data": "data is a required field"
  }
}

additional_data carries a map of JSON field name to message on a validation failure, and is absent otherwise. There is no request id.

One class of failure does not use that envelope. A request with a missing or unusable credential is refused before it reaches the API, and the body is the gateway's own, not ours. Handle it by status code rather than by parsing the body.

An unknown key, a revoked key and a key with the wrong secret are deliberately indistinguishable to the caller: telling them apart would let anyone holding no credential at all learn which key ids exist. Your logs are the place to diagnose a credential; the response is not.

Every response carries Access-Control-Allow-Origin: *. The only rate-limit header the API emits is Retry-After, and only on a 429.

Every code you can receive is listed in Errors and Rate Limits.