helpr docs

Webhooks

Receive real-time HTTP callbacks when events happen in your Helpr workspace — new conversations, messages, emails, and more.

Overview

Webhooks let you subscribe to events in your Helpr workspace. When an event occurs — a visitor starts a conversation, sends a message, an email arrives, or an outbound email is sent — Helpr sends a POST request to your endpoint with the event data.

Webhook payloads are compact event notifications. They include stable IDs, routing metadata, previews, and api_resources links. Fetch full conversation, message, attachment, or raw EML data from the API with a key that has chats.read.

Use webhooks to sync conversations and email threads to your CRM, trigger notifications in Slack, log messages to your data warehouse, or build custom integrations.

Setup

  1. Go to Settings → Developer in the Helpr dashboard
  2. Scroll to the Webhooks section
  3. Click Add Webhook
  4. Enter your HTTPS endpoint URL
  5. Choose This team only or, as an org admin, Entire organization
  6. Select the events you want to receive
  7. Click Create — copy and save the signing secret immediately

Your endpoint must use HTTPS and respond with a 2xx status code within 10 seconds.

Team webhooks receive events for the selected team. Organization webhooks receive matching events from every team and personal inbox in the organization.

Important: The signing secret is only shown once when you create the webhook. You’ll need it to verify signatures on incoming requests.

Managing Webhooks

All webhook management is done in the Helpr dashboard under Settings → Developer → Webhooks:

  • Pause / Resume — temporarily stop deliveries without deleting the webhook
  • Send test — deliver a signed webhook.test payload to verify your endpoint
  • Delivery log — inspect recent response codes, response bodies, attempts, latency, and retry state
  • Replay delivery — resend a previous payload with a new delivery ID
  • Rotate secret — generate a new signing secret with a 7-day previous-secret signature grace period
  • Delete — permanently remove the webhook and its delivery history

If a webhook accumulates 100 consecutive delivery failures, it is automatically disabled. You can re-enable it from the dashboard after fixing your endpoint.

chat.created

Fired when a visitor starts a new conversation (first message in a chat).

{
  "event": "chat.created",
  "schema_version": "2026-05-14-compact",
  "timestamp": "2026-04-27T14:32:10+00:00",
  "data": {
    "chat_id": 4521,
    "conversation_id": 4521,
    "message_id": 18923,
    "team_id": 3,
    "visitor_id": "v_8f3a2b1c",
    "identity": {
      "email": "[email protected]",
      "name": "Jane Smith",
      "userId": "usr_123",
      "company": "Acme Inc"
    },
    "api_resources": {
      "conversation": {
        "href": "https://www.helpr.so/api/v1/chats/get?id=4521",
        "method": "GET",
        "scope": "chats.read"
      },
      "messages": {
        "href": "https://www.helpr.so/api/v1/chats/messages?id=4521",
        "method": "GET",
        "scope": "chats.read"
      },
      "message": {
        "href": "https://www.helpr.so/api/v1/chats/message?id=18923",
        "method": "GET",
        "scope": "chats.read"
      }
    }
  }
}

The identity object is included when the visitor has been identified via helpr.identify(). It contains whichever fields were set: email, name, userId, company, phone. If the visitor is anonymous, the identity field is omitted.

chat.message

Fired on every public message in a conversation — from visitors, agents, or bots.

{
  "event": "chat.message",
  "schema_version": "2026-05-14-compact",
  "timestamp": "2026-04-27T14:32:10+00:00",
  "data": {
    "chat_id": 4521,
    "conversation_id": 4521,
    "message_id": 18923,
    "sender_type": "visitor",
    "sender_name": "Jane Smith",
    "body": "Hi, I need help with my order",
    "body_preview": "Hi, I need help with my order",
    "body_format": "text",
    "body_truncated": false,
    "full_body_available": true,
    "identity": {
      "email": "[email protected]",
      "name": "Jane Smith",
      "userId": "usr_123"
    },
    "api_resources": {
      "conversation": {
        "href": "https://www.helpr.so/api/v1/chats/get?id=4521",
        "method": "GET",
        "scope": "chats.read"
      },
      "messages": {
        "href": "https://www.helpr.so/api/v1/chats/messages?id=4521",
        "method": "GET",
        "scope": "chats.read"
      },
      "message": {
        "href": "https://www.helpr.so/api/v1/chats/message?id=18923",
        "method": "GET",
        "scope": "chats.read"
      }
    }
  }
}
FieldDescription
chat_idThe conversation ID
message_idUnique message ID
sender_typevisitor, agent, or bot
sender_nameDisplay name of the sender
bodyMessage text preview. Use api_resources.message for the full stored message
body_truncatedtrue when the preview does not contain the full text body
api_resourcesDurable API links for fetching the conversation and message
identityVisitor identity object (omitted if anonymous). See chat.created for available fields

