SendSetsDocs
Endpoint reference

App connections

The app your campaigns call: base URL, signing secret, and the signed app.action and app.ping requests.

An app connection is how a campaign reaches into your product. It is a name, an HTTPS base URL and a signing secret. An app_action step POSTs to a path under the base URL, signed with the secret, and the JSON object your app answers becomes .App.<output_key> for every later step in that contact's sequence. Product events flow the other way: your app calls POST /events (see product events).

Every route needs the Integrations scope and, for a session, the manage_settings permission. The secret is returned once, on create and on rotate, and never again.

List app connections

GET /app-connections

Auth: Scope INTEGRATIONS · Org permission manage_settings

{
  "data": [
    {
      "id": "108178ff-8dee-4279-87f2-2df2a485ceb5",
      "organization_id": "f9e8d7c6-...",
      "name": "app",
      "base_url": "https://app.example.com/sendsets",
      "created_at": "2026-09-01T10:00:00Z",
      "updated_at": "2026-09-01T10:00:00Z"
    }
  ]
}

Create an app connection

POST /app-connections

Auth: Scope INTEGRATIONS · Org permission manage_settings

FieldTypeRequiredDescription
namestringyesUnique within the workspace, up to 100 characters.
base_urlstringyesHTTPS. Query and fragment are dropped; a URL with credentials is refused.

Response: 201 Created

{
  "connection": { "id": "108178ff-...", "name": "app", "base_url": "https://app.example.com/sendsets", "created_at": "...", "updated_at": "..." },
  "secret": "ss_app_wnR54NSd8vKbMcDRztk7yiiZLv-abjmve7Lhca4P_vo"
}

Store the secret: it is what your app verifies requests with.

Get, update, delete

GET /app-connections/:id, PATCH /app-connections/:id (name, base_url), DELETE /app-connections/:id.

Auth: Scope INTEGRATIONS · Org permission manage_settings

A connection an app_action step still references cannot be deleted: 409 app_connection_in_use.

Rotate the secret

POST /app-connections/:id/rotate-secret

Replaces the signing secret and returns the new one once, as {"secret": "..."}. Requests signed with the old secret stop verifying immediately.

Test an app connection

POST /app-connections/:id/test

Sends a signed app.ping to the base URL and reports each stage of reaching the app. The answer is always 200: a failing app is a result, not a transport error.

Auth: Scope INTEGRATIONS · Org permission manage_settings

{
  "dns": true,
  "tls": true,
  "connection": true,
  "authentication": true,
  "status": 200,
  "latency_ms": 184
}

authentication is true when the app answered any 2xx to the signed ping, which is the app saying it verified X-SendSets-Signature. Anything else fills error:

{
  "dns": true,
  "tls": true,
  "connection": true,
  "authentication": false,
  "status": 401,
  "latency_ms": 90,
  "error": {
    "code": "app_connection_failed",
    "message": "the app answered HTTP 401 to a signed app.ping",
    "fix": "Check the app is reachable at https://app.example.com/sendsets and verifies X-SendSets-Signature with the connection secret."
  }
}

What your app receives

Both the ping and a live app_action are a POST with these headers:

HeaderValue
Content-Typeapplication/json
X-SendSets-Eventapp.ping or app.action
X-SendSets-Event-IdThe step run id, stable across retries. Dedupe on it.
Idempotency-KeyThe same id.
X-SendSets-Signaturet=<unix>,v1=<hex>: HMAC-SHA256 of <unix>.<raw body> with the connection secret, the same scheme as webhooks.

An app.ping body is {"id": "ping:<uuid>", "run_mode": "test", "ping": true, "requested_at": "..."}; answer any 2xx.

An app.action body carries the campaign, the step, the contact (standard fields plus custom fields), the .App values captured so far, and the step's rendered inputs:

{
  "id": "<campaign>:<contact>:<step>",
  "run_id": "3f9c...",
  "run_mode": "live",
  "campaign": { "id": "...", "name": "Q3 founders" },
  "step": { "id": "...", "name": "Audit" },
  "contact": { "id": "...", "email": "jane@acme.com", "first_name": "Jane", "company": "Acme" },
  "app": { },
  "inputs": { "domain": "acme.com" },
  "requested_at": "2026-09-01T10:00:00Z"
}

Answer a 2xx with a JSON object of at most 64 KiB within 30 seconds. That object is stored as .App.<output_key>, so {"score": 67, "report_url": "https://..."} on a step with output_key: audit renders as {{.App.audit.score}} in a later email. A non-2xx, a non-object or a timeout ends the step with outcome error (app_action_failed), which a node_outcome branch can route; unrouted, the run is blocked and POST /campaigns/:id/step-runs/:runId/retry runs it again.

On this page