Skip to content
CredenShare
Secure requests

Read submissions

GET /v1/requests/{shortCode}/submissions — the ciphertext somebody submitted, which only you can open.
GET /v1/requests/{shortCode}/submissions

Requires the requests:read scope. This is the only read on the /v1 surface that returns content.

Why this endpoint is allowed to return content

Every other API read returns metadata, apart from the sealed account key that GET /v1/custody returns to a key granted account custody. Content stays off them because a bearer key skips the proof-of-work and captcha challenges that guard the anonymous recipient page — exposing recipient reads to a key would turn the API into an enumeration bypass.

This endpoint is not an exception to that rule; it is the rule working. Each submission is sealed to the request's public key. The private half never reached us in the clear: it lives as a seed in the owner's access-link fragment, or wrapped under their account key, which we hold only sealed under secrets we do not keep — their passphrase, which never reaches us, or the custody secret of an API key they granted account custody (as the runner below is), which we generate when the key is created, return once and never store. We hand back something we cannot open, to the party who can.

The party who can is whoever holds the request's private key. Account custody does not stand in for it: the /v1 surface does not return the request's seed or any wrapped copy of it, so a runner granted the account key still needs that private key to decrypt what this endpoint returns. For a request created through the API, that is the private key matching the public_key sent on create, so keep it where the runner can reach it.

Request

curl https://api.credenshare.io/v1/requests/a1b2c3d4/submissions \
  -H "Authorization: Bearer crs_sk_live_<keyId>.<authSecret>"

Success response

200 OK:

{
  "submissions": [
    {
      "short_code": "e5f6g7h8",
      "created_at": "2026-09-03T14:22:11Z",
      "data": "<base64 ciphertext>",
      "encryption_type": "e2ee-aes256-gcm"
    }
  ],
  "count": 1
}

count is the number of entries in submissions, which is not necessarily the number of submissions the request received — see the next section.

data is ciphertext. Decrypt it with the private key matching the public_key you supplied on create. submissions is always an array; a request nobody has filled in yet returns [] and count: 0.

Each submission has its own short_code, distinct from the request's.

This read does not ask for the Password protect share password that a request created in the app may set, and it does not start a First View countdown. Opening a submission in the app's viewer does both. A First View submission that is only ever read here expires 30 days plus its after-open time after it was submitted. Expired submissions are not returned, because their content was deleted when they expired.

Legacy submissions are skipped, and counted

Submissions that are not client-encrypted are omitted from the response rather than returned.

A request that predates end-to-end encryption may hold submissions stored under the older server-side encryption types. For those rows the server can decrypt, so returning them would make this the one place in the product where a bearer-authenticated call yields readable secrets. They are skipped.

The omission is named in the response rather than merely logged:

{
  "submissions": [],
  "count": 0,
  "skipped_not_end_to_end_encrypted": 2
}

The key is present only when the count is greater than zero. Without it you would see fewer submissions than your dashboard shows and have no way to learn why. If you see it, retrieve those submissions through the app instead.

Check encryption_type on each entry you do receive; every returned row is client-encrypted, but reading the field rather than assuming keeps your client correct if another client-side type is added later.

Ownership

Ownership is checked twice: once in the handler before anything else runs, and again inside the export path against the viewing user. Submissions are the most sensitive thing this API can return, so the check is not delegated to a service that might later relax it for the recipient path.

A short code on another account returns 404, the same as a short code that does not exist.

Errors

Statuserror_codeWhen
40044No short code in the path. GET /v1/requests/submissions is not a bulk read.
40378The key lacks requests:read.
40470No such request on this account.
429107Rate limit exceeded. Retry-After gives the seconds to wait.
50043The submissions could not be read.

Composition

request.submitted fires
  → GET /v1/requests/{shortCode}/submissions
  → decrypt with your private key
  → use the credential

Subscribing to request.submitted is cheaper and faster than polling this endpoint. See Webhook events.