Book a build call

Webhooks

Mrs. NetShow is an AI agent.

Subscribe to signed MCP Events and integrate every verified provider webhook NetShow receives.

Guided article

Audience: a developer, or an AI agent, that wants NetShow to tell it when something happens, or that runs a service NetShow listens to (Stripe, Paddle, Twilio, OpenAI, Slack, Telegram, Linear, Meta). Scope: the events NetShow sends to your HTTPS address (MCP Events), and every inbound webhook NetShow receives: what calls it, how the call is proved, which events it acts on and what it answers. How this guide was written: from the code on main (the class and method are named under each door), with the host shown as https://app.netshow.ai. A door marked off until your administrator turns it on sits behind a switch that ships off; while it is off the door answers 404 (an MCP method answers Method not found). Signing secrets live in NetShow's server configuration and are never shown in a page or a response.

13 min read 2,717 words Developer reference

Which webhook do I need?

You want to Use Section
Hear when one of your agents gets a lead, a quote request or finishes a job MCP Events: events/subscribe on your private MCP connection Events NetShow sends you
Hear when an A2A task you sent finishes The task's push notification config The A2A Protocol guide
Point Stripe, Paddle, Twilio, OpenAI, Slack, Telegram, Linear or Meta at NetShow The inbound door for that provider Webhooks NetShow receives

What you can do without a key

Nothing here is keyless for you as a caller. Events go only to the owner of the agent, over the owner's own MCP connection. Every inbound door is proved by the provider's own signature (or a token the provider echoes), not by a NetShow key; a call without that proof is refused.

Events NetShow sends you (MCP Events)

Classes: AliveMcpEvents (subscribe), McpEventDelivery (raise and send), McpEventWebhook (sign and post). An agent's owner subscribes an HTTPS address to one event for one agent, and NetShow posts a small signed JSON body to it each time that event happens. Off until your administrator turns it on: ai.features.chatgpt_mcp_events, ai.features.chatgpt_plugin_extensions and ai.features.alive_plugin_owner_demo must all be on, and the delivery lane ai.spend.lanes.chatgpt_mcp_events must have its daily ceiling and its daily attempt allowance set (both ship 0, which is closed).

The events

Event When data.status
netshow.lead.created A new lead reached your agent. new
netshow.quote.requested A visitor asked your agent for a quote. requested
netshow.agent.work_completed A job your agent ran finished. completed

An event carries an opaque reference, a status and a link that opens NetShow. It never carries a visitor's name, email, phone, words or a screenshot, and it never asks you to send or charge anything: mode is always draft_only.

Subscribe: POST /api/mcp/workboard, method events/subscribe

Use the owner's MCP connection token (the Authentication and Keys guide shows how the owner makes one) and the MCP revision 2026-07-28. A connection made for one agent can subscribe for that agent only.

curl --max-time 30 https://app.netshow.ai/api/mcp/workboard \
  -H 'Authorization: Bearer <MCP_CONNECTION_TOKEN>' -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"events/subscribe","params":{"name":"netshow.lead.created","arguments":{"agent_id":"42"},"delivery":{"mode":"webhook","url":"https://hooks.example.com/netshow","secret":"whsec_<base64 of 24 to 64 random bytes>"},"ttlMs":2592000000}}'

Rules the request must meet:

  • name is one event from the table; arguments is exactly {"agent_id": "<your agent's id>"}.
  • delivery.mode is webhook; delivery.url is a public https address on port 443, at most 512 characters, with no fragment and no user name or password in it. NetShow resolves the host and refuses any private address, now and again at each send, and never follows a redirect.
  • delivery.secret is whsec_ followed by base64 of 24 to 64 bytes. You keep it; NetShow stores it encrypted.
  • ttlMs is how long the subscription lives, at most 30 days (the default). Subscribe again before refreshBefore to keep it.
  • An owner may hold 5 live subscriptions.

Before anything is stored, NetShow proves the address is yours. It posts (signed exactly like an event, below):

{"type": "verification", "challenge": "<48 hex characters>"}

and your address must answer 200 with the same challenge, in at most 4 KB, within 5 seconds:

{"challenge": "<the same 48 hex characters>"}

Response:

{"jsonrpc": "2.0", "id": 1, "result": {"id": "mcpsub_<32 hex>", "refreshBefore": "2026-11-04T11:00:00+00:00", "cursor": null, "truncated": false}}

Subscribing the same event, agent and address again with a new secret rotates it: for the next 5 minutes each delivery is signed with both secrets.

events/list (same door, same revision) returns the three events with their inputSchema and payloadSchema. events/unsubscribe takes the same name, arguments and delivery and answers {}.

