Start
Quickstart: receive and reply
Receive messages and delivery results by webhook, check the signature, and reply.
Configure a webhook#
Create a webhook endpoint in the Developers page and subscribe to
message.received and message status events. Each delivery is a JSON envelope
(id, type, created_at, account_id, data, ...) with these headers:
| Header | Meaning |
|---|---|
MyComm-Signature |
t=<unix seconds>,v1=<hex HMAC>. A second v1= appears while a rotated secret is still valid. |
MyComm-Event-Id |
The event id, the same as id in the body. Identical on every retry. |
MyComm-Event-Type |
The event type, for example message.received. |
MyComm-Delivery-Id |
The id of this delivery (use it to replay). |
MyComm-Delivery-Attempt |
1 for the first try, then 2, 3, ... |
Verify the raw request body#
The signature covers the string <t>.<raw request body>: the timestamp from the
header, a dot, then the exact bytes of the body. Compute HMAC-SHA256 of that
with the endpoint's signing secret, compare it in constant time with every v1
value in the header, and reject a timestamp more than 300 seconds from now.
Verify before parsing JSON, then de-duplicate by event id before doing any work.
PHP#
$rawBody = file_get_contents('php://input');
$header = $_SERVER['HTTP_MYCOMM_SIGNATURE'] ?? '';
$t = null;
$signatures = [];
foreach (explode(',', $header) as $part) {
[$name, $value] = array_pad(explode('=', trim($part), 2), 2, '');
if ($name === 't') {
$t = (int) $value;
} elseif ($name === 'v1') {
$signatures[] = $value;
}
}
$expected = $t === null ? '' : hash_hmac('sha256', $t.'.'.$rawBody, $_ENV['MYCOMM_WEBHOOK_SECRET']);
$valid = $t !== null && abs(time() - $t) <= 300;
$matched = false;
foreach ($signatures as $signature) {
$matched = hash_equals($expected, $signature) || $matched;
}
if (! $valid || ! $matched) {
http_response_code(400);
exit;
}
$event = json_decode($rawBody, true, flags: JSON_THROW_ON_ERROR);
if (alreadyProcessed($event['id'])) {
http_response_code(204);
exit;
}
recordEventId($event['id']);
With the PHP SDK the same check is one call, MyComm\Webhooks\Webhook::verify().
Node.js#
import crypto from 'node:crypto';
// rawBody must be the unparsed request body (a Buffer or string), for example express.raw({ type: 'application/json' }).
const parts = (request.headers['mycomm-signature'] ?? '').split(',').map((p) => p.trim().split('='));
const t = Number(parts.find(([name]) => name === 't')?.[1]);
const expected = crypto
.createHmac('sha256', process.env.MYCOMM_WEBHOOK_SECRET)
.update(`${t}.`)
.update(rawBody)
.digest('hex');
const valid =
Number.isFinite(t) &&
Math.abs(Date.now() / 1000 - t) <= 300 &&
parts.some(([name, value]) =>
name === 'v1' &&
value.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(value), Buffer.from(expected)));
if (!valid) {
return response.sendStatus(400);
}
const event = JSON.parse(rawBody.toString('utf8'));
if (await alreadyProcessed(event.id)) {
return response.sendStatus(204);
}
await recordEventId(event.id);
Return a 2xx response after the event id is durably recorded. Queue the business work so a slow downstream service does not cause a retry storm.
Reply to a received message#
A message.received event carries the message in data.message. To reply,
send a new message to its from address through the same line
(data.message.line_id). The reply joins the same conversation.
curl -sS -X POST https://mycomm.cogneer.tech/v1/messages \
-H "Authorization: Bearer $MYCOMM_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: reply-event-01" \
-d '{
"line_id": "LINE_ID_FROM_THE_EVENT",
"to": "FROM_ADDRESS_OF_THE_EVENT",
"body": "Thanks, we received your message.",
"category": "transactional"
}'
Use the event id in the idempotency key (reply-<event id>) so a retried
delivery cannot send the reply twice.
Tunnel tip and URL rule#
For local development, use a tunnel that forwards HTTPS traffic to your local webhook handler. Register the public HTTPS URL, not the localhost URL, and keep the tunnel hostname stable while testing signature verification.