Skip to content
CredenShare
API Reference

Secure requests

Ask somebody else to hand you a secret, over a link that carries no key and that CredenShare cannot read.

A share moves a secret to somebody. A secure request moves one from them.

You create a request, give the person a collect link, and they type the credential into their own browser. Their browser encrypts it to the request's public key before it leaves their machine. You fetch the ciphertext later and open it with a private key we have never held.

That is the whole reason this resource exists separately from shares: the secret starts on somebody else's keyboard, so no step of the flow can involve you knowing it in advance.

The collect link contains no key material. Unlike a share link, it is safe to paste into chat, email or a ticket — the encryption target is the request's public key, which is public by construction. This is why an automation can hand out a collect link and a share link cannot be handed out the same way.
Two different links share that path, and they differ only by a fragment. Truncated in a log, a chat client or a ticket preview, the two are indistinguishable — so if your automation ever holds an access link, treat it with the care you would give a private key, and make sure the value you hand to a human is the one without a fragment.
  • The collect link you hand out: https://crs.sh/r/<short_code>
  • The owner's access link: https://crs.sh/r/<short_code>#<seed> — that fragment is the private key seed, everything needed to decrypt every submission to the request.

Who holds which key

This is the part worth getting right before you write any code.

KeyGenerated byHeld bySees the plaintext
Request public keyYou, the callerSent to us on create, returned on readsNo — it only encrypts
Request private keyYou, the callerNever sent to usYes — this is what opens submissions
Submission ciphertextThe submitter's browserStored by us, returned to youNo — we cannot open it

public_key is a P-256 public key, base64url, and it is required on this surface. The app tolerates a request without one only for legacy rows; an API caller who omits it would be creating a request that nobody can encrypt to, and would find out when a submitter's browser had nowhere to send the value.

The private half is yours to keep. In the app it exists as a seed in the owner's access-link fragment or wrapped under their account key; over the API, it exists wherever you put it.

The flow

  1. Generate a P-256 keypair in your own code. Keep the private half.
  2. Create the request with a title, the fields to ask for, and the public half. You get back a short_code.
  3. Assemble the collect linkhttps://crs.sh/r/<short_code> — and send it to the person. As with shares, the API returns a short code rather than a URL, because only you know which origin your recipients use.
  4. The person fills the form. Their browser encrypts each value to your public key.
  5. Fetch the submissions and decrypt them with your private key.

Step 5 is the only read on the whole /v1 surface that returns content rather than metadata — and it is the metadata-only rule working rather than an exception to it. What comes back is sealed to a key we do not have. We are willing to hand it over precisely because we cannot open it.

Endpoints

Method and pathWhat it doesScope
POST /v1/requestsCreate a request. Returns a short code.requests:write
GET /v1/requestsList the requests this key's account owns, newest first.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 permanently delete an already-expired one.requests:write

Scopes are matched exactly. requests:write does not imply requests:read, and neither is implied by the shares:* scopes — a key that can create shares cannot read submissions unless you granted it that.

What reads return

Request reads are metadata only, and the shape is deliberately narrow:

{
  "short_code": "…",
  "expired_at": "2026-09-30T12:00:00Z",
  "public_key": "…"
}

public_key is returned, which is the opposite of how share key material is treated. It is the public half, you supplied it, and you need it back to verify what was stored against what you generated. The private half never existed on our side, so there is nothing else here to withhold.

Titles, descriptions and field prompts are not returned. Neither is anything about who submitted.

Ownership

Every endpoint on this resource checks ownership before anything else, and reports a failure as not found rather than forbidden. A key cannot be used to discover that a short code exists on another account.

An owner asking about their own expired request gets its metadata rather than a 404 — expiry is not the same as absence, and an automation reconciling its own records needs to tell those apart.

Composition with webhooks

The pairing this resource was built for:

request.submitted fires
  → your automation calls GET /v1/requests/{shortCode}/submissions
  → it decrypts with the private key it holds
  → it uses a credential a human handed over through a keyless link

Before this surface existed, an automation could be told a submission had arrived and had no way to fetch it. See Webhook events for the event contract.