Substy Developers

Conversation webhooks

Receive handoffs, AI status changes, blocked conversations, and message send metadata without polling conversations.

Register a webhook for each creator you want to follow. Substy sends JSON to your HTTPS endpoint when a subscribed event occurs. Events contain identifiers and timestamps, without message text or media.

Register an endpoint

Use an API key with Inbox: Manage, the role permission to view fans, and access to the creator.

curl --request POST \
  --header "Authorization: Bearer $SUBSTY_API_KEY" \
  --header 'Content-Type: application/json' \
  --data '{"url":"https://your-app.example/substy/webhook","events":["conversation.handoff","conversation.ai_enabled","conversation.message_sent","conversation.ai_status_changed","conversation.ai_blocked_changed"]}' \
  'https://api.substy.ai/v1/agencies/AGENCY_ID/creators/CREATOR_ID/webhooks'

The creation response includes id, creatorId, url, events, createdAt, and signingSecret inside the normal API data envelope. Store the signing secret securely. Listing webhooks does not return it. If you omit events, all five event types are selected. An existing subscription keeps its selection: to receive a newly added event type, delete the subscription and create a replacement that includes it.

Each API connection can register one endpoint per creator. Repeat registration for other creators; they can share the same destination URL, but each registration has its own signing secret. Registering a second endpoint for the same creator and connection returns 409.

MethodPath, after /v1/agencies/{agencyId}/creators/{creatorId}Purpose
POST/webhooksCreate a subscription
GET/webhooksList this connection's subscription
DELETE/webhooks/{webhookId}Remove it and cancel pending deliveries

To change the URL, event selection, or signing secret, delete the subscription and create a replacement. Events during that gap are not retained for the replacement. A request already in flight may still arrive after deletion. Subscriptions belong to the API connection that created them; another key cannot list or delete them.

If a creator moves to another agency, the same API connection can list and delete its previous subscription using the creator's current agency, provided it has the required access there. Delete the previous subscription before registering a replacement; otherwise creation returns 409. The previous agency can no longer manage that creator's subscriptions.

Events

TypeMeaningData
conversation.handoffSubsty records a human handoff after an AI conversationcreatorId, fanId, handoffAt
conversation.ai_enabledDeprecated: use conversation.ai_status_changed. AI is enabled for a fan or creatorcreatorId, fanId, enabledAt
conversation.message_sentSubsty confirms a message was sentcreatorId, fanId, messageId, sentByAi, sentAt
conversation.ai_status_changedAI is turned on or off for a fan or creator, for any reasoncreatorId, fanId, aiEnabled, reason, changedByEmployeeId, changedAt
conversation.ai_blocked_changedAI is on but cannot reply to a fan because there is nothing left to sell, or it can reply againcreatorId, fanId, blocked, reason, changedAt
{
  "id": "a87002d6-d219-4cbd-b612-4a2f6b258a35",
  "createdAt": "2026-09-22T12:00:00.000Z",
  "type": "conversation.message_sent",
  "data": {
    "creatorId": "creator_123",
    "fanId": "fan_456",
    "messageId": "789",
    "sentByAi": true,
    "sentAt": "2026-09-22T12:00:00.000Z"
  }
}

messageId is the platform message identifier, encoded as a string. sentByAi is the recorded attribution for that message: false includes employee and automation sends through Substy. This event does not monitor messages sent directly through OnlyFans, Inflow, or other external clients.

For creator-wide enablement, fanId is null. This changes the creator's AI setting; it does not override individual fan handoffs, disabled fan settings, or hybrid eligibility rules. For fan-level enablement, fanId identifies the affected fan. Enablement is a configuration change, not a guarantee that a reply is currently being generated.

There is no handoff-resolved event. Manual disablement and first-time hybrid ineligibility without an AI conversation do not produce a handoff event; use conversation.ai_status_changed to follow every change. Only new events after registration are delivered; registration does not backfill conversations.

AI status changes

conversation.ai_status_changed is sent whenever AI is switched on or off, including handoffs, manual changes, automations, schedules, and wallet balance. A handoff produces both conversation.handoff and conversation.ai_status_changed.

