# Pipedream

**Wiring agent identity into a no-code workflow usually means a generic HTTP node, a hand-built JSON body, and a prayer that you got the escaping right.**

One unescaped quote in an agent label and your Cypher call breaks; one leaked bearer in a step export and it's in your run history forever. Whisper's Pipedream app removes all of that: a real registered app with typed props, a Cypher-literal builder that can't be broken by user input, and a response decoder that turns the control-plane envelope into a plain array of records — 12 actions, split into a keyless tier anyone can drag onto a canvas and a keyed control tier that provisions and governs your fleet.

## Two tiers, one app

The `whisper` app follows the same Postel's-Law shape as every Whisper integration ([see the integration standard](/docs/integrations)): the credential is what unlocks the second tier, not a separate product.

- **Tier 1 — keyless.** Reads Whisper's public, anonymous identity API at `rdap.whisper.online`. No connected account. Same data anyone on the internet can query.
- **Tier 2 — control plane.** Connect a Whisper account (one `whisper_live_…` key) and the fleet-management actions unlock: register agents, allocate identities, read logs, set policy, wire up egress, revoke.

The app registers **one** optional auth field, `api_key`. Leave it blank and the four keyless actions still work; every keyed action checks for it up front and throws a `ConfigurationError` — a clear message, not a bare 401 three steps into a workflow — if it's missing.

## The 12 actions

| # | Action | Tier | Op / endpoint |
|---|---|---|---|
| 1 | Verify Agent Identity | keyless | `GET /verify-identity?ip=` |
| 2 | Lookup RDAP Record | keyless | `GET /ip/<addr>` |
| 3 | Get Transparency Log | keyless | `GET /ip/<addr>/transparency` |
| 4 | Get Inbound Lookups | keyless | `GET /ip/<addr>/lookups` |
| 5 | Register Agent | control | `op:register` |
| 6 | Allocate Identity | control | `op:identity` |
| 7 | List Agents | control | `op:list` |
| 8 | Get Agent | control | `op:agent` |
| 9 | Set Policy | control | `op:policy` |
| 10 | Get Logs | control | `op:logs` |
| 11 | Connect Egress | control | `op:connect` |
| 12 | Revoke Agent | control | `op:revoke` |

All eight keyed actions carry the same `admin:dns` (write) or read scope that a matching `whisper` CLI subcommand carries, and every one is confined to the caller's own tenant — there is no cross-tenant argument to smuggle through `args`.

## Under the hood: one Cypher verb

Every control action is a thin wrapper over a single call: `CALL whisper.agents({op:'<op>', args:{…}})`, POSTed as `{"query": "..."}` to `https://graph.whisper.security/api/query` with the caller's key in `X-API-Key` — never in the body. This is the exact contract documented in the [Graph API reference](/docs/graph-api) and implemented once, correctly, in the `whisper` CLI's reference client.

**With stock tools** — build and send the same call by hand with `curl` and `jq`:

```bash
curl -s https://graph.whisper.security/api/query \
  -H "X-API-Key: whisper_live_…" \
  -H 'content-type: application/json' \
  -d '{"query":"CALL whisper.agents({op:'"'"'register'"'"', args:{label:'"'"'scout'"'"'}})"}' \
  | jq '.rows[0].result'
```

Get the string-escaping wrong — an unescaped `'` in a label like `O'Brien's bot` — and that quote closes the Cypher literal early and the query fails, or worse, is misparsed. Doing this safely by hand means writing your own doubled-quote escaper and a two-shape response decoder (the control endpoint can return either a procedure-row table or a flat `{ok,status,result,error}` envelope — see the reference for both shapes).

**With Whisper** — the app does the escaping and decoding for you; the action just takes typed props:

```text
Register Agent
  Name:            scout
  Contact Email:   (optional)
→ $summary: "Registered agent scout at 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4"
→ returns: { agent, address, fqdn, ptr, state, api_key }
```

That `api_key` field is the new agent's own credential and is returned **once** — capture it in the same run (write it to a connected secrets step, not to a public output).

The app's Cypher builder mirrors the CLI's exactly: a string value is single-quoted with `'` and `\` each doubled (`Tim O'Reilly` → `'Tim O''Reilly'`), map keys are emitted in **sorted order** so the wire query is byte-stable, and the response decoder accepts both envelope shapes plus a bare problem object — surfacing `detail` (or `title`, or `type`) verbatim on failure rather than an opaque 500. That's the Robustness Principle (RFC 761) applied to a no-code canvas: conservative in what the action emits on the wire, liberal in what it accepts back.

## A worked example: gate a Slack alert on agent identity

Say a workflow ingests inbound webhook traffic and you want to flag anything not coming from a genuine Whisper agent before it reaches a downstream step.

**With stock tools** (a Pipedream Node code step, no Whisper software):

```javascript
import { axios } from "@pipedream/platform";

export default defineComponent({
  async run({ steps, $ }) {
    const ip = steps.trigger.event.headers["x-forwarded-for"];
    const r = await axios($, {
      url: "https://rdap.whisper.online/verify-identity",
      params: { ip },
      headers: { Accept: "application/json" },
      validateStatus: () => true,
    });
    if (!r.is_whisper_agent) throw new Error(`${ip} is not a verified Whisper agent`);
    return r;
  },
});
```

**With Whisper** — drop in the **Verify Agent Identity** action (no code, no auth needed), then branch on its `is_whisper_agent` export directly in the workflow's conditional path — one action, typed output, no hand-rolled HTTP client.

## Connect Egress — the one action with secrets in it

`op:connect` is the odd one out: its result carries live transport secrets (an `et_…` bearer embedded in `http_proxy`/`connection_string` for the `socks5`/`anyip` tiers, or a `client_private_key` for a zero-key WireGuard setup). Because a hosted Pipedream step's output can end up in run history and downstream step data, the action follows the same bearer-hygiene rule as every other Whisper integration: don't wire the raw secret fields into a Slack message, a database row, or anything outside the same run's next HTTP step. If your workflow just needs a routed egress endpoint to hand to an HTTP-request node's proxy setting, that's exactly what this action is for — use it as a proxy config, not a value to persist.

## Installing it

The app is contributed as source in [`PipedreamHQ/pipedream`](https://github.com/PipedreamHQ/pipedream) under `components/whisper/`, the same self-serve model as the Whisper `n8n`, [`Make`](/docs/make), and [`Power Platform`](/docs/powerplatform) connectors — no vendor account required to review the code, just Pipedream's own app-integration step before a submitted app goes live in the workspace picker. Once live, search "Whisper" when adding an app to a workflow; the keyless actions need nothing further, and the control actions prompt you to connect an account with your `whisper_live_…` key.

> The key-gated DNS resolver (`/dns-query`) is intentionally not wrapped here — it's a stateful per-tenant `:53`/DoH endpoint, not a request/response action. Point your own resolver config at it directly; see [DoH](/docs/dns-over-https).

## Next

[Graph API — the `whisper.agents` control verb](/docs/graph-api) · [Integrations catalog](/docs/integrations)