Email events

Email webhooks are delivered for shared email conversations and, through organization-scoped webhooks, personal inbox conversations. Subscribe to email.thread_created for the first inbound email in a thread, email.received for every inbound email, email.sent after Gmail or Microsoft confirms a send, and email.delivery_failed when an outbound email reaches a terminal failure.

{
  "event": "email.received",
  "schema_version": "2026-05-14-compact",
  "timestamp": "2026-05-14T14:32:10+00:00",
  "data": {
    "event_id": "email.received:18924",
    "chat_id": 4521,
    "conversation_id": 4521,
    "team_id": 3,
    "organization_id": 1,
    "message_id": 18924,
    "direction": "inbound",
    "provider": "gmail",
    "mailbox_state": "inbox",
    "inbox": {
      "id": 12,
      "scope": "team",
      "provider": "gmail",
      "email": "[email protected]",
      "name": "Support"
    },
    "sender": {
      "type": "visitor",
      "name": "Jane Smith"
    },
    "email": {
      "subject": "Invoice question",
      "from": { "email": "[email protected]", "name": "Jane Smith" },
      "to": [{ "email": "[email protected]", "name": "Support" }],
      "cc": [],
      "bcc": [],
      "body_format": "html",
      "body_preview": "Hi, can you send me the invoice?",
      "body_text": "Hi, can you send me the invoice?",
      "body_truncated": false,
      "full_body_available": true,
      "provider_message_id": "18f80aa1a2b3c4d5",
      "rfc_message_id": "<[email protected]>",
      "attachments": [{
        "id": "8ed3f6ad...",
        "name": "invoice.pdf",
        "mime": "application/pdf",
        "size": 248193,
        "api_resource": {
          "href": "https://www.helpr.so/api/v1/chats/attachment?messageId=18924&attachmentId=8ed3f6ad...",
          "method": "GET",
          "scope": "chats.read"
        }
      }],
      "raw_eml": {
        "provider": "gmail",
        "size_bytes": 129384,
        "sha256": "f2c7...",
        "api_resource": {
          "href": "https://www.helpr.so/api/v1/chats/raw-eml?messageId=18924",
          "method": "GET",
          "scope": "chats.read"
        }
      },
      "secure": false
    },
    "api_resources": {
      "conversation": {
        "href": "https://www.helpr.so/api/v1/chats/get?id=4521",
        "method": "GET",
        "scope": "chats.read"
      },
      "messages": {
        "href": "https://www.helpr.so/api/v1/chats/messages?id=4521",
        "method": "GET",
        "scope": "chats.read"
      },
      "message": {
        "href": "https://www.helpr.so/api/v1/chats/message?id=18924",
        "method": "GET",
        "scope": "chats.read"
      },
      "raw_eml": {
        "href": "https://www.helpr.so/api/v1/chats/raw-eml?messageId=18924",
        "method": "GET",
        "scope": "chats.read"
      }
    }
  }
}

Email webhook bodies are previews. Full HTML, full text, attachments, and raw EML are retrieved through the api_resources links with an API key that has chats.read. Attachment and raw EML API responses return temporary signed download URLs when requested. Use X-Helpr-Delivery or data.event_id to deduplicate processing on your side.

Wildcard Subscription

Pass ["*"] as the events array to receive every event type, including any added in the future.

{
  "url": "https://your-server.com/helpr/webhook",
  "events": ["*"]
}

Automation webhooks automation.triggered

Beyond event subscriptions, an automation can call a webhook as one of its actions — so the request fires only when the automation's trigger and conditions match (for example, an email from a specific sender, on a specific inbox, during business hours). Add a Trigger webhook action under Settings → Automations.

Enable Sign requests on the action and Helpr signs each call with a secret unique to that automation, shown in the automation editor — no separate webhook needs to be created. It includes the legacy-compatible signature plus the canonical v1 signature, so the same universal verifier from Signature Verification applies.

