New You can now talk to your Gelee agent from Claude, ChatGPT or Cursor through our MCP. Read the docs →

Reference

Webhooks

Rather than polling, have Gelee call you. Point a destination at any public HTTPS endpoint or a Discord channel and pick the events you want. Every event carries the person, the seat and the campaign.

Last updated 22 October 2026

Events

GroupEvents
Repliesreply_received, reply_positive, reply_negative, needs_human
Connectionsinvite_sent*, connection_accepted, invite_withdrawn
Messagesmessage_sent*
Bookingscall_booked, auto_booking_created, auto_booking_failed
Leadslead_accepted, lead_rejected, lead_updated
Campaignscampaign_started, campaign_completed
Contentpost_published, post_scheduled
Alertsaccount_rate_limited, account_disconnected, account_reconnected, sales_nav_unavailable, sales_nav_restored, report_generated, report_sends_skipped, daily_digest
* Pick these two by name

invite_sent and message_sent fire for every invite and message a seat sends, from a campaign or the API. That is hundreds a day per seat, so subscribing to all leaves them out.

  • reply_received also delivers reply_positive and reply_negative.
  • Reply events only cover people you reached through a Gelee campaign, so personal messages and recruiters stay out of your CRM. needs_human is not filtered: if someone needs an answer, you hear about it.
  • lead_updated fires when a lead's custom fields, tags, flags or notes change, or it is paused, resumed or removed, through the API or the assistant.
  • sales_nav_unavailable means Sales Navigator lapsed or its session died on a seat. Classic LinkedIn still works, unlike account_disconnected.

What we send

Every event uses one envelope. Which blocks appear inside data depends on the event.

POST to your endpoint
{
  "event": "reply_received",
  "timestamp": "2026-10-22T13:04:11.522Z",
  "org_id": "0f2c…",
  "data": {
    "prospect": { "name": "Dana Reyes", "lead_id": "5d70…",
                  "linkedin_url": "https://www.linkedin.com/in/dana",
                  "linkedin_id": "ACoAA…", "job_title": "VP Sales",
                  "company": "Acme", "industry": "Computer Software",
                  "location": "Toronto, Ontario, Canada" },
    "sender":   { "name": "Sofia Salazar", "account_id": "SExF…",
                  "public_identifier": "sofiasalazar",
                  "linkedin_url": "https://www.linkedin.com/in/sofiasalazar" },
    "campaign": { "id": "9a2c…", "name": "Q4 Founders" },
    "message":  { "text": "Sounds good, send times?", "chat_id": "chat_88",
                  "sentiment": "positive" }
  }
}
BlockOnFields
prospectAnything about a personname, lead_id, linkedin_url, linkedin_id, job_title, company, industry, location, email, headline, company_website, company_linkedin_url, company_size, follower_count
senderAnything a seat didname, account_id, public_identifier, linkedin_url
campaignAnything tied to a campaignid, name, stats
messageReplies and sendstext, subject, message_type, sentiment, chat_id, conversation
bookingcall_booked, auto bookingsstart_time, provider, title
leadlead_accepted, lead_rejectedbatch_id, reason
lead_updatelead_updatedchange (fields, tags, flags, note, paused, resumed, removed), custom_fields, tags, flags, by (api or assistant)
needs_humanneeds_humanreason, reason_label, ai_draft, queue_id, inbox_url
accountSeat alertsname, account_id, reason, reconnect_url, plus cooldown_hours, status, sn_verdict and similar
reportdaily_digest, report_generatedtype, period, share_url, stats
postpost_published, post_scheduledtext, url, scheduled_time

Webhooks say job_title where the REST API says role. A field we do not know is left out of a webhook rather than sent as null, so check a key exists before reading it.

Verify the signature

A destination with a signing secret signs every request. Read X-Gelee-Signature-256: t=<unix seconds>,v1=<hex>, where v1 is an HMAC SHA-256 of <t>.<raw body>. The time is signed too, so reject anything older than 5 minutes and a captured request cannot be replayed. The older X-Gelee-Signature (HMAC of the body alone) is still sent.

Node
import { createHmac, timingSafeEqual } from "crypto";

// rawBody must be the UNPARSED request body. Re-serialising JSON changes the bytes.
export function verifyGelee(rawBody, header, secret) {
  const t = /t=(\d+)/.exec(header ?? "")?.[1];
  const v1 = /v1=([0-9a-f]+)/.exec(header ?? "")?.[1] ?? "";
  if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // 5 min
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return v1.length === expected.length &&
    timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

// verifyGelee(rawBody, req.headers["x-gelee-signature-256"], process.env.GELEE_WEBHOOK_SECRET)
Python
import hmac, hashlib, re, time

def verify_gelee(raw_body: bytes, header: str, secret: str) -> bool:
    t = re.search(r"t=(\d+)", header or "")
    v1 = re.search(r"v1=([0-9a-f]+)", header or "")
    if not t or not v1 or abs(time.time() - int(t.group(1))) > 300:
        return False
    signed = t.group(1).encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1.group(1))

Delivery

Behaviour
RetriesFour attempts. Anything that is not a 2xx is retried.
Timeout10 seconds. Answer 2xx as soon as you have the payload and do the work after.
DuplicatesIdentical payloads to the same destination collapse into one delivery.
VisibilityThe last attempt, its HTTP code and your endpoint's error show next to the destination in Settings.
SafetyPrivate network, localhost and cloud metadata addresses are refused, including as a redirect target. Up to three redirects to public addresses are followed.

Manage destinations from code

POST/webhooksread_write

The same destinations as Settings, Event Destinations. Up to 10 per workspace.

Body
urlstringrequired
https only. It gets a test POST first and must answer 2xx.
eventsarrayrequired
Event names, or all. GET /webhooks/events lists them with what each means.
labelstring
Your name for it.
typewebhook | discord
discord needs a discord.com/api/webhooks/ URL.
The secret is shown once

A webhook made through the API is always signed. Store signing_secret from this answer: it cannot be read back. Rotate it with PATCH /webhooks/{id} { "rotate_secret": true }.

CallWhat it does
GET /webhooksYour destinations. URLs show only their last characters.
PATCH /webhooks/{id}Change label, events, active or url (tested first), or rotate the secret.
DELETE /webhooks/{id}Remove it.
POST /webhooks/{id}/test?event=…Send a full, signed sample of that event with "test": true, so you can map every field before real data flows.
curl -X POST "https://www.gelee.ai/api/v1/webhooks" \
  -H "Authorization: Bearer gk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://crm.example.com/hooks/gelee",
    "events": [
      "reply_received",
      "call_booked",
      "lead_updated"
    ],
    "label": "CRM"
  }'
201 response
{ "data": { "id": "d1f0…", "label": "CRM", "type": "webhook",
            "url_preview": "...ks/gelee",
            "events": ["reply_received", "call_booked", "lead_updated"],
            "active": true, "scope": "all", "signed": true,
            "signing_secret": "whsec_3b9…" } }