Webhooks

Webhooks let Capacity exchange event data with external systems in real time, without polling.

Capacity supports two directions:

DirectionNameWhat it does
InboundIncoming WebhooksAn external system POSTs JSON to a Capacity-generated URL. The payload is validated against a JSON Schema and can trigger an Automation or Workflow.
OutboundWebhook 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

  1. Open the App that should own the webhook (or create a new App)
  2. Go to the Incoming Webhooks tab
  3. Click Add New Webhook
  4. 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")
  5. 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 description values — 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 own properties block
  • 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

HeaderValue
x-capacity-idOrg ID making the request
AuthorizationBearer <api-key>
Content-Typeapplication/json

Request body

{
  "service": "my_external_service",
  "trigger_event_type": "order_shipped",
  "attributes": {
    "order_id": "SO-10432",
    "carrier": "UPS"
  }
}
FieldRequiredDescription
serviceYesThe service triggering the webhook
trigger_event_typeYesThe event value the webhook trigger listens for
attributesYesKey/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 type discriminator
  • 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

SymptomLikely CauseFix
Sender gets a success response but nothing happens in CapacityNo Automation is bound to the webhook triggerCreate an Automation with the webhook as its trigger
Some payload fields are empty downstreamField not declared in the JSON SchemaAdd the field to the schema and re-test
400 on POSTBody is not valid JSON, or Content-Type is missingSend Content-Type: application/json and validate the body
JSON.parse() fails in the JS Function nodeVendor sends Python str() format or single-quoted JSONFall back to JSON5.parse() (see Payload pre-processing)
Duplicate records createdSender retried after a slow or failed responseDe-duplicate on a vendor-supplied unique ID
Workflow fires but variables are undefinedVariables not mapped from the Automation into the Workflow inputsCheck the input mapping on the Workflow trigger step



Was this article helpful?