POST https://your-api.com/webhook
X-Helpr-Event: automation.triggered
X-Helpr-Signature: sha256=<legacy_hmac_hex_digest>
X-Helpr-Signature-V1: t=1717521605,v1=<hmac_hex_digest>

{
  "event": "automation.triggered",
  "timestamp": "2026-06-04T18:20:05+00:00",
  "data": {
    "automation": { "id": 42, "name": "VIP email alert" },
    "trigger_event": "email.inbound_new",
    "team_id": 8,
    "conversation": {
      "id": 109823,
      "channel": "gmail",
      "status": "open",
      "subject": "Urgent: order #5521",
      "visitor_id": "v_8fa2..."
    }
  }
}

Note: the automation signing secret is managed per-automation in the automation editor and is independent of the workspace webhook secret in Settings → Developer — rotating one does not affect the other.

Request Format

Every webhook delivery is an HTTP POST with a JSON body and these headers:

HeaderExampleDescription
Content-Typeapplication/jsonAlways JSON
X-Helpr-Signaturesha256=a1b2c3d4…Legacy-compatible HMAC-SHA256 of <body>. See Signature Verification.
X-Helpr-Signature-V1t=1717521600,v1=a1b2c3d4…Canonical v1 signature — HMAC-SHA256 of "<timestamp>.<body>"
X-Helpr-Timestamp1717521600Unix timestamp used by the v1 signature
X-Helpr-Signature-Previoussha256=…Optional legacy signature with the previous secret during a 7-day rotation grace period
X-Helpr-Signature-Previous-V1t=…,v1=…Optional v1 signature with the previous secret during a 7-day rotation grace period
X-Helpr-Eventchat.messageThe event type
X-Helpr-Delivery98231Unique delivery ID (useful for deduplication)
X-Helpr-Webhook-Version2026-05-14-compactPayload schema version

Signature Verification

Verify every request before processing it. Helpr signs with your endpoint’s secret using HMAC-SHA256. The primary X-Helpr-Signature header remains legacy-compatible, and X-Helpr-Signature-V1 carries the replay-resistant canonical signature.

Canonical format (v1): t=<timestamp>,v1=<hex>, where hex = HMAC-SHA256("<timestamp>.<body>", secret) — the timestamp lets you reject replays. The legacy form is sha256=<hex> (HMAC of the body alone). The verifier below accepts both, so you write it once. During secret rotation Helpr also sends previous-secret variants for 7 days — check them too if the primary fails.

const crypto = require('crypto');

function timingSafe(a, b) {
  const x = Buffer.from(a || ''), y = Buffer.from(b || '');
  return x.length === y.length && crypto.timingSafeEqual(x, y);
}

// Accepts canonical v1 (t=,v1=) and the legacy sha256= forms — write it once.
function verifyHelprSignature(rawBody, headers, secret, toleranceSec = 300) {
  const sig = headers['x-helpr-signature-v1'] || headers['x-helpr-signature'] || '';
  const hmac = (data) => crypto.createHmac('sha256', secret).update(data).digest('hex');

  if (sig.includes('v1=')) {                          // canonical v1
    const p = Object.fromEntries(sig.split(',').map(kv => kv.trim().split('=')));
    const ts = parseInt(p.t, 10);
    if (!ts || Math.abs(Date.now() / 1000 - ts) > toleranceSec) return false;
    return timingSafe(p.v1, hmac(`${ts}.${rawBody}`));
  }
  if (sig.startsWith('sha256=')) {                    // legacy
    const hex = sig.slice(7), ts = headers['x-helpr-timestamp'];
    if (ts && timingSafe(hex, hmac(`${ts}.${rawBody}`))) return true;  // Data API form
    return timingSafe(hex, hmac(rawBody));                              // body-only form
  }
  return false;
}

