One-Click Access for Influencer Whitelisting

Webhooks

ClientInvite sends an HTTP POST to your URL the moment a client finishes a connect flow. One event exists today: connection.completed.

Setup

  1. Go to Settings → API & Webhooks in your dashboard.
  2. Paste your webhook URL and click Save.
  3. Click Send test event.
[SCREENSHOT: API & Webhooks settings tab]

Build everything with the test event

The test event sends a realistic sample payload, so you can build and test your entire automation before a real client ever connects. Fire it as many times as you need.

When it fires

connection.completed fires when a client completes a connect flow, whether the result is SUCCESS (every requested asset granted) or PARTIAL_SUCCESS (some assets granted, some not). It never fires for abandoned flows — if the client opens your link and leaves without finishing, nothing is sent.

Payload

connection.completed — example payload
{
  "event": "connection.completed",
  "createdAt": "2026-07-13T09:41:27Z",
  "data": {
    "connectionId": "conn_9f2kd83a",
    "status": "SUCCESS",
    "clientName": "Acme Co",
    "clientEmail": "hello@acme.com",
    "customRef": "crm-record-8841",
    "completedAt": "2026-07-13T09:41:25Z",
    "connectionSummaryText": "Acme Co (hello@acme.com) connected 4 accounts: Facebook Ad Account \"Acme Co\", Facebook Page \"Acme Co\", Meta Pixel \"Acme Co Pixel\", Google Ads \"acme.com\".",
    "connectionSummaryHtml": "<p><strong>Acme Co</strong> (hello@acme.com) connected 4 accounts:</p><ul><li>Facebook Ad Account — Acme Co</li><li>Facebook Page — Acme Co</li><li>Meta Pixel — Acme Co Pixel</li><li>Google Ads — acme.com</li></ul>",
    "assets": [
      {
        "platform": "facebook",
        "type": "ad_account",
        "name": "Acme Co",
        "externalId": "act_1086344871362999",
        "permission": "manage",
        "status": "GRANTED"
      },
      {
        "platform": "facebook",
        "type": "page",
        "name": "Acme Co",
        "externalId": "104217693118841",
        "permission": "manage",
        "status": "GRANTED"
      },
      {
        "platform": "facebook",
        "type": "pixel",
        "name": "Acme Co Pixel",
        "externalId": "1265103317439916",
        "permission": "view",
        "status": "GRANTED"
      },
      {
        "platform": "google",
        "type": "ads_account",
        "name": "acme.com",
        "externalId": "734-201-8841",
        "permission": "manage",
        "status": "GRANTED"
      }
    ]
  }
}

Field reference

FieldTypeDescription
eventstringAlways "connection.completed".
createdAtstring (ISO 8601)When the webhook was generated.
data.connectionIdstringUnique ID for this connection. Stable across retries — use it to deduplicate.
data.statusstring"SUCCESS" or "PARTIAL_SUCCESS".
data.clientNamestringThe name your client entered in the connect flow.
data.clientEmailstringThe email address your client entered.
data.customRefstring | nullYour own ID, if the connect link included ?ref=. See below.
data.completedAtstring (ISO 8601)When the client finished the flow.
data.connectionSummaryTextstringHuman-readable one-line summary of everything that was connected.
data.connectionSummaryHtmlstringThe same summary as ready-to-send HTML.
data.assets[]arrayOne entry per requested asset.
data.assets[].platformstring"facebook", "google", "shopify", or "linkedin".
data.assets[].typestringAsset type, e.g. "ad_account", "page", "pixel", "ads_account".
data.assets[].namestringThe asset’s name in the client’s account.
data.assets[].externalIdstringThe platform’s own ID for the asset.
data.assets[].permissionstring"view" or "manage" — the level you requested.
data.assets[].statusstring"GRANTED" or "NOT_GRANTED".

Most automations only need one field

For most automations, connectionSummaryText (or its HTML twin) is all you need. Drop it into a Slack message or email and you get a readable summary of the whole connection without touching the assets array.

Tag connections with your own ID: customRef

Add ?ref=YOUR_ID to any connect link to tag the resulting connection with an ID from your own system. It comes back as data.customRef in every webhook payload and every API response, and you can look a client up by it.

