Read submissions
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
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
| Status | error_code | When |
|---|---|---|
| 400 | 44 | No short code in the path. GET /v1/requests/submissions is not a bulk read. |
| 403 | 78 | The key lacks requests:read. |
| 404 | 70 | No such request on this account. |
| 429 | 107 | Rate limit exceeded. Retry-After gives the seconds to wait. |
| 500 | 43 | The 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.