Skip to content

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.

Example

Terminal window
curl "https://api.graph8.com/api/v1/webhooks/events" \
-H "Authorization: Bearer $API_KEY"

Response

{
"data": [
{ "event": "campaign.created" },
{ "event": "campaign.updated" },
{ "event": "campaign.deleted" },
{ "event": "campaign.launched" },
{ "event": "campaign.status_changed" },
{ "event": "document.created" },
{ "event": "document.updated" },
{ "event": "document.generated" },
{ "event": "intelligence.completed" },
{ "event": "research.completed" },
{ "event": "company.enriched" },
{ "event": "person.enriched" },
{ "event": "company_intelligence.completed" }
]
}

List Webhooks

GET /webhooks

Returns all webhook subscriptions for your organization.

Query Parameters

ParameterTypeDefaultDescription
is_activebooleanFilter by active status

Example

Terminal window
curl "https://api.graph8.com/api/v1/webhooks" \
-H "Authorization: Bearer $API_KEY"

Response

{
"data": [
{
"id": "wh-abc",
"name": "CRM Sync",
"url": "https://example.com/webhooks/graph8",
"events": ["campaign.created", "campaign.updated"],
"is_active": true,
"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

FieldTypeRequiredDescription
urlstringYesTarget URL for delivery
eventsstring[]YesEvent types to subscribe to
namestringNoHuman-readable name

Example

Terminal window
curl -X POST "https://api.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

{
"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-Webhook-Signature header. Verify it with HMAC-SHA256:

import hashlib, hmac
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(), payload, 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

FieldTypeDescription
urlstringTarget URL
eventsstring[]Event types
namestringHuman-readable name
is_activebooleanEnable or disable

Delete Webhook

DELETE /webhooks/{webhook_id}

Returns 204 No Content on success.


List Deliveries

GET /webhooks/{webhook_id}/deliveries

Returns delivery attempts for a webhook, with pagination.

Query Parameters

ParameterTypeDefaultDescription
pageinteger1Page number
limitinteger50Items per page (max 200)
statusstringFilter by delivery status (success, failed, pending)

Example

Terminal window
curl "https://api.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:

HeaderExamplePurpose
Content-Typeapplication/json
X-Studio-Signaturesha256=8a91...c2f4HMAC-SHA256 over {timestamp}.{raw_body}
X-Studio-Timestamp1716624896Unix epoch seconds, also part of the signed message
X-Studio-Eventcampaign.launchedQuick filter without parsing body
X-Studio-Delivery-Iddel-abc-uuidUnique 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)

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",
"email": "[email protected]",
"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_skipped
  • engagement.call_dispatched
  • engagement.sms_sent / sms_replied
  • engagement.whatsapp_sent
  • engagement.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",
"email": "[email protected]",
"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_email": "[email protected]",
"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",
"email": "[email protected]",
"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.