app.post('/helpr/webhook', (req, res) => {
  if (!verifyHelprSignature(req.rawBody, req.headers, process.env.HELPR_WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  const { event, data } = JSON.parse(req.rawBody);
  // handle event…
  res.status(200).send('ok');
});
import hmac, hashlib, time, os

def _hmac(data: bytes, secret: str) -> str:
    return hmac.new(secret.encode(), data, hashlib.sha256).hexdigest()

# Accepts canonical v1 (t=,v1=) and the legacy sha256= forms.
def verify_helpr_signature(raw_body: bytes, headers, secret: str, tolerance=300) -> bool:
    sig = headers.get('X-Helpr-Signature-V1', '') or headers.get('X-Helpr-Signature', '')
    if 'v1=' in sig:                                      # canonical v1
        p = dict(kv.strip().split('=', 1) for kv in sig.split(','))
        ts = int(p.get('t', 0))
        if not ts or abs(time.time() - ts) > tolerance:
            return False
        return hmac.compare_digest(p.get('v1', ''), _hmac(f"{ts}.".encode() + raw_body, secret))
    if sig.startswith('sha256='):                        # legacy
        hex_sig, ts = sig[7:], headers.get('X-Helpr-Timestamp')
        if ts and hmac.compare_digest(hex_sig, _hmac(f"{ts}.".encode() + raw_body, secret)):
            return True
        return hmac.compare_digest(hex_sig, _hmac(raw_body, secret))
    return False

# In your handler:
if not verify_helpr_signature(request.data, request.headers, os.environ['HELPR_WEBHOOK_SECRET']):
    return 'Invalid signature', 401
// Accepts canonical v1 (t=,v1=) and the legacy sha256= forms.
function verify_helpr_signature(string $rawBody, array $headers, string $secret, int $tolerance = 300): bool {
    $sig  = $headers['X-Helpr-Signature-V1'] ?? ($headers['X-Helpr-Signature'] ?? '');
    $hmac = fn(string $d) => hash_hmac('sha256', $d, $secret);

    if (str_contains($sig, 'v1=')) {                 // canonical v1
        $p = [];
        foreach (explode(',', $sig) as $kv) { [$k, $v] = array_pad(explode('=', trim($kv), 2), 2, ''); $p[$k] = $v; }
        $ts = (int) ($p['t'] ?? 0);
        if ($ts <= 0 || abs(time() - $ts) > $tolerance) return false;
        return hash_equals($hmac($ts . '.' . $rawBody), $p['v1'] ?? '');
    }
    if (str_starts_with($sig, 'sha256=')) {          // legacy
        $hex = substr($sig, 7);
        $ts  = $headers['X-Helpr-Timestamp'] ?? null;
        if ($ts && hash_equals($hmac($ts . '.' . $rawBody), $hex)) return true;  // Data API form
        return hash_equals($hmac($rawBody), $hex);                                // body-only form
    }
    return false;
}

$raw = file_get_contents('php://input');
$headers = [
    'X-Helpr-Signature-V1' => $_SERVER['HTTP_X_HELPR_SIGNATURE_V1'] ?? '',
    'X-Helpr-Signature' => $_SERVER['HTTP_X_HELPR_SIGNATURE'] ?? '',
    'X-Helpr-Timestamp' => $_SERVER['HTTP_X_HELPR_TIMESTAMP'] ?? null,
];
if (!verify_helpr_signature($raw, $headers, getenv('HELPR_WEBHOOK_SECRET'))) {
    http_response_code(401);
    exit('Invalid signature');
}
$data = json_decode($raw, true); // $data['event'], $data['data']

Retries & Failures

Helpr expects a 2xx response within 10 seconds. Initial delivery is attempted immediately. If your endpoint fails or times out, delivery is retried with backoff:

AttemptRetry after
1st failure1 minute
2nd failure5 minutes
3rd failure15 minutes
4th failure1 hour
5th failure3 hours
6th failure6 hours
7th failure12 hours
8th failureMarked as failed

After 100 consecutive failures across all deliveries, the webhook is automatically disabled. A single successful delivery resets the failure counter to zero.

Use the delivery log to inspect failures and replay any failed delivery after your endpoint is fixed. To re-enable a disabled webhook, resume it from the dashboard.

Best Practices

  • Return 200 quickly. Process events asynchronously — queue the payload, respond immediately. If your handler takes more than 10 seconds, the delivery is marked as failed.
  • Deduplicate with the delivery ID. The same event may be delivered more than once during retries. Use the X-Helpr-Delivery header to detect duplicates.
  • Fetch full records on demand. Treat webhook payloads as notifications. Use api_resources when you need full message HTML, attachment downloads, raw EML, or the latest conversation state.
  • Always verify signatures. Validate X-Helpr-Signature before trusting the payload. Use constant-time comparison to prevent timing attacks.
  • Use HTTPS. Webhook payloads contain conversation data. Only HTTPS endpoints are accepted.
  • Monitor your endpoint. If your endpoint starts failing, Helpr disables the webhook after 100 consecutive failures. Set up alerting on your side.