Error code Message Why
-32601 Method not found A switch above is off.
-32602 MCP Events requires protocol 2026-07-28. The MCP-Protocol-Version header is missing or older.
-32602 Use an authorized agent and a public HTTPS webhook. Unknown event, an agent that is not yours, or an address that is not a public https address.
-32602 Use a valid signing secret, finite lifetime and no replay cursor. A bad whsec_ secret, ttlMs under 1000, or a cursor.
-32602 Too many active subscriptions. You already hold 5.
-32015 The callback did not verify. Your address did not echo the challenge; data.reason is timeout or challenge_failed.
-32015 Callback verification is unavailable. The delivery lane is closed (data.reason is lane_closed).
-32001 Use your private MCP connection. The token is not an MCP connection token.

The plain /api/mcp door also answers events/list, events/subscribe and events/unsubscribe (class McpEventSubscriptions, NetShow's earlier draft shape, switch ai.features.chatgpt_mcp_events). Its callback check is not wired to a sender yet, so a subscription made there never becomes active: build on /api/mcp/workboard.

What a delivery looks like

POST to your address, Content-Type: application/json, at most 2 KB:

{
  "eventId": "mcpevt_<32 hex>",
  "name": "netshow.lead.created",
  "timestamp": "2026-10-05T11:00:00+00:00",
  "data": {
    "reference": "mcpevt_<32 hex>",
    "status": "new",
    "link": "https://app.netshow.ai/...",
    "mode": "draft_only",
    "next_step": "Draft a follow-up plan for the owner. Open NetShow to review the lead; send nothing."
  },
  "cursor": null
}

Headers (the Standard Webhooks shape):

Header Value
webhook-id The message id: the eventId, or msg_verification_... for the challenge.
webhook-timestamp Unix seconds when it was sent.
webhook-signature One or more v1,<base64> entries separated by spaces.
X-MCP-Subscription-Id Your mcpsub_... id.

To check a delivery: base64-decode the part of your secret after whsec_ to get the key; compute HMAC-SHA256 of <webhook-id>.<webhook-timestamp>.<raw body> with that key; base64-encode it and compare (in constant time) with each v1, entry. Refuse a timestamp more than 5 minutes from your clock.

import base64, hashlib, hmac
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{msg_id}.{timestamp}.".encode() + raw_body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
ok = any(hmac.compare_digest(expected, s.split(",", 1)[1]) for s in signature_header.split())

Answer any 2xx to accept. A 408, 429 or 5xx (or no answer in 5 seconds) is tried again, up to 3 attempts, 30 and then 60 seconds apart, within the event's 24 hours; any other answer stops it. Delivery is at least once: use eventId to drop a repeat. A subscription that was revoked, has expired, or whose MCP connection was revoked stops receiving at once.

Webhooks NetShow receives

Each door below is for one provider. Point the provider at the path, give NetShow the matching signing secret through your administrator, and the provider's own signature does the rest. Every door under the api prefix answers JSON.

Provider Door Proof Switch
Stripe (membership billing) POST /webhook/stripe Stripe-Signature Always on
Stripe (plan tier checkout) POST /api/stripe/webhook/tier Stripe-Signature Always on
Stripe (pay to play: AI employees and one-time services) POST /webhooks/stripe/commerce Stripe-Signature ai.features.commerce_pay_to_play
Stripe (Agentic Commerce) POST /api/agent-commerce/webhook Stripe-Signature Always on (with its own kill switch)
Paddle POST /api/paddle/webhook Paddle-Signature ai.features.paddle_webhook
OpenAI Realtime (phone calls over SIP) POST /api/realtime/webhook webhook-signature Always on
Twilio (calls, keypad, in-call actions) The twilio/ws and dtmf doors below, POST /twilio/call, POST /twilio/forward X-Twilio-Signature Always on
Twilio ConversationRelay POST /api/twilio/voice/conversation-relay, POST /telephony/twilio/answer X-Twilio-Signature ai.features.phone_conversation_relay
Messaging channels (SMS, Telegram) POST /api/channels/{channel}/inbound The channel's own signature ai.features.agent_channels
Slack POST /api/channels/slack/events X-Slack-Signature ai.features.agent_channels
Telegram (live chat operator bot) POST /live-chat/telegram/webhook X-Telegram-Bot-Api-Secret-Token ai.features.live_chat_telegram
Meta (Facebook page messages) /webhook/meta/verifyWebhook hub.verify_token, then X-Hub-Signature-256 Always on
Linear POST /api/linear/webhook Linear-Signature Always on
A dial request from your system POST /webhook/create_twilio_call/{id} X-Dial-Token Always on
Google Sheets check-in POST /api/daily_checkin/sheet/webhook None (stores nothing) Always on

Stripe

All four Stripe doors check Stripe-Signature over the raw body with Stripe's own library (Stripe\Webhook::constructEvent); a bad signature answers 400, and a door with no secret configured refuses every call.

POST /webhook/stripe (class StripeController, method webhook). Membership billing. It reads the checkout type from the event's metadata: a membership event goes to the membership handler, an AI sales employee checkout to its own fulfilment, and anything else answers 200 {"status": "ignored", "message": "Not a membership event"}. Bad signature: {"error": "Invalid signature"}.

POST /api/stripe/webhook/tier (class StripeWebhookController, method handle, 120 a minute). Plan tier checkout and its lifecycle: checkout.session.completed and checkout.session.async_payment_succeeded grant the tier; customer.subscription.updated, customer.subscription.deleted and invoice.payment_failed end it when the subscription has ended.

POST /webhooks/stripe/commerce (class PayToPlayWebhookController, method handle, 300 a minute). Off until your administrator turns it on (ai.features.commerce_pay_to_play). It acts on checkout.session.completed, checkout.session.async_payment_succeeded, invoice.paid, invoice.payment_failed, customer.subscription.updated, customer.subscription.deleted, charge.succeeded and charge.dispute.created; charge.dispute.closed needs ai.features.chargeback_desk, and charge.refunded, charge.refund.updated, refund.created and refund.updated need ai.features.commerce_refund_rows. Each event is applied once ({"status": "duplicate"} on a repeat). A failure answers 500 so Stripe tries again.

POST /api/agent-commerce/webhook (class AgentCommerceWebhookController, method handle, 60 a minute). Stripe's Agentic Commerce: checkout.session.completed for an agent-started checkout, data_management.import_set.succeeded and data_management.import_set.failed for catalog imports. A missing header answers 400 {"error": "Missing signature"}. While the service's kill switch is set, every call answers 200 {"status": "disabled"}. A checkout that an agent did not start, or that is not paid, answers {"status": "skipped"}. With ai.features.agent_commerce_fulfilment on, a paid agent order is accepted for fulfilment and answers {"status": "fulfilled"} (or the order's current status).

Paddle

POST /api/paddle/webhook (class PaddleWebhookController, method index, 60 a minute). Off until your administrator turns it on (ai.features.paddle_webhook). The Paddle-Signature header is ts=<10 digits>;h1=<64 hex>, an HMAC-SHA256 of <ts>:<raw body>. Bodies over 1 MB answer 413. PaddleEventRecorder acts on transaction.completed and subscription.canceled.

Status status Why
200 the recorded outcome Payment update received.
401 unverified The signature did not match.
409 unmatched or reconciliation The payment could not be matched to a subscription.
422 invalid The event or plan is not recognized.
503 unavailable No secret is configured, or the update could not be recorded (retry).

OpenAI Realtime (phone calls over SIP)

POST /api/realtime/webhook (class RealtimeWebhookController, method handleWebhook, 60 a minute). The Standard Webhooks headers webhook-id, webhook-timestamp and webhook-signature: an HMAC-SHA256 of <id>.<timestamp>.<raw body> with the decoded whsec_ secret, a timestamp within 5 minutes. A failure answers 401 {"status": "invalid_signature"}. It acts on realtime.call.incoming: it accepts the call on the NetShow voice when the phone bridge is on, else answers {"status": "sip_bridge_off"}. A repeated event answers {"status": "duplicate_ignored"}; any other event {"status": "ignored"}.

Twilio

Every Twilio door runs the VerifyTwilioSignature middleware, which checks X-Twilio-Signature with Twilio's own RequestValidator against the account's auth token. No token configured, a missing header or a wrong signature: 403.

Door Class and method What Twilio sends
POST /api/twilio/ws/agent-data SmsController@getAgentDataForInboundCall Which agent answers an inbound call.
POST /api/twilio/ws/agent-voice-for-inbound SmsController@getSelectedVoiceForInboundCall Which voice it speaks in.
POST /api/twilio/ws/openai/realtime/agentdata SmsController@getDataForOpenAiRealTimeInboundCall The realtime call's agent setup.
POST /api/twilio/ws/openai/realtime/agentdata/deepgram SmsController@getDataForOpenAiRealTimeInboundCallDeepgram The same, for the Deepgram lane.
POST /api/twilio/ws/openai/deepgram/sendSms SmsController@toolAction_SendSmsAPI An in-call "send a text" action.
POST /api/twilio/ws/openai/deepgram/callForward SmsController@toolAction_ForwardCallAPI An in-call "forward this call" action.
POST /api/twilio/ws/openai/deepgram/sendEmail SmsController@toolAction_SendEmailAPI An in-call "send an email" action.
POST /api/dtmf/gather and GET /api/dtmf/menu DtmfController@handleGather, DtmfController@menu Keypad digits and the phone menu (10 a minute).
POST /twilio/call SmsController@twilio_call The live call leg.
POST /twilio/forward SmsController@handleForwardedCall The forwarded call leg.
POST /api/twilio/voice/conversation-relay ConversationRelayVoiceWebhookController An inbound call handed to ConversationRelay; answers TwiML. Off until your administrator turns it on (ai.features.phone_conversation_relay).
POST /telephony/twilio/answer TwilioRelayAnswerController The relay worker's answer. Needs ai.features.voice_runtime, ai.features.phone_conversation_relay and ai.features.phone_conversation_relay_worker.

Messaging channels and Slack

POST /api/channels/{channel}/inbound (class ChannelWebhookController, 30 a minute). One door for each messaging channel: sms (switch ai.features.channel_sms) and telegram (switch ai.features.channel_telegram), both also behind ai.features.agent_channels. A channel that is off, or not built, answers 404 before anything else runs. ChannelBridge checks the channel's own signature (Twilio's for SMS, Telegram's secret token), then answers the sender with the agent's reply on the same channel only.

POST /api/channels/slack/events (class SlackEventsController, method handle). Off until your administrator turns it on (ai.features.agent_channels). The VerifySlackSignature middleware checks X-Slack-Signature against X-Slack-Request-Timestamp and the raw body (403 on a mismatch). It answers Slack's url_verification with {"challenge": "..."} and handles event_callback; anything else answers {"ok": true}.

POST /live-chat/telegram/webhook (class LiveChatTelegramController, method webhook, 300 a minute). Off until your administrator turns it on (ai.features.live_chat_telegram). Telegram sends the secret token you gave setWebhook in X-Telegram-Bot-Api-Secret-Token; a mismatch answers 403 {"ok": false}. A verified update always answers 200 {"ok": true}, and acts only for the configured operators in the configured chat.

Meta (Facebook page messages)

/webhook/meta/verifyWebhook (class SocialSettingController, method verifyWebhook). GET is Meta's handshake: with hub.mode=subscribe and the matching hub.verify_token it echoes hub.challenge, else 403 Verification failed. POST delivers page messages: it needs X-Hub-Signature-256: sha256=<hex>, an HMAC of the raw body with the Meta app secret (403 when it does not match), a body under 256 KB, and answers 200 Event queued; the reply is worked by a background job.

Linear

POST /api/linear/webhook (class LinearWebhookController, method handle, 120 a minute). Linear-Signature is an HMAC-SHA256 of the raw body (401 {"status": "invalid_signature"}); a body whose webhookTimestamp is missing or stale answers 401 {"status": "stale_or_missing_timestamp"}. Each delivery is accepted once (by Linear-Delivery) for 6 hours; a repeat answers {"status": "duplicate_ignored"}. It records the event for NetShow's operations board and starts nothing on its own.

A dial request from your system

POST /webhook/create_twilio_call/{id} (class TwilioController, method twilio_webhook, 5 a minute). Asks the phone agent {id} to place a call. It needs the dial token made for that agent, as X-Dial-Token or ?token= (class DialWebhookToken); without it: 403 {"error": "A valid dial token for this agent is required."}. The token is made from NetShow's own server key for that one agent id and is shown on no page yet: ask your NetShow administrator for it.

Google Sheets check-in

POST /api/daily_checkin/sheet/webhook (class GoogleSheetCheckinController, method handleWebhook, 10 a minute). A sheet automation may call it; it logs only the shape of the row (never its cells) and answers 202 {"received": true, "stored": false}. Nothing is stored yet.

How to get a key

For events, the owner of the agent makes an MCP connection (the Authentication and Keys guide, "The owner's MCP connection card" and "MCP sign-in") and subscribes with it; the whsec_ secret is yours to make and keep. For inbound doors there is no NetShow key: give the provider's signing secret to your NetShow administrator, who stores it in the server configuration, and the provider's signature proves each call.

Was this helpful?

We can turn this into interactive help, search, and guided checklists next.

Previous guide

Authentication and Keys

Ready to meet your AI agent?

New accounts open soon.

Join the opening list Talk to our agent
AIMrs. NetShow