Webhooks
Webhooks let Capacity exchange event data with external systems in real time, without polling.
Capacity supports two directions:
| Direction | Name | What it does |
|---|---|---|
| Inbound | Incoming Webhooks | An external system POSTs JSON to a Capacity-generated URL. The payload is validated against a JSON Schema and can trigger an Automation or Workflow. |
| Outbound | Webhook Trigger API (POST /v1/webhooks) | A third-party event calls the Capacity API, which fires a registered webhook trigger inside Capacity. |
Most implementations use Incoming Webhooks. Use the Webhook Trigger API when the calling system needs to authenticate with a Capacity API key rather than post to an unauthenticated URL.
Part 1 — Incoming Webhooks
Where to find them
Developer Platform → My Apps → [your app] → INCOMING WEBHOOKS
Incoming Webhooks live inside an App, alongside that app's APIs, Scripts, and Conversations.
Step 1 — Create the webhook
- Open the App that should own the webhook (or create a new App)
- Go to the Incoming Webhooks tab
- Click Add New Webhook
- Enter a Name and Description — the name is what you will select when wiring up the Automation, so make it specific (e.g. "Call Ended", not "Webhook 1")
- Save
Capacity generates a unique URL for the webhook.
Step 2 — Define the JSON Schema
The schema tells Capacity what fields to expect and makes them available as variables downstream. Fields not declared in the schema are not exposed to Automations or Workflows.
{
"type": "object",
"properties": {
"call_id": {
"type": "string",
"description": "Unique identifier for the call"
},
"from_number": {
"type": "string",
"description": "Caller phone number in E.164 format"
},
"disposition": {
"type": "string",
"description": "Outcome of the call"
}
}
}
Supported types: string, number, integer, boolean, object, array.
Schema tips
- Use descriptive
descriptionvalues — they surface in the builder UI and make the variable's purpose obvious to the next implementer - Declare only the fields you actually consume. A 60-field vendor payload does not need a 60-field schema
- For nested objects, declare the parent as
"type": "object"with its ownpropertiesblock - Test with a real sample payload from the sending system before wiring anything downstream
Step 3 — Send data to the webhook
- Method:
POST - Content-Type:
application/json - Body: JSON matching the schema
curl -X POST "https://<your-capacity-webhook-url>" \
-H "Content-Type: application/json" \
-d '{
"call_id": "abc123",
"from_number": "+13145550100",
"disposition": "completed"
}'
Step 4 — Consume the payload
Create an Automation with the webhook as its trigger. Payload fields declared in the schema are available as variables to the Automation's actions — typically:
- Kicking off a Workflow and passing the payload fields in as workflow inputs
- Writing the record to CapacityDB via a stored query
- Creating or updating a Helpdesk ticket
For anything beyond a couple of steps, trigger a Workflow rather than building the logic in the Automation itself. Workflows give you instance history, retries, and step-level error visibility.
Payload pre-processing
Some vendors do not send clean JSON. Some delivers payloads in Python str() format (single quotes, True/False/None), which JSON.parse() rejects.
Handle this in a JS Function node immediately after the trigger:
var parsed = null;
var parse_error = '';
try {
parsed = JSON.parse(raw_payload);
} catch (e) {
try {
// JSON5 tolerates single quotes and trailing commas
parsed = JSON5.parse(raw_payload);
} catch (e2) {
parse_error = 'Unable to parse payload';
}
}
set_output('parse_error', parse_error);
set_output('call_id', parsed && parsed.call_id ? parsed.call_id : '');
set_output('disposition', parsed && parsed.disposition ? parsed.disposition : '');
JSON5.parse() is available in the Capacity vm2 sandbox. Remember the sandbox is synchronous only — no async/await, Promises, fetch, or setTimeout — and every declared output must be set on every code path.
Part 2 — Webhook Trigger API (outbound / authenticated inbound)
POST /v1/webhooks lets an authenticated third party fire a Capacity webhook trigger.
Headers
| Header | Value |
|---|---|
x-capacity-id | Org ID making the request |
Authorization | Bearer <api-key> |
Content-Type | application/json |
Request body
{
"service": "my_external_service",
"trigger_event_type": "order_shipped",
"attributes": {
"order_id": "SO-10432",
"carrier": "UPS"
}
}
| Field | Required | Description |
|---|---|---|
service | Yes | The service triggering the webhook |
trigger_event_type | Yes | The event value the webhook trigger listens for |
attributes | Yes | Key/value pairs passed through to the trigger |
Response: 200 on success, 400 on malformed request.
Use this over an Incoming Webhook when the sending system requires bearer-token auth, or when you need one endpoint to fan out to multiple event types.
Best Practices
Security
- Treat the generated webhook URL as a secret
- Validate payload contents downstream; schema validation confirms shape, not trustworthiness
- Rotate the webhook (delete and recreate) if the URL is ever exposed
- Never log full payloads containing PII into CapacityDB tables that aren't access-controlled
Reliability
- Assume duplicate deliveries. Most senders retry on non-2xx — de-duplicate on a vendor-supplied ID (
call_id,message_sid, etc.) before writing records - Keep the receiving Automation thin so it acknowledges quickly; push the real work into a Workflow
- Set Workflow Retries and Retry Time (Workflow → Settings) for steps that hit flaky downstream systems
Maintainability
- One webhook per event type, not one catch-all endpoint with a
typediscriminator - Name the webhook after the event, not the client
- Document the sending system, the sample payload, and the downstream Workflow in the app description
Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
| Sender gets a success response but nothing happens in Capacity | No Automation is bound to the webhook trigger | Create an Automation with the webhook as its trigger |
| Some payload fields are empty downstream | Field not declared in the JSON Schema | Add the field to the schema and re-test |
400 on POST | Body is not valid JSON, or Content-Type is missing | Send Content-Type: application/json and validate the body |
JSON.parse() fails in the JS Function node | Vendor sends Python str() format or single-quoted JSON | Fall back to JSON5.parse() (see Payload pre-processing) |
| Duplicate records created | Sender retried after a slow or failed response | De-duplicate on a vendor-supplied unique ID |
| Workflow fires but variables are undefined | Variables not mapped from the Automation into the Workflow inputs | Check the input mapping on the Workflow trigger step |
