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.
| Name | Sonny API |
| Header Name | Authorization |
| Header Value | Bearer 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.
| Method | POST |
| URL | https://9mil.io/api/v1/sonny/source |
| Authentication | Header Auth — Sonny API |
| Body Content Type | JSON |
| Send Headers | Idempotency-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.
{
"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.
| Method | POST |
| URL | https://9mil.io/api/v1/sonny/screen |
| Idempotency-Key | screen-{{ $json.candidate_id }}-{{ $execution.id }} |
{
"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.
- Add a Webhook trigger. Method:
POST. Response Mode: When last node finishes. - Toggle Raw body on so the Code node can hash the bytes Sonny actually sent.
- 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.
- 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.
// 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.summaryand 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_signature | Raw 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_credits | Top up at /dashboard or downgrade to a sk_test_… key while you iterate. |
| 429 rate_limited | Honour Retry-After. n8n's Retry On Fail with 2s wait covers most cases. |
| 409 idempotency_replay | You re-used an Idempotency-Key with a different body. Generate a fresh key (e.g. {{ $execution.id }}-{{ $itemIndex }}). |