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
Everywhere else, an API read returns metadata, 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: it lives as a seed in the owner's access-link fragment, or wrapped under their account key. We hand back something we cannot open, to the party who can.
That is what makes an ephemeral automation possible. A runner derives its keypair, is granted the account key once, and can then unwrap every submission it is handed without keeping any local state.
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.
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 | 11 | 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.