Webhooks
A webhook delivery is an HTTP POST that CredenShare sends to a URL you own when
something happens in your account — a share is created, a recipient opens one, a
member joins your organization. The body is a small, flat JSON object describing
the event. It never contains the shared content itself.
POST /v1/webhooks, and no equivalent under any other path. An API key's only
webhook powers are to list your endpoints and to rotate an endpoint's signing
secret, both through the hosted MCP server.Where endpoints are managed
Add and remove endpoints in the app, under Account → Security → Webhooks:
- Choose Add webhook endpoint.
- Enter the Endpoint URL. It must be
httpsand must resolve to a public address. - Optionally add a Description (up to 256 characters).
- Tick the events you want. The selection defaults to
share.created. - Choose Add endpoint. The signing secret is displayed once — copy it, confirm you have saved it, and close the dialog.
Each endpoint then offers Deliveries, Disable/Enable, Rotate secret and Remove. The delivery log shows the event, status, attempt count, response code and time for recent deliveries, and lets you replay one.
Removing an endpoint is permanent: it deletes the endpoint's event selection and its whole delivery log with it, and cannot be undone.
What an API key can do
Through the hosted MCP server, an API key can:
list_webhooks— list your endpoints with their id, URL, events and enabled state. It never returns a signing secret.rotate_webhook_secret— rotate one endpoint's signing secret, given itsendpoint_id. The new secret is returned once.
That is the whole of it. Listing and rotating cannot redirect your event stream anywhere; creating and re-pointing can, which is exactly why they are not reachable with an API key. A stolen key that could re-point a webhook would turn one leaked credential into an ongoing feed of your event metadata.
What a delivery looks like
CredenShare sends POST to your URL with these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | CredenShare-Webhooks/1 |
X-CredenShare-Event | The event code, for example share.created |
X-CredenShare-Delivery | The delivery's id (a UUID). The same value repeats on every retry of that delivery. |
X-CredenShare-Signature | t=<unix seconds>,v1=<hex> — see Verifying a delivery |
The body is a flat JSON object with an event key and a handful of string
fields. There is no envelope: no data, no object, no id or timestamp
inside the body. The delivery id and event code travel in the headers above. See
the Event reference for every event code and its fields.
Deliveries are queued when the event happens and sent by a worker that runs once a minute, so a delivery normally arrives within about a minute of the event rather than synchronously with the action that caused it. Queuing a delivery can never fail the action itself.
Payloads carry metadata only
A payload names the object an event concerns — a short code, a title, an organization id — and stops there. It never carries shared content, a URL fragment, a decryption key, a passcode, an access token or a signing secret.
This is not a policy that could be relaxed later; it is what the product can do.
Content is encrypted in the sender's browser, and the decryption key lives in the
link fragment, which never reaches CredenShare's servers. There is no plaintext on
the server to put into a webhook, and the keys data, fragment, key,
content, secret, passcode and access_token are refused outright before a
delivery is queued.
The practical consequence: a webhook tells you that something happened and to which share. To act on the content itself, you still need the link and its fragment.
The signing secret
Each endpoint has one signing secret, a string beginning whsec_. It is shown
once, when the endpoint is created, and again each time you rotate it.
CredenShare cannot show it to you a second time. The secret is derived on demand from a master key plus the endpoint's id and secret version; only hashes are stored. If you lose it, rotate to get a new one.
Rotation
Rotating bumps the endpoint's secret version and issues a new secret. For 24
hours afterwards, every delivery is signed twice — once with the new secret
and once with the previous one — so two v1= values appear in the signature
header, and a receiver that accepts any matching v1 keeps working while you
roll your configuration.
Verifying a delivery
Verify every delivery before you act on it. The signature header looks like this:
X-CredenShare-Signature: t=1756270800,v1=3f9c1a2b4d6e8f0a1c3e5d7b9f1a3c5e7d9b1f3a5c7e9d1b3f5a7c9e1d3b5f7a
t is the Unix time in seconds at which that attempt was signed. Each v1 is a
lowercase hex HMAC-SHA256 over the timestamp, a single ., and the raw request
body:
signed_payload = "<t>" + "." + <raw body bytes>
The . is not decoration — without it, different timestamp and body pairs could
produce the same input.
To verify:
- Read the raw body bytes exactly as received. Do not parse the JSON and re-serialize it before hashing — the bytes on the wire are what was signed, and their key order and spacing are not guaranteed to match what your serializer would produce.
- Split the header value on commas. Take
t, and take everyv1value, not just the first or the last. - Reject the delivery if
tis missing or if there is nov1value. - Reject the delivery if
tdiffers from your own clock by more than 300 seconds in either direction. - Compute
hex(HMAC-SHA256(secret, signed_payload)), wheresecretis the full issued string including thewhsec_prefix — not the characters after it, and not a decoded form of it. - Compare your result against each
v1value using a constant-time comparison. Accept the delivery if any one of them matches.
Step 2 matters during a rotation window, when two v1 values are present. Code
that reads only one of them will reject roughly half of what arrives.
An illustrative recomputation at the shell:
# $TS — the t value from the signature header
# $BODY — the request body exactly as received
# $SECRET — the full secret, including the whsec_ prefix
printf '%s.%s' "$TS" "$BODY" \
| openssl dgst -sha256 -hmac "$SECRET" -hex
Each attempt is signed at the moment it is sent, so a retry of the same delivery
arrives with a different t and a different signature over an identical body.
Verify each attempt on its own, and do not cache a signature.
Retries and failures
Any 2xx response counts as success. Your handler has 10 seconds to answer —
acknowledge first and do your work afterwards, or the attempt is recorded as a
timeout.
A failed attempt is retried, up to six attempts in total:
| Attempt | Sent |
|---|---|
| 1 | Within about a minute of the event |
| 2 | 1 minute after attempt 1 |
| 3 | 5 minutes after attempt 2 |
| 4 | 25 minutes after attempt 3 |
| 5 | 2 hours 5 minutes after attempt 4 |
| 6 | 6 hours after attempt 5 |
Six attempts span roughly 8 hours 36 minutes from the first.
What is and is not retried:
- Retried: timeouts, connection errors, any
5xx,408 Request Timeout,429 Too Many Requests, and3xxresponses (redirects are never followed, so a redirect counts as a failure). - Not retried: any other
4xx. A400,404or410ends that delivery immediately, however many attempts remain. A handler that rejects a body it does not recognise will therefore drop events quietly rather than fill your log with retries.
A delivery that exhausts its attempts is marked dead and stays in the endpoint's
delivery log. Replay it from the app once your receiver is fixed. A replay sends
the original payload again under a new delivery id, so a receiver that
de-duplicates on X-CredenShare-Delivery will treat it as new.
An endpoint can also be disabled for you, in which case it stops receiving immediately until you re-enable it:
- Its target resolved to a private or otherwise blocked address at delivery time.
- The master signing key changed, so the secret you hold can no longer verify anything. An endpoint in that state has to be re-created.
Endpoint limits
Webhooks are part of the same entitlement as API access, so they come with the Business and Enterprise plans:
| Plan | Endpoints |
|---|---|
| Business | 3 |
| Enterprise | 10 |
| Every other plan | 0 — webhooks are not included |
The cap counts the endpoints on your account, disabled ones included. Disabling an endpoint does not free a slot; removing it does.
URL requirements
| Rule | Detail |
|---|---|
https only | The URL must begin with https://. Plain http is refused. |
| Public addresses only | A hostname that resolves to a loopback, private, link-local, carrier-NAT, documentation or multicast address is refused — localhost, 127.0.0.1, 10.x, 172.16–31.x, 192.168.x, 169.254.x, and the IPv6 equivalents ::1, fc00::/7 and fe80::/10. |
| Any port | https on a non-standard port is accepted. |
| No redirects | Redirects are never followed. Register the final destination, not something that forwards to it. |
The address check runs again before every delivery, not only when you add the endpoint. That is deliberate: an endpoint whose DNS later pointed inward would otherwise become a way to make CredenShare's servers reach a private network. If a target resolves somewhere blocked at delivery time, that delivery is dropped and the endpoint is disabled. A hostname that does not resolve at all when you add it is accepted, and gets checked the same way at delivery.