Appearance
Webhooks
Webhooks let an external system trigger work in your account (run an agent, workflow, or factory, or call an MCP tool), or act as an OAuth 2.0 callback endpoint when connecting OAuth-protected external MCP servers.
There are three webhook types:
generic_trigger— a public URL that, when called with the correct secret (or a valid GitHub-style HMAC signature), runs an agent, workflow, or factory, or calls an MCP tool with the incoming payload.oauth_callback— a redirect/callback endpoint used during an OAuth 2.0 authorization-code flow. These are created automatically when you connect an OAuth-protected external MCP server; you rarely create them by hand.outbound— configuration for posting results to an external URL (cannot be triggered externally).
Management endpoints (/v1/webhooks*) require x-api-key. The trigger and OAuth callback endpoints are public and protected by a secret/state instead.
Config & response types
ts
type WebhookType = "oauth_callback" | "generic_trigger" | "outbound"
type WebhookAction = "run_agent" | "run_workflow" | "run_factory" | "call_mcp_tool" | "custom"
type WebhookVerificationMode = "header_secret" | "hmac_sha256"
interface TriggerConfig {
action: WebhookAction
actionParams: {
agentName?: string // for "run_agent"
promptTemplate?: string // supports {{placeholder}} substitution from the payload (incl. dot paths)
workflowId?: string // for "run_workflow"
dataPayload?: string // for "run_workflow": optional static JSON string
factoryConfigId?: string // for "run_factory"
initialCarryPayload?: string // for "run_factory": optional static JSON string
mcpServerName?: string // for "call_mcp_tool"
mcpToolName?: string
mcpToolParams?: Record<string, any>
customHandler?: string // for "custom" — reserved: currently acknowledges without side effects
}
conditions?: WebhookConditionGroup // optional gate; absent = always run
payloadTransform?: Record<string, unknown> // optional whitelist mapping; absent = payload unchanged
executionMode?: "sync" | "async" // default "sync"; forced "async" in hmac_sha256 mode
sendResultInResponse?: boolean // return the action result in the trigger response
resultWebhookUrl?: string // also POST the result to this URL
}
// Conditions evaluate against { body, headers } (see the platform guide for
// operator semantics, wildcards, and limits):
interface WebhookConditionLeaf {
path: string // e.g. "body.action", "headers.x-github-event"
op: "eq" | "neq" | "in" | "nin" | "exists" | "not_exists" | "contains" | "startsWith" | "gt" | "lt"
value?: string | number | boolean | null | Array<string | number | boolean | null>
}
interface WebhookConditionGroup {
all?: Array<WebhookConditionLeaf | WebhookConditionGroup>
any?: Array<WebhookConditionLeaf | WebhookConditionGroup>
}
interface OutboundConfig {
targetUrl: string
method?: "POST" | "PUT" | "PATCH"
headers?: Record<string, string>
eventTypes: string[]
retry?: { maxAttempts: number; delayMs: number }
}
interface OAuthConfig {
provider: string
state: string // CSRF state; must match on callback
codeVerifier?: string // PKCE
redirectUri: string
frontendOrigin?: string
scopes?: string[]
mcpServerName?: string
authorizationServer?: string
tokenEndpoint?: string
clientId?: string
clientSecret?: string
resource?: string // RFC 8707 resource indicator
tokenEndpointAuthMethod?: "none" | "client_secret_post" | "client_secret_basic"
registrationMode?: "cimd" | "dcr" | "manual"
}
// Returned by create/get/list. The raw secret is returned exactly once, as a
// top-level field of the creation response — never inside WebhookResponse:
interface WebhookResponse {
webhookId: string
name: string
description?: string
type: WebhookType
enabled: boolean
verificationMode?: WebhookVerificationMode // present when set; default behavior is "header_secret"
triggerUrl?: string // generic_trigger only: `${baseUrl}/webhooks/trigger/${webhookId}`
callbackUrl?: string // oauth_callback only: `${baseUrl}/webhooks/oauth/callback/${webhookId}`
createdAt: string // Date
updatedAt: string
lastTriggeredAt?: string
triggerCount: number
expiresAt?: string
tags?: string[]
}Management endpoints
POST /v1/webhooks
Create a webhook.
Body (CreateWebhookRequest):
ts
{
webhookId?: string
name: string // required
description?: string
type: WebhookType // required
oauthConfig?: OAuthConfig // required when type === "oauth_callback" (needs state + redirectUri)
triggerConfig?: TriggerConfig// required when type === "generic_trigger" (needs action)
outboundConfig?: OutboundConfig // required when type === "outbound" (needs targetUrl)
expiresAt?: string
tags?: string[]
secret?: string // optional bring-your-own secret: 16-128 printable ASCII chars, no whitespace
verificationMode?: WebhookVerificationMode // "hmac_sha256" only valid for generic_trigger
}A secret is generated when you don't supply one, and only its SHA-256 hash is stored. For generic_trigger webhooks the raw secret is returned once as a top-level secret field of the 201 response — capture it there; it cannot be retrieved again. In hmac_sha256 mode an encrypted copy is also stored, since HMAC verification needs the raw value as a key. (For oauth_callback webhooks, you may instead pass an x-api-key header at create time, whose hash becomes the secret.)
Example (generic trigger that runs an agent):
ts
await fetch("https://agents.missionsquad.ai/v1/webhooks", {
method: "POST",
headers: { "x-api-key": process.env.MSQ_API_KEY!, "Content-Type": "application/json" },
body: JSON.stringify({
name: "Lead intake",
type: "generic_trigger",
triggerConfig: {
action: "run_agent",
actionParams: {
agentName: "lead-qualifier",
promptTemplate: "Qualify this lead: name={{name}}, company={{company}}, note={{note}}"
},
sendResultInResponse: true
}
})
});Response (HTTP 201): { success: true, webhook: WebhookResponse } — plus, for generic_trigger webhooks, top-level secret (the raw secret, shown only this once) and secretNotice.
Status codes: 400 for missing name/type, a missing type-specific config, or any invalid configuration (bad secret format, hmac_sha256 on a non-generic webhook, invalid conditions/transform, executionMode: "sync" combined with hmac_sha256, missing workflowId/factoryConfigId for the run actions); 401 unauthenticated; 500 on error.
GET /v1/webhooks
List your webhooks. Query: type (filter), enabled ("true"/"false"), limit (default 50), skip (default 0). Response { success: true, webhooks: WebhookResponse[], count: number }.
GET /v1/webhooks/:webhookId
Fetch one webhook. Response { success: true, webhook: WebhookResponse }; 404 when not found.
PUT /v1/webhooks/:webhookId
Update a webhook. Body (UpdateWebhookRequest):
ts
{
name?: string
description?: string
enabled?: boolean
triggerConfig?: TriggerConfig
outboundConfig?: OutboundConfig
tags?: string[]
}(type, oauthConfig, the secret, and verificationMode are not updatable — recreate the webhook to change how it authenticates. An updated triggerConfig is validated like at creation time; invalid configs return 400.) Response { success: true, message: "Webhook updated successfully" }; 404 when not found.
DELETE /v1/webhooks/:webhookId
Delete a webhook and its execution history and OAuth tokens. Response { success: true, message: "Webhook deleted successfully" }; 404 when not found.
GET /v1/webhooks/:webhookId/executions
List execution history (audit trail). Query: limit (default 50), skip (default 0).
Response { success: true, executions: WebhookExecution[], count: number } where:
ts
interface WebhookExecution {
executionId: string
webhookId: string
username: string
triggeredAt: string
success: boolean
payload: any // the original inbound payload (never the transformed one)
response?: any
error?: string
duration: number
statusCode?: number
sourceIp?: string
userAgent?: string
status?: "running" | "success" | "failed" | "skipped"
skippedReason?: string // "conditions_not_met" | "ping"
deliveryId?: string // sender's idempotency key, e.g. GitHub's X-GitHub-Delivery
}GET /v1/webhooks/:webhookId/token
For oauth_callback webhooks, return stored OAuth token metadata (never the tokens themselves).
Response:
ts
{
success: true,
token: {
provider: string
tokenType: string
scopes?: string[]
expiresAt?: string
createdAt: string
updatedAt: string
}
}400 when the webhook is not an oauth_callback; 404 when the webhook or token is not found.
Public endpoints
POST /webhooks/trigger/:webhookId
Trigger a generic_trigger webhook. Public, authenticated per the webhook's verification mode:
header_secret(default):X-Webhook-Secret: <secret>header, verified against the stored hash (missing →401).hmac_sha256:X-Hub-Signature-256: sha256=<hex>— the HMAC-SHA256 of the raw request body using the shared secret, GitHub-style, compared in constant time (missing/invalid →401).- Request body: a JSON object (
Content-Type: application/jsonrequired — other content types →415; invalid JSON or a non-object payload →400).
Pipeline before the action runs:
- Duplicate deliveries: when an
X-GitHub-Deliveryheader is present and was already processed for this webhook, the request answers200with{ success: true, skipped: true, reason: "duplicate_delivery" }and runs nothing. - Ping (
hmac_sha256mode only): anX-GitHub-Event: pingdelivery answers{ success: true, message: "pong" }without dispatching. - Conditions: when configured and not met, the request answers
200with{ success: true, skipped: true, reason: "conditions_not_met" }, records askippedexecution, and does not count a trigger. - Transform: when configured, the payload is reshaped before the action (see the platform guide).
How the payload is used:
action: "run_agent"—{{placeholder}}tokens inpromptTemplateare replaced with payload values (dot paths reach nested values; objects render as JSON; unknown placeholders stay literal); the result is sent as a user message toagentNameunder your account.action: "run_workflow"— starts a workflow run; the payload (transformed, or the staticdataPayload, or the body) becomes the run's data payload. The run executes in the background and the response carriesrunId.action: "run_factory"— starts a factory run (trigger: "webhook") the same way viainitialCarryPayload; subject to the concurrent factory run limit.action: "call_mcp_tool"— the tool is called with{ ...mcpToolParams, ...payload }(payload keys override configured params).
Response (sync mode, the default): the action result is returned with the action's status code. When sendResultInResponse is false, you get { success: true, message: "Webhook triggered successfully" }.
Response (async mode — opt-in via executionMode: "async", always on in hmac_sha256 mode): HTTP 202 { success: true, accepted: true, executionId, runId? } immediately; the action completes in the background and its outcome lands in the execution record.
Status codes: 401 (missing/invalid secret or signature), 404 (webhook not found), 403 (disabled), 415 (wrong content type), 400 (invalid JSON / non-object payload / cannot trigger an outbound webhook externally / unknown type), 202 (async accepted), 200/500 from the action.
bash
curl -X POST "https://agents.missionsquad.ai/webhooks/trigger/$WEBHOOK_ID" \
-H "X-Webhook-Secret: $WEBHOOK_SECRET" \
-H "Content-Type: application/json" \
-d '{"name":"Ada","company":"Analytical Engines","note":"wants a demo"}'GET|POST /webhooks/oauth/callback and /webhooks/oauth/callback/:webhookId
The OAuth 2.0 authorization-code redirect target. Public — the webhook is resolved by :webhookId or by the OAuth state parameter, and the inbound state must match the stored value. On success the server exchanges the code for tokens at the provider's token endpoint, stores them encrypted, and (for an external MCP server) pushes them into the running server.
For GET requests the OAuth params arrive as query params (code, state, error, error_description); for POST, as the body. By default the endpoint returns an HTML page that posts a completion message to the opener window and closes the popup; send Accept: application/json to get a JSON result instead. This flow is set up for you by MCP Servers (Connect & OAuth) — you do not call it directly.