> ## Documentation Index
> Fetch the complete documentation index at: https://docs.custral.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Contact method webhooks

> The request Custral sends when a teammate messages someone through a webhook contact method, and how to verify it.

A workspace admin can add a **webhook** [contact method](/team/users/contact-methods).
When a member uses it from a teammate's profile, Custral sends one `POST` to
your endpoint with the message.

## The request

```http theme={null}
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: Custral-Webhooks/1.0
X-Custral-Event: contact.message
X-Custral-Signature: t=1792090000,v1=5f2b…
```

```json theme={null}
{
  "type": "contact.message",
  "workspace": "Pied Piper",
  "from": {"name": "Richard Hendricks", "email": "richard@piedpiper.com"},
  "to": {"name": "Jin Yang", "email": "jin@piedpiper.com"},
  "message": "Can you look at the Hooli deck before 3?",
  "sentAt": "2026-10-15T20:12:44.512Z"
}
```

The body carries names and emails only, never Custral's internal ids. `email`
is `null` when the person has none on their account.

## Verifying the signature

The signature is the same scheme as [event webhooks](/dev/webhooks/security):
`HMAC-SHA256(secret, "<t>.<raw body>")`, hex-encoded, where `t` is the
timestamp in the header. The secret is the one shown when the method was
created.

```ts theme={null}
import crypto from "node:crypto";

function verify(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
  const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  const given = Buffer.from(parts.v1 ?? "", "hex");
  const wanted = Buffer.from(expected, "hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return fresh && given.length === wanted.length && crypto.timingSafeEqual(given, wanted);
}
```

Reject a request older than a few minutes, so a captured one can't be replayed.

## Responding

Answer with any `2xx` within **10 seconds**. Anything else, a redirect
included, is reported to the sender as not delivered. There are no automatic
retries: the person sending sees the failure and can send again.

## Endpoint requirements

* `https://` only.
* It must resolve to a public address. Private, loopback and link-local
  addresses are refused, checked again on every send.

## Related

<CardGroup cols={2}>
  <Card title="Contact methods" icon="paper-plane" href="/team/users/contact-methods">
    Adding a webhook method.
  </Card>

  <Card title="Webhook security" icon="shield" href="/dev/webhooks/security">
    The signature scheme, in detail.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.