MyComm docs
Menu

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.