← Sonny

Workflow automation cookbook.

Wire Sonny into your workflow automation tool in under five minutes. Outbound HTTP Request calls, signed-webhook receivers, and idempotency on retries — all paste-ready. Examples target n8n but the patterns apply to Zapier, Make.com, and Pipedream.

Need the underlying API reference? /sonny/docs · /sonny/sdk.

1 · Set up credentials once

In n8n, create a Header Auth credential with these fields. Reuse it on every Sonny HTTP Request node.

NameSonny API
Header NameAuthorization
Header ValueBearer sk_live_…

Use a sk_test_… key while you build — same gateway, zero credit charges, errors come back the same shape.

2 · Source candidates from a role brief

Drop an HTTP Request node into your workflow and point it at /api/v1/sonny/source. Set authentication to the credential you just created.

MethodPOST
URLhttps://9mil.io/api/v1/sonny/source
AuthenticationHeader Auth — Sonny API
Body Content TypeJSON
Send HeadersIdempotency-Key: {{ $execution.id }}

$execution.id is unique per run, so a manual re-execute reuses the same key and Sonny replays the cached response instead of double-charging credits.

JSON body
{
  "role_brief": "Senior backend engineer, 5+ yrs, Go/Postgres, remote EU",
  "limit": 25,
  "filters": {
    "min_years_experience": 5,
    "must_have_skills": ["Go", "Postgres"]
  }
}

Response is the same shape as /sonny/docs shows. Reference fields downstream as {{ $json.candidates[0].name }}.

3 · Deep-screen one candidate

For each candidate from step 2, call /screen. Wrap the call in n8n’s built-in Loop Over Itemsso the HTTP Request node runs once per item.

MethodPOST
URLhttps://9mil.io/api/v1/sonny/screen
Idempotency-Keyscreen-{{ $json.candidate_id }}-{{ $execution.id }}
JSON body
{
  "candidate_id": "{{ $json.candidate_id }}",
  "role_brief": "Senior backend engineer, 5+ yrs, Go/Postgres, remote EU"
}

Sonny returns a 429 with a Retry-Afterheader if you burst too fast. Enable n8n’s Retry On Fail on the HTTP Request node and set Wait Between Tries to 2000ms.

4 · Receive Sonny webhooks (signed)

Use the Webhook node as the trigger and a Code node to verify the signature. Sonny signs every delivery with HMAC-SHA256 over {ts}.{rawBody} and sends the result in the x-sonny-signature header.

  1. Add a Webhook trigger. Method: POST. Response Mode: When last node finishes.
  2. Toggle Raw body on so the Code node can hash the bytes Sonny actually sent.
  3. Drop in the Code node below as the next step. It throws on bad signatures, which n8n surfaces as a 500 — Sonny will retry with backoff.
  4. Register the webhook URL once via POST /api/v1/sonny/webhooks. Sonny returns the signing secret in the response only once — store it as an n8n credential or env var.
n8n Code node — verify signature
// Drop into a Code node (mode: Run Once for All Items).
// Reads raw body + x-sonny-signature, verifies, then forwards
// the parsed event downstream. Throws on bad signatures so n8n
// returns 500 and Sonny retries with backoff.
const crypto = require('crypto');

const secret = $env.SONNY_WEBHOOK_SECRET; // or use credentials
const headers = $input.first().json.headers || {};
const rawBody = $input.first().json.body; // requires 'Raw body' on the Webhook node
const sigHeader = headers['x-sonny-signature'] || '';

const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(sigHeader);
if (!m) throw new Error('malformed signature');
const [, ts, v1] = m;

const drift = Math.abs(Math.floor(Date.now() / 1000) - Number(ts));
if (drift > 300) throw new Error(`timestamp drift ${drift}s`);

const expected = crypto
  .createHmac('sha256', secret)
  .update(`${ts}.${rawBody}`)
  .digest('hex');

if (
  v1.length !== expected.length ||
  !crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
) {
  throw new Error('signature mismatch');
}

// All good — forward the parsed event.
const event = JSON.parse(rawBody);
return [{ json: event }];

Once verified, branch on {{ $json.event }} using an IF or Switch node. Events are documented at /sonny/docs/reference#webhooks (sign-in required).

5 · Test your endpoint without waiting

After registering a subscription, fire a synthetic delivery at it so you can see the verification logic light up before any real event arrives:

curl https://9mil.io/api/v1/sonny/webhooks/<id>/test \
  -H "Authorization: Bearer sk_live_…" \
  -H "content-type: application/json" \
  -X POST \
  -d '{"event": "sonny.webhook.test", "payload": {"hello": "from n8n"}}'

The response includes the receiver’s status code and a truncated body, so you can debug a 401 from the verify node without spelunking through n8n executions.

6 · Common patterns

  • Source → Screen → Slack: chain Source, a Loop Over Items, Screen, IF (fit_score >= 80), then a Slack node. Top of funnel filtered before any human looks at it.
  • ATS reply mirror: on sonny.message.replied, take $json.data.thread.summary and patch your ATS candidate notes via its REST API.
  • Compliance reminder: on sonny.placement.reported, schedule a Wait node for 90 days, then trigger a check-in email through your existing outreach workflow.

Troubleshooting

401 invalid_signatureRaw body is off, or you copied the secret with whitespace. Toggle 'Raw body' on the Webhook node and re-paste the secret with no leading/trailing chars.
402 insufficient_creditsTop up at /dashboard or downgrade to a sk_test_… key while you iterate.
429 rate_limitedHonour Retry-After. n8n's Retry On Fail with 2s wait covers most cases.
409 idempotency_replayYou re-used an Idempotency-Key with a different body. Generate a fresh key (e.g. {{ $execution.id }}-{{ $itemIndex }}).