Product

How it works Widgets Compare HAPI score

Who it's for

Professionals Companies Services

More

Developers Pricing Journal
Claim your pageSign in

Webhooks

Moat tells your app when a connected person's page changes. Events go to the user_update_path you set when linking the person.

Who receives events#

Each Moat page keeps a list of connected apps. An app receives events for a person when its entry has both a callback_site_param and a user_update_path. Moat sends one request per app, even if the app appears more than once.

Event catalog#

EventSent whenFormat
profile.updatedAny section of the page is savedJSON, signed
profile.deletedThe page is permanently deletedJSON, signed
profile.suspend.updatedAn admin suspends or restores the accountJSON, signed
profile.unlinkedThe person removes your app from their pageJSON, signed
profile.basic.updatedName, title, company or location is savedForm, token
profile.tags.updatedStatus signals are savedForm, token

New integrations should build on the four signed JSON events. The two form events exist for older integrations.

Verifying signatures#

JSON events carry X-Moat-Signature: sha256=<hex>, an HMAC-SHA256 of the raw request body using your webhook secret. Compute it over the exact bytes you received, before any JSON parsing, and compare in constant time.

$body     = file_get_contents('php://input');
$given    = $_SERVER['HTTP_X_MOAT_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $body, MOAT_XSITE_WEBHOOK_SECRET);
if (!hash_equals($expected, $given)) {
    http_response_code(401);
    exit;
}
$event = json_decode($body, true);
Reject stale events. Every JSON event includes a Unix timestamp. After the signature passes, discard events older than a few minutes to limit replays.

Form events carry a token field instead. Compare it with the token we issued for your app, in constant time.

Payloads#

Examples are trimmed to the fields your integration should rely on. Ignore fields you don't recognise, because new ones may be added.

profile.updated

{
  "event": "profile.updated",
  "item_key": "38e98f6431107e72ccf0e4ba",
  "profile_url": "https://moat.page/profile/d/amara-okafor-38e98f6431107e72ccf0e4ba",
  "timestamp": 1790000000,
  "profile": {
    "first_name": "Amara",
    "last_name": "Okafor",
    "headline": "Designs payment flows people finish",
    "current_title": "Staff Product Designer",
    "current_company": "Loomwell",
    "profile_photo": "https://...",
    "city": "Toronto",
    "country": "CA",
    "open_to_work": "open",
    "profile_flags": "open_to_work,can_mentor",
    "skills": [{ "value": "Figma", "proficiency": "expert" }],
    "public_key": "c6424007df52eee0ee82483f7c464dc3"
  }
}

profile.deleted and profile.unlinked

{
  "event": "profile.deleted",
  "item_key": "38e98f6431107e72ccf0e4ba",
  "timestamp": 1790000000
}

On profile.deleted, delete what you hold about the person from Moat, as the partner terms require. On profile.unlinked, stop showing their Moat page and stop syncing it.

profile.suspend.updated

{
  "event": "profile.suspend.updated",
  "action": "suspend",
  "item_key": "38e98f6431107e72ccf0e4ba",
  "timestamp": 1790000000
}

action is suspend or unsuspend. Hide the person's card while they're suspended.

Handling events well#

  • Respond fast. Return a 2xx as soon as the signature checks out, then do the work in a queue.
  • Expect repeats. Use item_key, event and timestamp together to skip events you've already processed.
  • Don't rely on order. If two updates arrive close together, keep the one with the later timestamp.
  • Refetch when in doubt. The payload is a snapshot. For the latest state, read the profile from the API.