AllyOneCRM / Webhooks

Webhooks

Receive CRM events at your system's URL, signed with HMAC-SHA256 — no polling.

Register

POST/v1/webhooks
{
  "name": "ERP — production",
  "url": "https://erp.yourcompany.com/webhooks/allyone",
  "events": ["contact.created", "deal.won", "form.submitted"]
}

Requires the integrations:manage permission (owner and admin by default). The URL must be public: internal, loopback and cloud-metadata addresses are refused. The 201 response carries the secret used for signing — store it in your secrets vault.

Events

EventWhen it firesdata
contact.createdContact created through the API or the dashboard.id, email, first_name, tags
form.submittedA CRM form was submitted.form_id, form_name, contact_id, data
deal.stage_changedA deal moved to another stage.deal_id, title, stage_id
deal.wonA deal was marked as won.deal_id, title, value
deal.lostA deal was marked as lost.deal_id, title
contact.updatedContact changed through the API or the dashboard.id, fields (changed fields)
deal.createdA deal was created.deal_id, title, value, stage_id, contact_id
survey.respondedA survey (NPS/CSAT) was answered.survey_id, contact_id, score
campaign.completedA campaign finished (end of sending, limit reached, empty audience or manual completion).campaign_id, name, sent, failed, reason
journey.enrolledA contact was enrolled in a journey.journey_id, contact_id, enrollment_id
journey.completedA contact reached the end of a journey.journey_id, contact_id, enrollment_id
contact.unsubscribedUnsubscribe through the link (per channel), "SAIR" on WhatsApp or erasure at the data subject's request (LGPD).contact_id, channel, reason
score.rule_firedA scoring rule was applied to a contact.rule and contact

Payload

{
  "id": "5b0d2c8e-7f1a-4c3e-9a51-2e6c0b7d4f10",
  "event": "deal.won",
  "timestamp": "2026-10-01T14:03:11.204Z",
  "tenant_id": "4f1c...",
  "data": { "deal_id": "9a2e...", "title": "Annual plan — ACME", "value": 18000 }
}

The event type is in the body's event field. Each delivery has its own id (also in the X-AllyOne-Delivery header), the same across every attempt — use it to discard repeats.

Verify the signature

Content-Type: application/json
X-AllyOne-Signature: sha256=<hex>
X-AllyOne-Event: deal.won
X-AllyOne-Delivery: 5b0d2c8e-7f1a-4c3e-9a51-2e6c0b7d4f10
User-Agent: AllyOne-CRM-Webhook/1.0

X-AllyOne-Signature is the HMAC-SHA256 of the raw body with the webhook's secret. Compute it over the bytes received, before parsing the JSON, and compare in constant time:

import crypto from 'node:crypto'

// rawBody: the EXACT body received (Buffer/string), before any JSON.parse
function isValid(rawBody, header, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
  const a = Buffer.from(expected), b = Buffer.from(header ?? '')
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}
import hmac, hashlib

def is_valid(raw_body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header or "")

Delivery and failures

  • Success is any 2xx response within 10 seconds. Redirects are not followed.
  • On failure, the CRM retries right away: up to 3 attempts in total, 1 s and 2 s apart. After that there is no further retry.
  • Every delivery is recorded in GET /v1/webhooks/:id/deliveries (the 50 most recent, with HTTP status, attempt count and success) — use it to reconcile what failed.
  • Respond 2xx quickly and process in a queue on your side: a slow endpoint burns the 3 attempts in seconds.

Manage

RouteWhat it does
GET /v1/webhooksLists the tenant's webhooks.
GET /v1/webhooks/eventsEvents accepted for subscription.
PATCH /v1/webhooks/:idChanges name, url, events or active.
POST /v1/webhooks/:id/rotate-secretGenerates a new secret (the old one stops working immediately).
GET /v1/webhooks/:id/deliveriesDelivery history.
DELETE /v1/webhooks/:idRemoves it.
Webhooks — AllyOneCRM