Create a share
POST /v1/shares
Requires the shares:write scope. Creates a share from ciphertext your client produced, and returns the short code you use to assemble the recipient link.
If you have not read the end-to-end flow, start there — four of the five steps in a create happen on your side, and this page only covers the request itself.
The Idempotency-Key header is required
Idempotency-Key: deploy-42
400 and error_code 104.It is required because a retried automation must not duplicate a secret. A second share is a second copy of a credential in the world, with its own link and its own audit trail, that your caller does not know exists.
Any unique string you can reproduce on retry will do. The value is scoped to your API key, so it cannot collide with another customer's, and it is retained for 24 hours.
Request fields
| Field | Type | Required | Notes |
|---|---|---|---|
title | string | yes | Max 256 characters. Visible to CredenShare — it is metadata, not encrypted content. |
data | string | yes | Your ciphertext. See below. |
encryption_type | string | yes | Exactly one accepted value: e2ee-aes256-gcm. |
access_token | string | yes | Derived from the content key. See below. |
description | string | no | Max 1024 characters. Also visible to CredenShare. |
passcode_verifier | string | no | A derived verifier, never the passcode. See below. |
expired_at | string | no | When the share stops working. Accepted formats below. |
access_counts_left | integer | no | View limit, from 1 to 10000. |
timed_view | integer | no | Seconds the content stays visible once opened, from 5 to 86400. |
organization_id | string | no | Omit it. The organization is taken from the key, and a value that disagrees is refused — see below. |
item_key_wrap | string | no | Makes the share readable from your dashboard later. See below. |
organization_id that matches, it is accepted;
if you send one that does not, the create is refused rather than quietly filed somewhere else.
A key with no team binding cannot attribute a share to one at all.Keep secrets out of title and description. Those two fields are exactly the ones we can read.
data
data is standard base64 of salt(16) || iv(12) || ciphertext+tag, as described in the flow.
CredenShare stores and serves this string verbatim. It is not parsed, not re-encoded and not encrypted again — there is no server-side encryption step on this path, because the content arrived already encrypted.
Nothing validates its internal structure either, so a malformed blob is accepted on create and fails only when a recipient tries to open it. Verify a new integration by opening one of its shares, not by trusting the 201.
access_token
access_token = base64url(HKDF-SHA256(contentKey, "", "access", 32))
This is the value the recipient must present to read the share, and it is what binds a link to its content. Their browser recomputes it from the key in the URL fragment, so you do not send it to the recipient separately.
CredenShare stores only SHA-256 of the token's trimmed value. The token itself is not retained, and it is not a decryption key — possession of it proves possession of the link and nothing more.
passcode_verifier
passcode_verifier = base64url(HKDF-SHA256(utf8(passcode), "", "verify", 32))
Send the verifier, never the passcode itself. The passcode is mixed into the derivation of your content key, so handing us the passcode would hand us a component of that key — which is the whole thing this design exists to avoid. The verifier is one-way, so the server can check an attempt without gaining the ability to decrypt.
Passcode protection is a plan feature. A create carrying a passcode_verifier on a plan without view protection is refused with a 403 and error_code 53.
item_key_wrap
An API-created share is normally readable only from its link, because only the link carries the key. item_key_wrap is how you keep a copy readable from your dashboard: it is the content key wrapped to your own key's custody public key.
Only you can compute it. The wrap is made to the custody keypair derived from the third part of your credential, which never leaves your machine, so we can store it without being able to open it.
Two conditions apply:
- Your API key must be at custody level
selforaccount. A key at custody levelnonesending a wrap gets a400with the messageThis key has custody level 'none', so an item_key_wrap would never be readable. Mint a key with custody 'self' or omit the wrap.It is refused rather than dropped on purpose — a silently ignored wrap means discovering months later that the content was never reachable. - Your account must have a zero-knowledge account key enrolled, and the zero-knowledge plan feature. When that is not the case the share is still created and the response reports
"custody": "failed".
The subject of the wrap is always the acting API key, taken from the verified credential. There is no request field for it and no way to name a different one.
Omit item_key_wrap when the caller keeps the link and nothing else needs to read the item.
expired_at
expired_at is a datetime, not a duration. Values like 24h are rejected.
| Format | Example |
|---|---|
| RFC 3339 with fractional seconds | 2026-09-01T00:00:00.123456789Z |
| RFC 3339 with a zone or offset | 2026-09-01T00:00:00Z or 2026-09-01T02:00:00+02:00 |
| Date and time with no zone | 2026-09-01T00:00:00 |
| Date only | 2026-09-01 |
A value with no timezone is interpreted as UTC. An unparseable value is a 400.
Request
curl -X POST https://api.credenshare.io/v1/shares \
-H "Authorization: Bearer crs_sk_live_<keyId>.<authSecret>" \
-H "Idempotency-Key: deploy-42" \
-H "Content-Type: application/json" \
-d '{
"title": "Deploy credentials",
"encryption_type": "e2ee-aes256-gcm",
"data": "oKGio6SlpqeoqaqrrK2ur7CxsrO0tba3uLm6u...",
"access_token": "d2DJ6L1GBXjLD-YhpdyVxJQNkLOHIovvBRUbSMcZf0A",
"expired_at": "2026-09-01T00:00:00Z",
"access_counts_left": 1
}'
Success response
201 Created, with exactly three keys:
{
"short_code": "a1b2c3d4",
"expired_at": "2026-09-01T00:00:00Z",
"custody": "none"
}
| Field | Type | Notes |
|---|---|---|
short_code | string | The share's public identifier. Use it to build the link, and as the path parameter on the other endpoints. |
expired_at | string or null | RFC 3339, or null when you sent no expired_at. |
custody | string | What happened to item_key_wrap: none, stored or failed. |
url field and no id field. The response returns a short code because a link would have to contain the content key, which CredenShare never receives. Assemble the link yourself as https://crs.sh/{short_code}#1{base64url(contentKey)}.The custody field
| Value | Meaning |
|---|---|
none | No item_key_wrap was sent. |
stored | The wrap was persisted. The share is readable from your dashboard. |
failed | The share exists, but the wrap could not be stored. The share is readable only from its link. |
failed comes back with a 201, not an error. By the time the wrap is attempted the share already exists, so reporting a failure would make you retry with a fresh idempotency key and mint a second copy of the secret. The wrap is additive: losing it costs dashboard visibility, not the share.
Idempotent replay
Repeating a create with an Idempotency-Key that has already completed returns 200 OK — not 201 — with the first call's response body verbatim, including the same short code.
The request body is fingerprinted with SHA-256 over the raw bytes received. A retry must therefore be byte-identical: reformatted JSON or reordered keys count as a different body and are refused with a 409, even when the meaning is unchanged. Send the same serialized bytes you sent the first time.
Claims are scoped to your API key and expire after 24 hours.
Order of checks
A create runs its checks in a fixed order, and stops at the first failure. This matters when you are debugging a status code you did not expect:
- Scope — is
shares:writeon the key? Idempotency-Key— is the header present?- JSON — does the body parse?
- Field validation — are the required fields present and within their limits?
- Plan share allowance — do you have shares left this period?
encryption_type— is ite2ee-aes256-gcm?item_key_wrap— is the key's custody level high enough?- Idempotency claim.
So a request that sends the wrong encryption_type while your plan's share allowance is spent returns the 403 for the allowance, not the 400 for the encryption type. Fix the earlier check first and the later one will surface.
Errors
| Status | error_code | When |
|---|---|---|
| 400 | 104 | The Idempotency-Key header is missing or blank. |
| 400 | 19 | Field validation failed. additional_data maps each rejected field to a message. |
| 400 | 19 | encryption_type is not e2ee-aes256-gcm, or an item_key_wrap was sent by a none-custody key. Both carry their own message. |
| 400 | — | The body is not valid JSON. |
| 403 | 78 | The key lacks the shares:write scope. |
| 403 | 61 | Your plan's share allowance is spent. API creates count against the same allowance the dashboard enforces. |
| 403 | 53 | A gated feature was requested without the plan for it, such as passcode_verifier without view protection. |
| 409 | 105 | This Idempotency-Key was already used with a different request body. |
| 409 | 106 | An earlier request with this Idempotency-Key has not finished. |
| 429 | 107 | Rate limit exceeded. Retry-After gives the seconds to wait, and is never below 1. |
| 500 | 24 or 45 | The account or team behind the plan lookup could not be read. |
| 503 | 108 | Your entitlements could not be resolved, so we cannot tell whether this create is within your allowance. Nothing was created. Retry — a failed billing lookup is deliberately not treated as permission to exceed a cap. |
Team shares and quota
A key can act inside a team. When it does, the plan consulted for the share allowance is the team's plan, not the key holder's own account — a seat member of a paying team is judged against the team.
API creates consume the same monthly share allowance as the dashboard. The API is not a way around a plan limit. max_shares = -1 on a plan means unlimited.
Webhooks
A successful create emits the share.created event to your webhook endpoints, carrying short_code plus title, organization_id and expired_at where those are set. The payload is metadata only; ciphertext, keys, tokens and fragments are never included in any webhook body.