Webhooks

Send signed contact and account events to a business's Pairly CRM.

One endpoint takes every event. The event field picks the shape.

POST https://api.pairlyhq.com/v1/crm/webhooks/{business_id}/events

Headers

HeaderRequiredValue
Content-Typeyesapplication/json
X-Pairly-Signatureyessha256= + hex HMAC-SHA256 of the raw body, keyed with the secret
X-Pairly-Key-IdnoThe key's id; Pairly then checks only that key

Signing a request

Sign the exact bytes you send. Serialise the JSON once, sign that string, and send that same string.

BODY='{"event":"person.upsert","source":"acme-site","person":{"email":"jules@example.com"},"tags":["lead"]}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$PAIRLY_KEY_SECRET" | sed 's/^.* //')
curl -X POST "https://api.pairlyhq.com/v1/crm/webhooks/$BUSINESS_ID/events" \
  -H "Content-Type: application/json" \
  -H "X-Pairly-Signature: sha256=$SIG" \
  -d "$BODY"

Body

FieldTypeNotes
eventstringOne of the six events below
sourcestring, 1–100 chars, requiredYour integration's name, free text. Helps the business tell traffic apart
personobjectemail (required), first_name, last_name, phone, company_domain (links an existing company with that domain; never creates one, use company.upsert)
companyobjectname (required), domain
tagsstring[]Merged onto the contact; never removes a tag
event_namestringuser.event only, required there
propertiesobjectuser.event only, stored verbatim
idempotency_keystring, 1–200 charsuser.event only: resend with the same key and the activity is recorded once
email_marketing_consentobjectgranted (required bool), granted_at (ISO datetime), method (≤100 chars), source_url (URL). true records consent with this request as evidence, false revokes it, and omitting it changes nothing

Events

eventRequiresDoesTakes tagsTakes consent
person.upsertpersonCreates or fills in a contact matched by emailyesyes
company.upsertcompanyCreates or matches a company by domainnono
person.deletedpersonArchives the contact; idempotent. A later upsert for the same email finds the archived contact and does not un-archive itnono
user.createdpersonAs person.upsert, plus an "Account created" note on the contactyesyes
user.email_confirmedpersonRecords the confirmation; creates the contact if newyesyes
user.eventperson, event_nameLogs any named moment with your propertiesyesno

Tags are the business's segmentation. Pairly gives tag names no meaning; the business builds newsletter and campaign audiences from them.

Responses

StatusBodyMeaningWhat to do
200{"status":"ok","person_id":"…"} (company_id for company.upsert; no id for person.deleted)Applied—
400{"detail":"Malformed webhook payload"} or {"detail":"<event> requires …"}Body did not parse or a required object is missingFix the payload; retrying unchanged fails again
401{"detail":"Invalid signature"}Wrong secret, body changed after signing, or key rotated/revoked. Deliberately not distinguishedCheck you sign the exact bytes; ask the business for a current key
413{"detail":"Request body too large"}Body over 1,124,000 bytesSend less

Retry only on network errors and 5xx, with backoff. person.upsert, company.upsert and person.deleted are safe to retry, and user.event is safe with an idempotency_key. user.created and user.email_confirmed add a lifecycle note on every call, so a retry may add a duplicate note.