All docs

Webhooks

Subscribe to events and manage webhook deliveries

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

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." }
  ]
}
FieldTypeDescription
eventstringThe event type to subscribe to
categorystringGrouping for the event (e.g. Campaign, Engagement)
descriptionstring | nullWhat the event means / when it fires

Event catalog

EventCategoryDescription
campaign.createdCampaignA campaign was created.
campaign.updatedCampaignA campaign’s configuration was updated.
campaign.deletedCampaignA campaign was deleted.
campaign.launchedCampaignA campaign was launched and is now sending.
campaign.pausedCampaignA campaign was paused.
campaign.completedCampaignA campaign finished all of its steps.
campaign.status_changedCampaignA campaign’s status changed.
document.generatedContentA campaign document finished generating.
document.failedContentA campaign document failed to generate.
campaign.content_readyContentAll content for a campaign is ready.
intelligence.completedIntelligenceAn intelligence/research run completed.
intelligence.failedIntelligenceAn intelligence/research run failed.
company.enrichedEnrichmentA company record was enriched.
company_intelligence.completedEnrichmentCompany intelligence gathering completed.
audience.readyAudienceAn audience finished building and is ready to use.
audience.failedAudienceAn audience failed to build.
sequence.deployedSequenceA sequence was deployed.
sequence.startedSequenceA sequence started running for a contact.
sequence.pausedSequenceA sequence was paused.
sequence.completedSequenceA sequence completed all steps for a contact.
engagement.email_sentEngagementAn email was sent to a contact.
engagement.email_repliedEngagementA contact replied to an email.
engagement.email_bouncedEngagementAn email to a contact bounced.
engagement.email_skippedEngagementAn email step was skipped for a contact.
engagement.call_dispatchedEngagementA call task was dispatched to the dialer.
engagement.sms_sentEngagementAn SMS was sent to a contact.
engagement.sms_repliedEngagementA contact replied to an SMS.
engagement.whatsapp_sentEngagementA WhatsApp message was sent to a contact.
engagement.linkedin_connection_sentEngagementA LinkedIn connection request was sent.
engagement.linkedin_message_sentEngagementA LinkedIn message was sent.
engagement.linkedin_inmail_sentEngagementA LinkedIn InMail was sent.
engagement.linkedin_reply_receivedEngagementA LinkedIn reply was received from a contact.
engagement.linkedin_connection_acceptedEngagementA LinkedIn connection request was accepted.
meeting.bookedMeetingsA meeting was booked.
meeting.cancelledMeetingsA booked meeting was cancelled.
meeting.rescheduledMeetingsA booked meeting was rescheduled.

List Webhooks

GET /webhooks

Returns all webhook subscriptions for your organization.

Query Parameters

ParameterTypeDefaultDescription
is_activebooleanFilter by active status

Example

cURL

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

Python

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

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

Example

cURL

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"
  }'

Python

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

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

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

cURL

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:

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.

Python

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)

Node.js

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",
  "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.

Build with graph8