# Outbound Integrations (iPaaS) (/docs/connectors/outbound-integrations)



Most of this section is about Ductor reaching *out* to providers, or providers
reaching *in* through triggers. **Outbound integrations** are the opposite
direction of a different kind: Ductor as the *event source* for an external
automation platform (iPaaS) like Zapier, n8n, or a custom endpoint. When a unit
of <Term name="Work" /> is routed or a worker goes down, Ductor pushes that event
to whatever the customer wired up on the other side.

Here the usual framing inverts: Zapier and n8n are **downstream consumers of the
clearing layer**, not competitors to it. Ductor clears the work — prices it,
routes it, executes it, settles it — and emits the lifecycle events those
platforms react to. They automate what happens *after* a decision; Ductor is what
made the decision.

<Callout type="warn" title="Not the same as inbound provider webhooks">
  This is distinct from the Ductor-owned provider webhook subscriptions in
  [Triggers & Polling](/docs/connectors/triggers-and-polling), which receive events
  *from* providers. Here Ductor is the *sender*: it signs and delivers its own
  domain events to external consumers.
</Callout>

## The iPaaS webhook adapter [#the-ipaas-webhook-adapter]

The webhook adapter (`modules/integrations/webhook/adapter.go`) bridges Ductor's
internal event system to external webhook consumers. It subscribes to a fixed set
of domain events and maps them to stable iPaaS event types:

| iPaaS event        | Mapped from domain event                                                             |
| ------------------ | ------------------------------------------------------------------------------------ |
| `lead.routed`      | `RoutableAssigned`                                                                   |
| `recipient.down`   | `RecipientStateChanged` (only when the new state is `down` / `unhealthy` / `failed`) |
| `budget.exhausted` | `RecipientCapacityChanged`                                                           |
| `quality.alert`    | quality `TierChanged` and `ProbationEnter`                                           |
| `sla.breach`       | SLA breach                                                                           |

A `Registration` (id, URL, optional secret, and an events allowlist — empty or
`*` means all) receives a standardized `WebhookPayload` (`id`, `event`,
`timestamp`, `tenant_id`, `data`). An event whose type isn't mapped is dropped
before delivery, and `recipient.down` only fires on an actual failure transition.

### Signing and retries [#signing-and-retries]

When a registration has a secret, each delivery is signed per the
[Standard Webhooks](https://www.standardwebhooks.com/) spec — `SignPayload` computes
`HMAC-SHA256` over `{msg_id}.{timestamp}.{body}`, encodes it as `v1,<base64>`, and
sets the `X-Webhook-ID`, `X-Webhook-Timestamp`, and `X-Webhook-Signature` headers.
Consumers verify with `VerifySignature` (constant-time compare). Delivery retries
with exponential backoff (`MaxRetries`, `RetryBaseDelay`), treating any non-2xx
response as a failure to retry.

```mermaid
flowchart TD
    A[Ductor domain event] --> B["TransformEvent (map + filter)"]
    B --> C[Per subscribed registration]
    C --> D["Sign (HMAC-SHA256, Standard Webhooks)"]
    D --> E[POST with retries + backoff]
```

## The n8n community node [#the-n8n-community-node]

For n8n specifically, Ductor ships a community node package
(`modules/integrations/n8n/`) with three TypeScript entry points:

* `Ductor.node.ts` — the action node (Ductor as a step in an n8n workflow).
* `DuctorTrigger.node.ts` — the trigger node (n8n workflows started by Ductor events).
* `Ductor.credentials.ts` — the `ductorApi` credential used to authenticate.

Each operation's HTTP method and path come from an explicit route table
(`routes.ts`) derived from the `google.api.http` annotations on the proto
services, so every action maps to a REST endpoint that actually exists.

### Action node [#action-node]

The action node lets an n8n builder manage Ductor routing objects and push leads
without writing HTTP calls by hand. Four resources — **Pool**, **Recipient**,
**Rule**, and **Publisher** — expose full CRUD. **Lead** is create-only, because
a lead is *routed*, not stored as a collection.

| Resource  | Operations                          | Routing notes                                                                                                                                              |
| --------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Pool      | get, getAll, create, update, delete | `update` is a `PATCH` to `/api/pools/{id}`.                                                                                                                |
| Recipient | get, getAll, create, update, delete | `create` and `getAll` are pool-scoped (`/api/pools/{pool_id}/recipients`); `get` / `update` / `delete` address a recipient by id (`/api/recipients/{id}`). |
| Rule      | get, getAll, create, update, delete | Same shape as Recipient — pool-scoped create/list, id-scoped get/update/delete.                                                                            |
| Publisher | get, getAll, create, update, delete | `update` is a `PUT` to `/api/publishers/{id}`.                                                                                                             |
| Lead      | create                              | Routes a lead through `POST /api/route` and returns the routing decision.                                                                                  |

<Callout type="warn" title="Lead is create-only now">
  Earlier versions of the node listed get/getAll/update/delete on **Lead** and
  targeted `/api/v2/{resource}s` paths that did not exist — every one of those
  operations returned `404`. The node now offers only **Create** on Lead, which
  posts the routable to `/api/route`, and routes the remaining resources through
  the verified route table above.
</Callout>

### Trigger node and signature verification [#trigger-node-and-signature-verification]

The trigger node registers a webhook subscription through the API and then
receives the iPaaS events listed [above](#the-ipaas-webhook-adapter) at the n8n
webhook URL.

When you set a **Webhook Secret** on the trigger, the node verifies every
delivery before handing it to the workflow: it recomputes the `HMAC-SHA256` over
`{id}.{timestamp}.{body}`, compares it in constant time against the
`X-Webhook-Signature` header (keyed by `X-Webhook-ID` and `X-Webhook-Timestamp`),
and rejects a mismatch with `401`. The `v1,<base64>` format and inputs mirror
`SignPayload` in the webhook adapter exactly, so a forged or tampered payload
never reaches your workflow.

<Callout type="warn" title="Set a webhook secret">
  Configuring a secret is strongly recommended. When it is left empty the trigger
  passes deliveries through **without verification** — anyone who learns the
  webhook URL could post events to it. Set a secret and only deliveries signed by
  Ductor are accepted; everything else is rejected with `401`.
</Callout>

## Where to go next [#where-to-go-next]

* [SDKs](/docs/sdks) — programmatic integration when a webhook or n8n node isn't the right fit.
* [Triggers & Polling](/docs/connectors/triggers-and-polling) — the inbound, Ductor-owned provider webhook side.
