Skip to content
CredenShare
API Reference

Webhooks

How CredenShare delivers event notifications to your endpoint, how to verify a delivery, and what an API key can and cannot do with 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.

Webhook endpoints are managed in the app, not through the API. The API has no route for creating, editing, re-pointing or deleting an endpoint — there is no 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:

  1. Choose Add webhook endpoint.
  2. Enter the Endpoint URL. It must be https and must resolve to a public address.
  3. Optionally add a Description (up to 256 characters).
  4. Tick the events you want. The selection defaults to share.created.
  5. 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 its endpoint_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:

HeaderValue
Content-Typeapplication/json
User-AgentCredenShare-Webhooks/1
X-CredenShare-EventThe event code, for example share.created
X-CredenShare-DeliveryThe delivery's id (a UUID). The same value repeats on every retry of that delivery.
X-CredenShare-Signaturet=<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.

Only one previous secret is retained. Rotating twice inside the same 24-hour window drops the oldest, and a receiver still holding the original secret starts failing immediately.

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:

  1. 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.
  2. Split the header value on commas. Take t, and take every v1 value, not just the first or the last.
  3. Reject the delivery if t is missing or if there is no v1 value.
  4. Reject the delivery if t differs from your own clock by more than 300 seconds in either direction.
  5. Compute hex(HMAC-SHA256(secret, signed_payload)), where secret is the full issued string including the whsec_ prefix — not the characters after it, and not a decoded form of it.
  6. Compare your result against each v1 value 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:

AttemptSent
1Within about a minute of the event
21 minute after attempt 1
35 minutes after attempt 2
425 minutes after attempt 3
52 hours 5 minutes after attempt 4
66 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, and 3xx responses (redirects are never followed, so a redirect counts as a failure).
  • Not retried: any other 4xx. A 400, 404 or 410 ends 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:

PlanEndpoints
Business3
Enterprise10
Every other plan0 — 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

RuleDetail
https onlyThe URL must begin with https://. Plain http is refused.
Public addresses onlyA 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 porthttps on a non-standard port is accepted.
No redirectsRedirects 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.

An endpoint's URL cannot be changed after it is created. To send events somewhere else, add a new endpoint and remove the old one. The new endpoint has its own signing secret and its own delivery log; nothing carries over.

Webhooks are available on every paid plan. See what each tier includes, or talk to us if you need delivery guarantees or an event we do not emit yet.