Worked example — matching a connection back to a HubSpot record:

  1. Your HubSpot contact for Acme Co has record ID crm-record-8841.
  2. You send them your connect link with the ref appended: https://app.clientinvite.com/your-agency?ref=crm-record-8841
  3. When they connect, the webhook arrives with "customRef": "crm-record-8841" — your automation uses it to update exactly the right HubSpot record, even if the client typed a different name or email than the one in your CRM.

Verifying the signature

Every webhook includes an X-ClientInvite-Signature header: an HMAC-SHA256 hex digest of the request body, computed with your webhook secret (whsec_xxxxxxxxxxxx, shown in Settings → API & Webhooks). Verify it before trusting the payload.

Hash the raw body

Compute the HMAC over the raw request body, before any JSON parsing. If you hash the re-serialized parsed body, key order and whitespace changes will make verification fail intermittently.

Node.js

verify.js — Express
const crypto = require('crypto');
const express = require('express');
const app = express();

// IMPORTANT: use express.raw() so you hash the raw body, not parsed JSON
app.post(
  '/webhooks/clientinvite',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.headers['x-clientinvite-signature'] || '';
    const expected = crypto
      .createHmac('sha256', process.env.CLIENTINVITE_WEBHOOK_SECRET) // whsec_xxxxxxxxxxxx
      .update(req.body) // raw Buffer
      .digest('hex');

    const valid =
      signature.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));

    if (!valid) return res.status(401).send('Invalid signature');

    const event = JSON.parse(req.body);
    console.log(event.data.connectionSummaryText);
    // e.g. post to Slack, update your CRM, etc.

    res.sendStatus(200);
  }
);

app.listen(3000);

Python

verify.py — Flask
import hashlib
import hmac
import json
import os

from flask import Flask, request, abort

app = Flask(__name__)


@app.post("/webhooks/clientinvite")
def clientinvite_webhook():
    # IMPORTANT: hash request.get_data() (the raw body), not request.json
    raw_body = request.get_data()
    signature = request.headers.get("X-ClientInvite-Signature", "")

    expected = hmac.new(
        os.environ["CLIENTINVITE_WEBHOOK_SECRET"].encode(),  # whsec_xxxxxxxxxxxx
        raw_body,
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        abort(401)

    event = json.loads(raw_body)
    print(event["data"]["connectionSummaryText"])
    # e.g. post to Slack, update your CRM, etc.

    return "", 200

PHP

verify.php
<?php
// IMPORTANT: hash the raw body from php://input, not $_POST or decoded JSON
$rawBody   = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_CLIENTINVITE_SIGNATURE'] ?? '';

$expected = hash_hmac(
    'sha256',
    $rawBody,
    getenv('CLIENTINVITE_WEBHOOK_SECRET') // whsec_xxxxxxxxxxxx
);

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit('Invalid signature');
}

$event = json_decode($rawBody, true);
error_log($event['data']['connectionSummaryText']);
// e.g. post to Slack, update your CRM, etc.

http_response_code(200);

Retries & delivery

  • Your endpoint has 10 seconds to respond with a 2xx status.
  • Any non-2xx response (or a timeout) counts as a failed delivery.
  • Failed deliveries are retried at 1 minute, 10 minutes, 1 hour, and 6 hours, then marked failed.
  • Settings → API & Webhooks shows a delivery log for every attempt, with a resend button.

Respond fast: acknowledge with a 200 first, then do slow work (CRM calls, emails) asynchronously. Retries mean your endpoint can receive the same event more than once — deduplicate on data.connectionId.

Troubleshooting

  • Nothing fired — the client abandoned the flow before finishing, or your workspace is on the Freelancer plan (webhooks are Agency-plan only).
  • Missing fields — re-fetch a sample with Send test event; older captured samples may predate newer fields.
  • Signature mismatch — you're hashing the parsed body instead of the raw body. See Verifying the signature.
  • 403 on API calls — the API requires the Agency plan.

Next steps

Still stuck?

Contact support or use the chat bubble in your dashboard — we usually reply within a few hours.