Skip to content
CredenShare
Shares

Create a share

POST /v1/shares — store ciphertext you encrypted yourself and get back a short code.
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
This header is mandatory, not optional. A create without it is refused with a 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

FieldTypeRequiredNotes
titlestringyesMax 256 characters. Visible to CredenShare — it is metadata, not encrypted content.
datastringyesYour ciphertext. See below.
encryption_typestringyesExactly one accepted value: e2ee-aes256-gcm.
access_tokenstringyesDerived from the content key. See below.
descriptionstringnoMax 1024 characters. Also visible to CredenShare.
passcode_verifierstringnoA derived verifier, never the passcode. See below.
expired_atstringnoWhen the share stops working. Accepted formats below.
access_counts_leftintegernoView limit, from 1 to 10000.
timed_viewintegernoSeconds the content stays visible once opened, from 5 to 86400.
organization_idstringnoOmit it. The organization is taken from the key, and a value that disagrees is refused — see below.
item_key_wrapstringnoMakes the share readable from your dashboard later. See below.
The organization comes from the key, not the request. A key minted for a team creates inside that team automatically. If you send an 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 self or account. A key at custody level none sending a wrap gets a 400 with the message This 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.

FormatExample
RFC 3339 with fractional seconds2026-09-01T00:00:00.123456789Z
RFC 3339 with a zone or offset2026-09-01T00:00:00Z or 2026-09-01T02:00:00+02:00
Date and time with no zone2026-09-01T00:00:00
Date only2026-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"
}
FieldTypeNotes
short_codestringThe share's public identifier. Use it to build the link, and as the path parameter on the other endpoints.
expired_atstring or nullRFC 3339, or null when you sent no expired_at.
custodystringWhat happened to item_key_wrap: none, stored or failed.
There is no 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

ValueMeaning
noneNo item_key_wrap was sent.
storedThe wrap was persisted. The share is readable from your dashboard.
failedThe 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:

  1. Scope — is shares:write on the key?
  2. Idempotency-Key — is the header present?
  3. JSON — does the body parse?
  4. Field validation — are the required fields present and within their limits?
  5. Plan share allowance — do you have shares left this period?
  6. encryption_type — is it e2ee-aes256-gcm?
  7. item_key_wrap — is the key's custody level high enough?
  8. 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

Statuserror_codeWhen
400104The Idempotency-Key header is missing or blank.
40019Field validation failed. additional_data maps each rejected field to a message.
40019encryption_type is not e2ee-aes256-gcm, or an item_key_wrap was sent by a none-custody key. Both carry their own message.
400The body is not valid JSON.
40378The key lacks the shares:write scope.
40361Your plan's share allowance is spent. API creates count against the same allowance the dashboard enforces.
40353A gated feature was requested without the plan for it, such as passcode_verifier without view protection.
409105This Idempotency-Key was already used with a different request body.
409106An earlier request with this Idempotency-Key has not finished.
429107Rate limit exceeded. Retry-After gives the seconds to wait, and is never below 1.
50024 or 45The account or team behind the plan lookup could not be read.
503108Your 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.