C C-Connect Documentation

Webhooks

Registering an endpoint, verifying the signature, and what makes us switch one off.

Instead of asking us periodically what happened, you tell us where to push it. Every event goes out as a POST, signed, with retries.

Registering

In the panel, under Webhooks, or over the API:

curl -X POST https://app.c-connect.pt/v1/webhooks \
  -H "Authorization: Bearer $CCONNECT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.company.com/cconnect",
    "events": ["delivered", "bounced", "complained"]
  }'

https only, with a valid certificate: the payload carries recipient addresses.

With no events, we send them all. An endpoint that receives nothing is worse than one that receives too much, so the default is generous rather than restrictive.

The answer carries the signing secret, once and only once. Keep it.

Registering an endpoint and copying the signing secret.

Verifying the signature

Every delivery carries this header:

X-CConnect-Signature: t=1758182462,v1=8f3c...9ab

t is the time in seconds. v1 is an HMAC SHA-256, with your secret, over the string "<t>.<raw body>".

The raw body, byte for byte as it arrived. If you decode the JSON and encode it again, the signature stops matching, because key order and whitespace change.

$timestamp = ...;   // the t from the header
$expected = hash_hmac('sha256', $timestamp.'.'.$rawBody, $secret);

if (! hash_equals($expected, $received)) {
    abort(400);
}

Always use a constant time comparison, hash_equals in PHP. A === over strings returns sooner when the first characters do not match, and that is enough for an attacker to guess the signature one byte at a time.

Check that t is recent too. We allow five minutes of tolerance on our side; do the same, and refuse a delivery older than that. It is what stops somebody capturing a request and replaying it tomorrow.

What to answer

200, and quickly. If you take more than ten seconds we count the attempt as failed.

The pattern that works is: verify the signature, put the event on a queue of your own, answer 200, and process afterwards. An endpoint doing heavy work before answering ends up looking broken on a busy day.

Anything outside 2xx counts as a failure, including a 301 to another URL: we do not follow redirects, because a redirect is a way of sending the payload somewhere you did not register.

The retries

Seven attempts, over about 33 hours:

Attempt After
1 immediately
2 1 minute
3 5 minutes
4 30 minutes
5 2 hours
6 6 hours
7 24 hours

After that, that delivery is marked failed and is not attempted again.

When we switch an endpoint off

After ten consecutive failures, we disable the endpoint. In the panel it shows as Disabled after repeated failures, and over the API the disabled_at field is filled in.

It is said rather than done silently on purpose: an endpoint that stopped receiving with nobody noticing is why somebody discovers a month later that they lost every event.

One successful delivery resets the counter. To switch it back on, the Reactivate button in the panel, or:

curl -X PATCH https://app.c-connect.pt/v1/webhooks/{id} \
  -H "Authorization: Bearer $CCONNECT_KEY" \
  -d '{"enabled": true}'

Rotating the secret

Also in the panel. The new secret appears once, and deliveries already in flight were signed with the previous one: your system will refuse them if it has already changed. If that bothers you, accept both signatures for a few minutes before dropping the old one.