Secure requests
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 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.
| Key | Generated by | Held by | Sees the plaintext |
|---|---|---|---|
| Request public key | You, the caller | Sent to us on create, returned on reads | No — it only encrypts |
| Request private key | You, the caller | Never sent to us | Yes — this is what opens submissions |
| Submission ciphertext | The submitter's browser | Stored by us, returned to you | No — 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
- Generate a P-256 keypair in your own code. Keep the private half.
- Create the request with a title, the fields to ask for, and the public half. You get back a
short_code. - Assemble the collect link —
https://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. - The person fills the form. Their browser encrypts each value to your public key.
- 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 path | What it does | Scope |
|---|---|---|
POST /v1/requests | Create a request. Returns a short code. | requests:write |
GET /v1/requests | List 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}/submissions | The 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.