Authentication
Every request to the CredenShare API carries an API key in the Authorization header. There is no session, no cookie and no shared gateway key on this surface — the API key is the only credential, and it is checked on every single request.
The credential
A CredenShare API credential has a fixed prefix and three dot-separated parts:
crs_sk_live_<keyId>.<authSecret>.<custodySecret>
│ │ └─ custody secret: never transmitted
│ └─ auth secret: sent as the bearer credential
└─ key id: identifies the key
You send the prefix, the key id and the auth secret — the first two parts, nothing more:
curl https://api.credenshare.io/v1/shares \
-H "Authorization: Bearer crs_sk_live_<keyId>.<authSecret>"
The three components are 12, 32 and 32 random bytes, each base64url-encoded without padding. So the value you put in the header is 72 characters long, and the full credential you were shown at creation is 116 characters. Size your storage accordingly.
Only a SHA-256 hash of the auth secret is stored, and the presented secret is compared in constant time. The full credential is never persisted anywhere — which is why it is displayed exactly once, when the key is created, and can never be retrieved again. If you lose it, revoke the key and mint a new one.
Never transmit the custody secret
Authorization header, in a request body, in a query string or in a log. There is deliberately no field anywhere in the API that accepts it.The custody secret is not a second password for the server. It is key material your client uses locally: you derive a custody keypair from it as HKDF(custodySecret, "custody") and register only the public half with CredenShare. The server therefore has no use for the secret half, and by never receiving it we cannot lose it.
A credential that arrives with all three parts is refused outright — not tolerated, not silently truncated. The attempt is recorded as a key compromise, because the secret has now been transmitted over the network and may exist in logs, proxies and traces along the way. Treat that key as burned: revoke it and mint a replacement.
Getting a key
Keys are minted in the CredenShare web app, not through the API. Open the account page (/user-account), go to the Security tab, and find the card headed API keys.
- Click Create API key.
- Enter a Name (required, up to 256 characters). Use something that identifies the caller, like
CI deploy pipeline. - Tick the Scopes the key needs. The form starts with
shares:writeselected. - Choose a Custody level. The form starts on
Self. - Click Create key. The full credential appears once, in a highlighted panel.
- Click Copy credential, store it in your secret manager, then tick the acknowledgement and click Done.
The app registers your custody public key for you, immediately after showing you the credential, while the custody secret is still in memory in your browser. Nothing else in the flow ever sends it.
Scopes
A key carries a set of scopes, chosen at creation and fixed for the life of the key. Matching is exact: there is no hierarchy and no wildcard, so shares:write does not imply shares:read. Grant both if the caller needs both. A key can also be created with no scopes at all, in which case every endpoint refuses it.
| Scope | What it gates | Enforced today |
|---|---|---|
shares:read | GET /v1/shares, GET /v1/shares/{shortCode}; MCP tools list_shares, get_share, list_webhooks | Yes |
shares:write | POST /v1/shares, DELETE /v1/shares/{shortCode}; MCP tools create_share_via_browser, expire_share, rotate_webhook_secret | Yes |
requests:read | GET /v1/requests, GET /v1/requests/{shortCode}, GET /v1/requests/{shortCode}/submissions | Yes |
requests:write | POST /v1/requests, DELETE /v1/requests/{shortCode}; MCP tool request_secret_from | Yes |
stats:read | GET /v1/stats; MCP tool share_stats | Yes |
webhooks:manage | Reserved | No — checked nowhere |
webhooks:manage is a reserved name that nothing currently checks. Granting it grants no additional access and withholding it denies nothing — do not treat it as a least-privilege boundary. The two webhook tools are gated on shares:read and shares:write instead, so a key with shares:write can rotate a webhook signing secret even without webhooks:manage, and a key holding only webhooks:manage cannot use either webhook tool.The other five scopes are all enforced. Note in particular that the requests:* scopes are not implied by the shares:* ones: a key that can create shares cannot read a submission unless you granted requests:read as well. The reverse also holds — requests:read gates the submissions endpoint, which is the only read on the API that returns content.
When a key is missing a scope the REST API answers 403 with error code 78 and the message You do not have permission to perform this action on this team. The wording is generic; on this surface it means the key lacks the required scope, not that anything is wrong with your team.
The MCP endpoint reports the same condition differently. It answers 200 with a tool result marked as an error, whose text is:
This API key lacks the "shares:read" scope, which list_shares requires. Mint a key with that scope.
Custody levels
Custody is the part of this API most likely to surprise you, and it follows directly from the encryption model: content is encrypted on your side, and the server never sees the key. A share you create through the API is readable by whoever holds the link fragment — and by nobody else, including you, from your own dashboard, unless you deliberately store a wrapped copy of the item key. Custody is that opt-in.
| Level | Name | What the key can read | What the caller must do |
|---|---|---|---|
0 | none | Only content it is handed a link for | Omit item_key_wrap. Sending one is refused. |
1 | self | Content this key created | Register a custody public key, then send item_key_wrap on each create |
2 | account | Everything the account holder can read | Register a custody public key, then have one of your own devices approve the request |
Level 0 is the API's default. The web app's key-creation form defaults to Self.
Level 0 refuses a wrap outright
At level 0 there is no custody public key, so a wrapped item key could never be unwrapped by anyone. Rather than store something unreadable, the create is refused with 400, error code 19, and this 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.
What custody buys you
Sending item_key_wrap at level 1 or higher is what makes an API-created share readable later from your dashboard. Skip it and the share still works perfectly for its recipient — you simply cannot open it yourself afterwards, because no copy of the item key exists on your side.
The create response tells you which happened, in its custody field:
| Value | Meaning |
|---|---|
none | You sent no item_key_wrap. The share is recipient-only. |
stored | The wrap was saved. The share is readable from your dashboard. |
failed | The wrap could not be saved. The share exists and the response is still 201. |
failed is reported rather than raised as an error on purpose. By that point the share has already been created, so failing the request would make you retry with a fresh idempotency key and mint a second copy of the secret. Storing a wrap also requires that your account has an enrolled zero-knowledge account key and the zero-knowledge plan feature; if either is missing you will see custody: "failed" and should enrol before relying on dashboard readability.
Reaching level 1
Every key is created at effective level 0, whatever level you asked for, because a non-zero level requires a custody public key and your client cannot produce one until it holds the credential. The level is raised on first use, when you register the public half of HKDF(custodySecret, "custody"). The web app does this for you as part of key creation.
Reaching level 2
Requesting account custody does not grant it. The key is created at self, the request for level 2 is recorded alongside it, and the effective level rises only after one of your devices performs the grant. That device has to hold your account key unwrapped, and the server cannot stand in for it — which is the whole point. An API key can never perform this approval itself.
Plans and limits
API access is a Business and Enterprise capability. It is granted by a plan feature, and every other plan resolves to no access at all rather than to an unlimited default.
| Plan | API access | Requests/min | Burst | API keys | Webhook endpoints |
|---|---|---|---|---|---|
| Business (monthly or yearly) | Yes | 100 | 200 | 5 | 3 |
| Enterprise (monthly or yearly) | Yes | 600 | 1200 | 25 | 10 |
| Every other plan | No | — | — | 0 | 0 |
Burst is always twice the sustained rate — there is one number to negotiate, not two. Exceeding the rate limit returns 429 with a Retry-After header. Webhooks are gated by the same plan feature as the API; there is no separate webhook entitlement. CredenShare can raise any of these numbers for an individual account or organization.
If a key acts inside an organization, its API access, rate limit and key allowance come from the organization's plan, not from the plan attached to the individual member.
Trying to mint a key on a plan without API access returns 403 with error code 98:
{"success":false,"message":"API access requires a Business or Enterprise plan","error_code":98}
The Create API key button is visible on every plan, so this error is how a plan without API access finds out. Reaching your plan's key allowance returns 403 with error code 100 and the message You have reached the API key limit for your plan.
Rotating and revoking a key
There is no rotate operation for API keys, and no way to change the scopes or the name of an existing one. Rotation is mint-then-revoke:
- Create a new key with the same scopes and custody level.
- Deploy it. Both keys work at the same time, so there is no window where your caller has no credential.
- Confirm the new key is being used, then revoke the old one.
To revoke, open the API keys card and click Revoke on that key's row. Each row shows the key's name, a badge for its custody level, and when it was last used — or never used — which is a good way to find keys nothing depends on before you remove them.
Revocation is immediate. The credential is re-verified from the database on every request and nothing about it is cached, so a revoked key stops working on the next call rather than at the end of some cache window. A revoked key shows a Revoked badge and no longer counts against your plan's key allowance, so revoking frees a slot right away. Revoking also removes the key's account-custody envelope, so content that key could reach through account custody is no longer reachable through it.
Rotate immediately if the full three-part credential was ever transmitted, pasted into a log or ticket, or committed to a repository.
When authentication fails
Authentication failures are answered by the gateway, before your request reaches the API, so they do not use CredenShare's normal JSON error envelope. Do not parse them as though they carry a numeric error_code.
| Situation | Status | Body |
|---|---|---|
No Authorization header | 401 | {"message":"Unauthorized"} |
| Any credential that is not currently usable | 403 | {"Message":"User is not authorized to access this resource with an explicit deny in an identity-based policy"} |
"Not currently usable" deliberately covers every reason at once: a malformed credential, an unknown key id, a wrong auth secret, a revoked key, a credential that carried the custody secret, and a key on an account without API access all produce the identical answer. That uniformity is intentional — a caller who could tell "this key id does not exist" from "this key id exists but is revoked" could probe for valid key ids. It also means the response cannot tell you why you were refused, so check the obvious causes in order: the prefix and the two-part shape, then whether the key was revoked, then whether the account's plan includes API access.
One timing detail worth knowing: the credential itself is never cached, but the plan entitlement behind it is cached briefly. A key you have just revoked stops working immediately, while a plan change — including newly granted API access — can take up to 30 seconds to take effect.