{
  "id": "3f0c5a52-8f41-4f7e-9d0a-6a1c2b7e9e10",
  "createdAt": "2026-10-04T09:04:12.000Z",
  "type": "conversation.ai_status_changed",
  "data": {
    "creatorId": "creator_123",
    "fanId": "fan_456",
    "aiEnabled": false,
    "reason": "HANDOFF_RULE_MATCHED",
    "changedByEmployeeId": null,
    "changedAt": "2026-10-04T09:04:12.000Z"
  }
}
aiEnabledreasonMeaning
falseHANDOFF_RULE_MATCHEDA handoff or hybrid limit rule matched, whether or not the AI had replied
falseMANUALLY_DISABLEDA team member or API client turned AI off
falseAUTOMATION_STOP_AIAn automation's Stop AI action ran
falseGLOBALLY_DISABLEDThe creator's AI was turned off
falseSCHEDULE_ENDEDThe creator's AI schedule ended
falseNEGATIVE_BALANCEThe agency wallet balance fell below its minimum
trueMANUALLY_ENABLEDA team member or API client turned AI on
trueSCHEDULE_STARTEDThe creator's AI schedule started
trueBALANCE_RESTOREDAI resumed after the wallet was topped up

fanId is null for creator-wide changes. AI replies in a conversation only when both the creator and the fan have AI enabled, so track both levels. New conversations start with fan-level AI on and do not produce an event. changedByEmployeeId identifies the team member who made a manual change, otherwise it is null.

Use the event id to deduplicate retries. Deliveries can arrive out of order, so keep the latest changedAt per creator and per conversation and ignore older events.

Blocked conversations

conversation.ai_blocked_changed with blocked: true is sent the first time a fan writes and the AI stays silent because no eligible script or solo bundle is left for that fan. The AI setting does not change, so no conversation.ai_status_changed event is sent. This is the moment to take over the conversation or add content.

{
  "id": "6b1f0f44-3c1e-4b8e-a0a7-0f4f3a2b9d11",
  "createdAt": "2026-10-08T09:00:00.000Z",
  "type": "conversation.ai_blocked_changed",
  "data": {
    "creatorId": "creator_123",
    "fanId": "fan_456",
    "blocked": true,
    "reason": "NO_SELLABLE_CONTENT",
    "changedAt": "2026-10-08T09:00:00.000Z"
  }
}

Further fan messages while the conversation is blocked do not produce more events. blocked: false is sent when the AI replies to that fan again, for example after a script is added. A conversation is blocked only after a fan message the AI could not answer, so a fan who stops writing never produces an event. Track this per conversation alongside the AI status: the AI replies only when AI is enabled and the conversation is not blocked.

Verify deliveries

Every POST includes:

HeaderValue
Webhook-IdThe event's id, unchanged across retries
Webhook-TimestampUnix time in seconds for this delivery attempt
Webhook-Signaturev1= followed by an HMAC-SHA256 hex digest

Compute the HMAC with the complete signingSecret string as the key and ${timestamp}.${rawBody} as the input. Compare using a timing-safe comparison. Verify the raw request body before parsing JSON, reject timestamps more than five minutes from your server's clock, and verify that Webhook-Id matches the body id. Each retry receives a new timestamp and signature.

The destination must use HTTPS on port 443 and resolve to public IP addresses. Redirects are not followed.

Acknowledge and retry

Store the event durably, return any 2xx response promptly, and process it asynchronously. Substy checks for new deliveries every second; this is not a guaranteed delivery deadline. Slow receivers, retries, and queue load can delay delivery.

Delivery is at least once: deduplicate using id. Requests have a ten-second deadline. Non-2xx responses and transport failures retry with exponential delays starting at five seconds, capped at one hour, for up to 12 attempts. Delivery order is not guaranteed; use the event timestamp when applying state updates and do not let an older event overwrite a newer one.

Substy verifies the creating connection, API key, current role permissions, creator assignment, and agency ownership. Successful authorization checks are reused for up to 15 seconds, so access changes can take that long to affect delivery. If authorization fails or its provider is unavailable, Substy defers for one minute without consuming a delivery attempt or sending the event. Delivery resumes only after access can be verified again. Pending deliveries expire 24 hours after they are queued, including when authorization remains unavailable; expired events are not replayed when access returns. Successful delivery records are retained internally for seven days and failed or expired records for 30 days.

On this page