Webhooks
Webhooks let you receive real-time HTTP callbacks when events occur in your organization. You can subscribe to specific event types and graph8 will POST a signed JSON payload to your endpoint.
Available Events
GET /webhooks/events
Returns all event types you can subscribe to, each with a category and a human-readable description — the catalog you browse to decide what to listen for.
Example
curl "https://be.graph8.com/api/v1/webhooks/events" \ -H "Authorization: Bearer $API_KEY"Response
{ "data": [ { "event": "campaign.launched", "category": "Campaign", "description": "A campaign was launched and is now sending." }, { "event": "meeting.booked", "category": "Meetings", "description": "A meeting was booked." } ]}| Field | Type | Description |
|---|---|---|
event | string | The event type to subscribe to |
category | string | Grouping for the event (e.g. Campaign, Engagement) |
description | string | null | What the event means / when it fires |
Event catalog
| Event | Category | Description |
|---|---|---|
campaign.created | Campaign | A campaign was created. |
campaign.updated | Campaign | A campaign’s configuration was updated. |
campaign.deleted | Campaign | A campaign was deleted. |
campaign.launched | Campaign | A campaign was launched and is now sending. |
campaign.paused | Campaign | A campaign was paused. |
campaign.completed | Campaign | A campaign finished all of its steps. |
campaign.status_changed | Campaign | A campaign’s status changed. |
document.generated | Content | A campaign document finished generating. |
document.failed | Content | A campaign document failed to generate. |
campaign.content_ready | Content | All content for a campaign is ready. |
intelligence.completed | Intelligence | An intelligence/research run completed. |
intelligence.failed | Intelligence | An intelligence/research run failed. |
company.enriched | Enrichment | A company record was enriched. |
company_intelligence.completed | Enrichment | Company intelligence gathering completed. |
audience.ready | Audience | An audience finished building and is ready to use. |
audience.failed | Audience | An audience failed to build. |
sequence.deployed | Sequence | A sequence was deployed. |
sequence.started | Sequence | A sequence started running for a contact. |
sequence.paused | Sequence | A sequence was paused. |
sequence.completed | Sequence | A sequence completed all steps for a contact. |
engagement.email_sent | Engagement | An email was sent to a contact. |
engagement.email_replied | Engagement | A contact replied to an email. |
engagement.email_bounced | Engagement | An email to a contact bounced. |
engagement.email_skipped | Engagement | An email step was skipped for a contact. |
engagement.call_dispatched | Engagement | A call task was dispatched to the dialer. |
engagement.sms_sent | Engagement | An SMS was sent to a contact. |
engagement.sms_replied | Engagement | A contact replied to an SMS. |
engagement.whatsapp_sent | Engagement | A WhatsApp message was sent to a contact. |
engagement.linkedin_connection_sent | Engagement | A LinkedIn connection request was sent. |
engagement.linkedin_message_sent | Engagement | A LinkedIn message was sent. |
engagement.linkedin_inmail_sent | Engagement | A LinkedIn InMail was sent. |
engagement.linkedin_reply_received | Engagement | A LinkedIn reply was received from a contact. |
engagement.linkedin_connection_accepted | Engagement | A LinkedIn connection request was accepted. |
meeting.booked | Meetings | A meeting was booked. |
meeting.cancelled | Meetings | A booked meeting was cancelled. |
meeting.rescheduled | Meetings | A booked meeting was rescheduled. |
List Webhooks
GET /webhooks
Returns all webhook subscriptions for your organization.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
is_active | boolean | — | Filter by active status |
Example
curl "https://be.graph8.com/api/v1/webhooks" \ -H "Authorization: Bearer $API_KEY"response = requests.get( f"{BASE_URL}/webhooks", headers=HEADERS)Response
{ "data": [ { "id": "wh-abc", "name": "CRM Sync", "url": "https://example.com/webhooks/graph8", "events": ["campaign.created", "campaign.updated"], "is_active": true, "secret": null, "created_at": "2026-02-20T10:00:00", "updated_at": "2026-02-20T10:00:00" } ]}Create Webhook
POST /webhooks
Create a new webhook subscription. The signing secret is returned only once in the response — store it securely.
Maximum 10 active webhooks per organization.
Returns 201 Created.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Target URL for delivery |
events | string[] | Yes | Event types to subscribe to |
name | string | No | Human-readable name |
Example
curl -X POST "https://be.graph8.com/api/v1/webhooks" \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/webhooks/graph8", "events": ["campaign.created", "campaign.launched"], "name": "CRM Sync" }'response = requests.post( f"{BASE_URL}/webhooks", headers=HEADERS, json={ "url": "https://example.com/webhooks/graph8", "events": ["campaign.created", "campaign.launched"], "name": "CRM Sync" })secret = response.json()["data"]["secret"] # Store this!Response
{ "data": { "id": "wh-abc", "name": "CRM Sync", "url": "https://example.com/webhooks/graph8", "events": ["campaign.created", "campaign.launched"], "is_active": true, "secret": "whsec_a1b2c3d4e5f6..." }}Verifying Signatures
Each delivery includes an X-Studio-Signature header (format: sha256=<hex>). Verify it with HMAC-SHA256 over {X-Studio-Timestamp}.{raw_body}:
import hashlib, hmac
def verify_signature(body: bytes, timestamp: str, signature: str, secret: str) -> bool: signed = f"{timestamp}.".encode() + body expected = "sha256=" + hmac.new( secret.encode(), signed, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature)Get Webhook
GET /webhooks/{webhook_id}
Returns webhook details. The secret is not included in the response.
Update Webhook
PATCH /webhooks/{webhook_id}
Partial update — send only the fields you want to change.
Request Body
| Field | Type | Description |
|---|---|---|
url | string | Target URL |
events | string[] | Event types |
name | string | Human-readable name |
is_active | boolean | Enable or disable |
List Deliveries
GET /webhooks/{webhook_id}/deliveries
Returns delivery attempts for a webhook, with pagination.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
limit | integer | 50 | Items per page (max 200) |
status | string | — | Filter by delivery status (success, failed, pending) |
Example
curl "https://be.graph8.com/api/v1/webhooks/wh-abc/deliveries" \ -H "Authorization: Bearer $API_KEY"Response
{ "data": [ { "id": "del-1", "event": "campaign.created", "status": "success", "attempts": 1, "max_attempts": 3, "response_code": 200, "error_message": null, "created_at": "2026-02-25T10:00:00", "completed_at": "2026-02-25T10:00:01" } ], "pagination": { "page": 1, "limit": 50, "total": 1, "has_next": false }}Failed deliveries are retried up to 3 times with increasing delays (10s, 60s, 300s).
Payload Envelope
Every webhook delivery uses the same outer envelope:
{ "event": "campaign.launched", "timestamp": "2026-05-25T12:34:56.789000Z", "org_id": "org_xxx123", "data": { /* event-specific payload below */ }}Delivery Headers
graph8 sets the following headers on every webhook POST:
| Header | Example | Purpose |
|---|---|---|
Content-Type | application/json | — |
X-Studio-Signature | sha256=8a91...c2f4 | HMAC-SHA256 over {timestamp}.{raw_body} |
X-Studio-Timestamp | 1716624896 | Unix epoch seconds, also part of the signed message |
X-Studio-Event | campaign.launched | Quick filter without parsing body |
X-Studio-Delivery-Id | del-abc-uuid | Unique per delivery attempt (idempotency) |
HMAC Verification
Verify every webhook before processing. The signed message is {X-Studio-Timestamp}.{raw_body}. Compare against X-Studio-Signature using a constant-time comparator.
import hmac, hashlib
def verify(body: bytes, timestamp: str, signature: str, secret: str) -> bool: signed = f"{timestamp}.".encode() + body expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature)import crypto from 'crypto';
function verify(body, timestamp, signature, secret) { const signed = `${timestamp}.${body}`; const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(signed).digest('hex'); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));}The body here is the raw request body (bytes / string), not the parsed JSON. Parse only after the signature passes.
Replay Protection
Reject deliveries where |now - X-Studio-Timestamp| > 5 minutes to prevent replay attacks. graph8 retries failed deliveries but never resigns — every retry carries the original timestamp.
Event Payload Schemas
campaign.launched
Fires when a campaign transitions to launched / active.
{ "id": "camp_abc123", "name": "Q2 Outreach Campaign", "slug": "q2-outreach", "concept_slug": "saas-retargeting", "goal": "Drive trial signups", "target_persona": "VP Sales, B2B SaaS", "status": "launched", "created_at": "2026-05-20T10:00:00Z"}campaign.created / campaign.updated / campaign.deleted / campaign.paused / campaign.completed
Same shape as campaign.launched, with status reflecting the new state.
company.enriched
Fires after a successful Apollo (or fallback provider) enrichment of a company record.
{ "mashup_company_id": 12345, "company_ext_id": "cext_xyz789", "account_name": "Acme Corp", "domain": "acme.com", "fields_updated": ["phone", "industry", "headcount"], "enriched_at": "2026-05-25T12:34:56.789000Z"}intelligence.completed
Global context or per-company intelligence generation finishes.
{ "website_url": "https://acme.com", "task_id": "task_uuid", "status": "completed", "success_count": 12, "fail_count": 0, "completed_at": "2026-05-25T12:34:56.789000Z"}audience.ready / audience.failed
Audience build finishes.
{ "audience_id": 42, "list_id": 637, "platform": "meta", "status": "ready", "record_count": 8421}sequence.deployed / sequence.started / sequence.paused / sequence.completed
{ "sequence_id": "seq_abc123", "sequence_name": "Q2 Cold Outreach", "campaign_id": "camp_def456", "contacts_completed": 247, "completed_at": "2026-05-25T12:34:56.789000Z"}engagement.email_replied (subscribers ask for this as reply_received)
Fires when a reply is detected on an outbound message via AI Inbox.
{ "contact_id": "contact_uuid", "reply_subject": "Re: Your Q2 offer", "sequence_id": "seq_abc123", "campaign_id": "camp_def456", "replied_at": "2026-05-25T12:34:56.789000Z", "is_positive": true}Related engagement events follow the same per-channel shape:
engagement.email_sent/email_bounced/email_skippedengagement.call_dispatchedengagement.sms_sent/sms_repliedengagement.whatsapp_sentengagement.linkedin_connection_sent/linkedin_message_sent/linkedin_inmail_sent/linkedin_reply_received/linkedin_connection_accepted
meeting.booked / meeting.cancelled / meeting.rescheduled
{ "contact_id": "contact_uuid", "meeting_id": "meeting_uuid", "meeting_title": "Discovery Call", "scheduled_at": "2026-05-28T14:00:00Z", "duration_minutes": 30, "sequence_id": "seq_abc123", "campaign_id": "camp_def456", "booked_at": "2026-05-25T12:34:56.789000Z"}form_submitted
Form submission captured by Jitsu (web form or appointments form).
{ "form_id": "form_uuid", "form_name": "Lead Capture", "submission_id": "sub_uuid", "contact_name": "John Doe", "contact_company": "Acme", "submitted_at": "2026-05-25T12:34:56.789000Z", "fields": { "email": "[email protected]", "name": "John Doe", "company": "Acme", "budget": "$50k-100k" }}visitor.identified (subscribers ask for this as visitor_identified)
Anonymous visitor matched to a known contact.
{ "contact_id": "contact_uuid", "visitor_id": "visitor_uuid", "pages_visited": ["/pricing", "/features", "/signup"], "identified_at": "2026-05-25T12:34:56.789000Z"}test
Verification ping. Use this when first wiring a subscription to confirm signature validation works.
{ "test": true, "message": "This is a test webhook from Graph8 Studio"}Full Event List (33 types)
For the complete catalog (campaign lifecycle, content generation, intelligence, enrichment, audience, sequence deployment, engagement per channel, meetings), call GET /webhooks/events — it returns the live registry. Subscribe to all events via events: ["*"] if you want a single firehose listener.