Skip to content

Data sync: keep your own copy

On the Private plan, ChatPat sends every change in your workspace to an address you run, so your messages, contacts and deals are also in your own database. Turn it on in Settings > Data sync.

What is sent

  • message.created: New message (sent or received)
  • message.status: Message status (sent, delivered, read, failed)
  • contact.upserted: Contact added or changed
  • deal.updated: Deal added or changed
  • test.ping: sent when you press Send test event

Each delivery is one HTTPS POST with a JSON body. data is the full row as ChatPat stores it. Answer with any 2xx status within 10 seconds. Anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, then marked failed. Redirects are not followed. Settings shows the last 7 days of deliveries and can re-send the last 24 hours.

{
  "id": 1042,
  "event": "message.created",
  "created_at": "2026-10-14T10:00:00Z",
  "workspace_id": "ws_123",
  "data": { "id": "wamid.HBg…", "conversation_id": "…", "content": "Is the blue tile in stock?",
            "sender_type": "CUSTOMER", "status": "received", "timestamp": 1760436000000, "…": "…" }
}

Check that it came from ChatPat

Every delivery carries X-ChatPat-Timestamp (Unix seconds) and X-ChatPat-Signature: sha256= followed by the hex HMAC-SHA256 of timestamp.body, keyed with the signing secret from Settings. Refuse a delivery whose signature does not match or whose timestamp is more than five minutes old. Deliveries can arrive more than once and out of order, so store them by row id.

import crypto from 'node:crypto';

// rawBody: the request body exactly as received, before JSON parsing.
function isFromChatPat(rawBody, headers, secret) {
  const timestamp = headers['x-chatpat-timestamp'];
  const given = Buffer.from(headers['x-chatpat-signature'] ?? '');
  const expected = Buffer.from('sha256=' +
    crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex'));
  const isFresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
  return isFresh && given.length === expected.length && crypto.timingSafeEqual(given, expected);
}

Sample receivers

PostgreSQL (Node.js)

// npm install express pg
import express from 'express';
import pg from 'pg';

const db = new pg.Pool({ connectionString: process.env.DATABASE_URL });
// CREATE TABLE chatpat_events (event text, row_id text, data jsonb, received_at timestamptz DEFAULT now(),
//                              PRIMARY KEY (event, row_id));
const app = express();
app.post('/chatpat', express.raw({ type: 'application/json' }), async (req, res) => {
  const raw = req.body.toString('utf8');
  if (!isFromChatPat(raw, req.headers, process.env.CHATPAT_SECRET)) return res.sendStatus(401);
  const { event, data } = JSON.parse(raw);
  // Upsert by row id: a retry or a replay overwrites instead of duplicating.
  await db.query(
    `INSERT INTO chatpat_events (event, row_id, data) VALUES ($1, $2, $3)
     ON CONFLICT (event, row_id) DO UPDATE SET data = EXCLUDED.data, received_at = now()`,
    [event, String(data.id ?? ''), data]);
  res.sendStatus(200);
});
app.listen(3000);

MySQL

// npm install mysql2; same route as above, with this store step:
// CREATE TABLE chatpat_events (event VARCHAR(40), row_id VARCHAR(191), data JSON,
//                              PRIMARY KEY (event, row_id));
await pool.execute(
  'INSERT INTO chatpat_events (event, row_id, data) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE data = VALUES(data)',
  [event, String(data.id ?? ''), JSON.stringify(data)]);

Google Sheets

// Google Apps Script, deployed as a web app (Execute as: me, Who has access: anyone).
// Apps Script cannot read request headers, so it cannot check the signature: put a long
// random token in the receiver address instead (…/exec?token=…) and check it here.
function doPost(e) {
  if (e.parameter.token !== PropertiesService.getScriptProperties().getProperty('TOKEN')) {
    return ContentService.createTextOutput('forbidden');
  }
  const body = JSON.parse(e.postData.contents);
  SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Events')
    .appendRow([new Date(), body.event, body.data.id, JSON.stringify(body.data)]);
  return ContentService.createTextOutput('ok');
}