API — Referencev1

Every endpoint. One page.

The Relay gateway is a plain REST API over JSON. Every example below is copy-paste ready — swap in your gateway URL and API key and it runs.

Quickstart

Three calls from zero to a delivered message. Set two shell variables first:

# your deployment + the API_KEY env var you configured
BASE=https://your-gateway.onrender.com
KEY=your-api-key
1
Link a number

Create a session — the response carries a QR to scan (or a pairing code).

2
Wait for “connected”

Poll the session status until WhatsApp finishes linking.

3
Send

Fire your first message through /v1/messages/text.

# 1 — create the session
curl -X POST $BASE/v1/sessions -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" -d '{"name":"main"}'

# 2 — check status (repeat until "connected")
curl $BASE/v1/sessions/main -H "Authorization: Bearer $KEY"

# 3 — send a message
curl -X POST $BASE/v1/messages/text -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"session":"main","to":"15550001111","text":"Hello from Relay 👋"}'

Prefer clicking to curling? The dashboard does all of this visually against the same API.

Authentication

Every /v1 request needs your API key as a Bearer token. The key is whatever you set as the API_KEY environment variable on your deployment — rotate it there whenever you like.

curl $BASE/v1/sessions -H "Authorization: Bearer $KEY"

Missing or wrong key → 401 unauthorized. Key not configured on the server → 503 server_not_configured.

Errors

Errors are JSON with a stable machine-readable error code and a human-readable message. Hygiene refusals add a detail object with specifics (like retryAfterSeconds).

HTTP/1.1 429
{
  "error": "rate_limited",
  "message": "Rate limit of 20 messages/minute reached for this session.",
  "detail": { "retryAfterSeconds": 41 }
}
CodeStatusMeaning
unauthorized401Missing or wrong API key.
missing_fields400A required body field is absent — the message names it.
session_not_found404No session with that name.
session_not_connected409Session exists but isn't linked/online yet.
recipient_opted_out403Recipient asked to stop. Never bypassable.
quiet_hours403Sends paused overnight. ignoreQuietHours: true for transactional messages only.
rate_limited429Per-minute limit hit — retry after detail.retryAfterSeconds.
daily_cap_reached429Warmup / daily cap hit — resumes at midnight UTC.
duplicate409Identical message sent seconds ago. allowDuplicate: true if intentional.

Sessions

A session is one linked WhatsApp number, addressed by name in every call. Credentials persist on the server's disk, so a linked number stays linked across restarts.

POST/v1/sessions
FieldType
namestring1–32 chars: letters, digits, _, -. Creating an existing session is idempotent — you get its current state back.
phoneNumberstring?Switches linking from QR to a pairing code for this number (international digits).
webhookUrlstring?Per-session webhook target (overrides the global WEBHOOK_URL).

Request

curl -X POST $BASE/v1/sessions -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" -d '{"name":"main"}'

Response — 201

{
  "name": "main",
  "status": "qr_pending",
  "qr": "data:image/png;base64,iVBORw0KG…",   // render this image, scan it
  "createdAt": "2026-08-16T09:00:00.000Z",
  "sentToday": 0
}

Render qr in an <img> and scan: phone → WhatsApp → Settings → Linked devices → Link a device. QR codes rotate about once a minute — re-fetch the session to get the current one.

Pairing code instead of QR

Pass phoneNumber and the response carries a short code instead — no camera needed. On that phone: Linked devices → Link a device → Link with phone number instead. The code is bound to that exact number and expires after a few minutes.

Request

curl -X POST $BASE/v1/sessions -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"main","phoneNumber":"15550001111"}'

Response — 201

{ "name": "main", "status": "qr_pending", "pairingCode": "PV58-622Q", … }

Status & list

GET/v1/sessions
GET/v1/sessions/:name

Response — 200

{
  "name": "main",
  "status": "connected",      // qr_pending · connecting · connected · offline · logged_out
  "warming": true,             // still inside the warmup schedule (lower daily caps)
  "warmup": {
    "ageDays": 2,               // how long since the number was linked
    "warmupDay": 3,             // 1-indexed day in the schedule (null once graduated)
    "warmupTotalDays": 7,
    "dailyCap": 200,            // today's send ceiling (null = uncapped)
    "sentToday": 12,
    "remainingToday": 188,
    "stage": "warming"          // "warming" → "ready" once past the schedule
  },
  "phoneNumber": "15550001111",
  "createdAt": "2026-08-16T09:00:00.000Z",
  "sentToday": 12
}

GET /v1/sessions returns { "sessions": [ … ] } with the same shape per entry. A dropped connection shows offline while the watchdog reconnects it — no action needed on your side. The warmup block is what the dashboard's progress bar reads: while stage is warming, keep volume low and let the caps rise; once it's ready, the number has aged past the schedule.

Logout

DELETE/v1/sessions/:name

Logs the device out of WhatsApp and wipes its stored credentials. Re-creating the session starts a fresh pairing.

Response — 200

{ "ok": true, "deleted": "main" }

Messages

All send endpoints are POST, share the same addressing fields, and return the same shape.

FieldTypeAll send endpoints
sessionstringWhich linked number sends it.
tostringRecipient — international digits (15550001111) or a full JID (…@g.us for groups).
allowDuplicatebool?Skip the duplicate guard for this send.
ignoreQuietHoursbool?Send during quiet hours — transactional messages only.
humanizebool | {delayMs}?Human-feel: mark their messages read, show "typing…", pause naturally (scales with length, max 8s), then send. Recommended for bot replies.

Response — 200 (every send endpoint)

{ "id": "3EB0538DA65A59F6D8A251", "to": "15550001111", "session": "main", "status": "sent" }

Keep the id — delivery/read receipts arrive on your webhook as message.ack events carrying it.

Text

POST/v1/messages/text
curl -X POST $BASE/v1/messages/text -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"session":"main","to":"15550001111","text":"Your order *shipped* 📦"}'

WhatsApp formatting works: *bold*, _italic_, ~strikethrough~, ```monospace```.

Image · video · document · sticker

POST/v1/messages/media

One endpoint, four types. The file comes from a public url or inline base64 — pass exactly one.

FieldType
typestringimage · video · document · sticker
urlstring?Public URL the gateway fetches the file from.
base64string?The file itself, base64-encoded (data-URL prefix allowed). Up to ~16MB decoded.
captionstring?Image, video, document.
fileNamestring?Document — the name the recipient sees.
mimetypestring?Document — e.g. application/pdf.

Request — image by URL

curl -X POST $BASE/v1/messages/media -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"session":"main","to":"15550001111","type":"image",
       "url":"https://picsum.photos/800","caption":"Random beauty"}'

Request — PDF from a local file (base64)

curl -X POST $BASE/v1/messages/media -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d "{\"session\":\"main\",\"to\":\"15550001111\",\"type\":\"document\",
       \"fileName\":\"invoice.pdf\",\"mimetype\":\"application/pdf\",
       \"base64\":\"$(base64 -w0 invoice.pdf)\"}"

Notes per type: videos deliver most reliably as MP4/H.264 · stickers must be .webp (512×512 is the sweet spot) · WhatsApp caps most media around 16MB.

Voice note

POST/v1/messages/audio

Delivered as a push-to-talk voice note (the waveform bubble) by default; pass voiceNote: false for a plain audio file. Same url/base64 choice as media. OGG/Opus is WhatsApp's native voice format; MP3 and M4A generally play fine.

FieldType
url / base64stringExactly one — the audio source.
voiceNotebool?Default true (push-to-talk bubble).
mimetypestring?Default audio/ogg; codecs=opus — set it to match your file, e.g. audio/mpeg for MP3.
curl -X POST $BASE/v1/messages/audio -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"session":"main","to":"15550001111","url":"https://cdn.example.com/note.ogg"}'

Bring your own TTS: generate audio with your provider (ElevenLabs, OpenAI, …) and pass its URL — passing tts returns 501 by design.

Buttons

POST/v1/messages/buttons

Up to three native tappable buttons — three kinds, mixable in one message. Relay sends them with the business-interactive envelope that makes WhatsApp clients render them (verified on current versions; a very old client may fall back to plain text — keep a menu or poll in flows that must reach everyone).

Button shapeRenders asWhat happens on tap
{ text, id }Reply buttonSends the label back to you as a message.received event.
{ text, url }Link buttonOpens the URL.
{ text, phone }Call buttonStarts a phone call.

Top-level fields: text (the body, required), optional header and footer, and buttons (1–3).

curl -X POST $BASE/v1/messages/buttons -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"session":"main","to":"15550001111",
       "text":"Your order is ready — what next?",
       "footer":"Acme Store",
       "buttons":[
         {"text":"Confirm pickup","id":"confirm"},
         {"text":"Track order","url":"https://acme.example/track"},
         {"text":"Call us","phone":"15550002222"}
       ]}'
Honest note: interactive rendering rides on WhatsApp's business message envelope, which isn't officially offered to unofficial clients — Relay sends the same construction hosted gateways use, and it renders on current WhatsApp versions. A very old or unusual client may still show plain text, so for flows that must reach absolutely everyone, pair buttons with a numbered menu or poll fallback.

List message

POST/v1/messages/list

The native WhatsApp “menu”: a button that opens a list of selectable rows grouped into sections. Sent with the same rendering envelope as buttons. Row selections arrive on your message.received webhook.

FieldType
textstringMessage body.
buttonTextstring?Label on the button that opens the list. Default “Menu”.
sectionsarray[{ title, rows: [{ title, id?, description? }] }]
title / footerstring?Optional header title and footer.
curl -X POST $BASE/v1/messages/list -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"session":"main","to":"15550001111","text":"How can we help?",
       "buttonText":"View menu",
       "sections":[{"title":"Support","rows":[
         {"title":"Track my order","id":"track"},
         {"title":"Speak to an agent","id":"agent"}
       ]}]}'

Numbered menu — always works

POST/v1/messages/menu

The bulletproof interactive pattern: a formatted text menu the recipient answers by replying with a number. Works on every WhatsApp version. Read their reply from the message.received webhook.

FieldType
promptstringThe question / intro line.
optionsstring[]1–20 choices — numbered automatically.
footerstring?Default: “Reply with the number of your choice.”
curl -X POST $BASE/v1/messages/menu -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"session":"main","to":"15550001111","prompt":"How can we help?",
       "options":["Track my order","Speak to support","Something else"]}'

# the recipient sees:
#   How can we help?
#
#   1. Track my order
#   2. Speak to support
#   3. Something else
#
#   Reply with the number of your choice.

Poll

POST/v1/messages/poll

A native poll that works on every WhatsApp version — the most universally compatible tappable interaction. Votes arrive on your webhook as poll.vote events.

FieldType
questionstringThe poll title.
optionsstring[]2–12 choices.
selectableCountnumber?How many options one person can pick. Default 1.
curl -X POST $BASE/v1/messages/poll -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"session":"main","to":"15550001111",
       "question":"Confirm your appointment?",
       "options":["✅ Confirm","📅 Reschedule","❌ Cancel"]}'

Location

POST/v1/messages/location

Drops a map pin. latitude/longitude required; name and address label the pin.

curl -X POST $BASE/v1/messages/location -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"session":"main","to":"15550001111","latitude":51.5033,"longitude":-0.1196,
       "name":"London Eye","address":"Riverside Building"}'

Contact card

POST/v1/messages/contact

Sends a tappable contact card. contactName and contactNumber required, organization optional.

curl -X POST $BASE/v1/messages/contact -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"session":"main","to":"15550001111",
       "contactName":"Relay Support","contactNumber":"15550002222"}'

Number check

GET/v1/check/:number?session=main

Is this number on WhatsApp? Check before you send instead of burning failed messages. ?session= is optional when exactly one session exists.

curl "$BASE/v1/check/15550001111?session=main" -H "Authorization: Bearer $KEY"

{ "number": "15550001111", "onWhatsApp": true, "jid": "15550001111@s.whatsapp.net" }

Opt-out list

Anyone who replies STOP (or UNSUBSCRIBE — configurable via OPTOUT_KEYWORDS) is added automatically, and the gateway refuses further sends to them with 403 recipient_opted_out. No override exists — by design. Remove a number only when that person has asked to hear from you again.

GET/v1/optout
{ "optout": ["15550001111", "15550002222"] }
POST/v1/optout
curl -X POST $BASE/v1/optout -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" -d '{"number":"15550001111"}'
DELETE/v1/optout/:number
{ "ok": true, "removed": true }

Campaigns — paced drip sends

A campaign delivers one message to a list of recipients, spread out over time with human-like jitter instead of a blast. Every send still runs through the full hygiene pipeline — rate limits, warmup caps, quiet hours, opt-out and duplicate guard all apply — the scheduler only decides when each one goes out. Campaigns survive restarts and resume automatically.

Opted-in recipients only. This is not a warm-up bot and not a cold-broadcast tool — it delivers real messages you wrote to people who agreed to hear from you. You must pass confirmOptIn: true, and opted-out numbers are skipped automatically. Blasting strangers is the fastest way to get a number banned.
POST/v1/campaigns
FieldType
sessionstringWhich linked number sends the campaign.
recipientsstring[]Phone numbers (international digits) or full jids. De-duplicated automatically.
labelstring?Instead of recipients: target everyone carrying this WhatsApp Business label — e.g. "1 Month" for renewal reminders to that tier.
textstringThe message body sent to everyone.
perHournumber?Pacing — messages per hour, 1–120. Default 12 (one every ~5 min, jittered ±40%).
startAtstring?ISO timestamp to begin. Default: now.
confirmOptInboolRequired. Must be true — affirms recipients opted in.
curl -X POST $BASE/v1/campaigns -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"session":"main","perHour":15,"confirmOptIn":true,
       "recipients":["15550001111","15550002222"],
       "text":"Your appointment is tomorrow at 10am. Reply STOP to opt out."}'

// → 201
{ "id": "camp_1a2b3c", "session": "main", "total": 2, "perHour": 15,
  "status": "running", "counts": { "pending": 2, "sent": 0, "failed": 0, "skipped": 0 },
  "nextSendAt": "2026-08-16T09:00:00.000Z" }
GET/v1/campaigns

Lists all campaigns with live progress counts. GET /v1/campaigns/:id adds a recipients[] array with each number's status (pending · sent · skipped · failed), scheduled time, and message id.

DELETE/v1/campaigns/:id

Cancels all remaining sends (already-sent messages are unaffected) and returns how many were cancelled.

Inbound media

When someone sends your number a photo, voice note, or document, the message.received webhook includes a media block with a downloadUrl. Fetch it (with your Bearer key) to get the raw bytes.

// webhook payload for an inbound image
{ "event": "message.received", "data": {
    "id": "3EB0…", "from": "15550001111", "type": "imageMessage",
    "hasMedia": true,
    "media": { "kind": "image", "mimetype": "image/jpeg", "downloadUrl": "/v1/media/main/3EB0…" } } }

curl $BASE/v1/media/main/3EB0… -H "Authorization: Bearer $KEY" --output photo.jpg

React · edit · delete · read · reply

Every send accepts quotedId to reply to a specific message. Message actions reference a message by id:

EndpointBody
POST /v1/messages/reaction{ session, to, targetMessageId, emoji }React (empty emoji removes it).
POST /v1/messages/edit{ session, to, targetMessageId, text }Edit a message you sent.
POST /v1/messages/delete{ session, to, targetMessageId, forEveryone? }Delete/revoke.
POST /v1/messages/read{ session, to }Mark a chat's inbound messages read.
GET /v1/messages/:session/:id/statusDelivery state of a sent message: pending → sent → delivered → read (or error), with statusAt.
POST /v1/presence{ session, to, state }composing = typing indicator; also recording, paused, available.

Inbound reactions arrive as a message.reaction webhook (emoji, targetMessageId).

Chats & history

The gateway keeps a rolling history window per session (last ~300 messages per chat) so you can build an inbox.

curl $BASE/v1/chats/main -H "Authorization: Bearer $KEY"              # recent chats
curl $BASE/v1/chats/main/15550001111 -H "Authorization: Bearer $KEY"  # conversation (?limit, ?before)
curl $BASE/v1/chats/main/15550001111/picture -H "Authorization: Bearer $KEY"  # profile picture URL

Labels WhatsApp Business

Tag chats with the labels you created in the WhatsApp Business app ("Trial", "1 month", "VIP"…) — the label shows up on the chat in WhatsApp itself, on every device. Perfect for automations: when your bot grants a trial, label the chat. Labels are created in the app (Business tools → Labels); the API lists and applies them. Chats returned by /v1/chats/:session include their labels.

# what labels exist on this number (synced from the phone)
curl $BASE/v1/labels/main -H "Authorization: Bearer $KEY"
# → { "labels": [ { "id": "6", "name": "Trial", "color": 3 }, … ] }

# tag a client — label by name (or id); action defaults to "add"
curl -X POST $BASE/v1/labels/main/chat -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"15550001111","label":"Trial"}'

# everyone carrying a label — e.g. all Trial clients (for follow-ups, campaigns…)
curl "$BASE/v1/labels/main/chats?label=Trial" -H "Authorization: Bearer $KEY"
# → { "label": {…}, "count": 12, "chats": [ { "number": "…", "name": "…", … } ] }

# remove it (e.g. when the trial converts to a subscription)
curl -X POST $BASE/v1/labels/main/chat -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"15550001111","label":"Trial","action":"remove"}'

Numbers linked before label support are backfilled automatically on their next reconnect; POST /v1/labels/:session/sync forces a full re-sync any time.

Label changes (from the phone or the API) also fire chat.labeled / chat.unlabeled webhooks with chat, labelId and labelName — so a flow can react when you hand-label someone on your phone. Works on WhatsApp Business accounts; on a regular WhatsApp account there are no labels and the list is empty.

Groups

Endpoint
GET /v1/groups/:sessionList groups this number is in.
POST /v1/groups/:session{ subject, participants[] } — create.
GET /v1/groups/:session/:groupIdMetadata + participant list.
POST /v1/groups/:session/:groupId/participants{ action: add\|remove\|promote\|demote, participants[] }
GET /v1/groups/:session/:groupId/inviteInvite code + chat.whatsapp.com link.
POST /v1/groups/:session/join{ code } — join by invite.
DELETE /v1/groups/:session/:groupIdLeave.

Profile & status

# update the linked number's own profile
curl -X POST $BASE/v1/profile/main -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Acme Support","status":"Here to help 9–5"}'

# post a text status/story (recipients who can see it)
curl -X POST $BASE/v1/status/main -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{"text":"We are open!","backgroundColor":"#2947FF","recipients":["15550001111"]}'

Webhooks

Point Relay at your server and it POSTs JSON events as they happen. Configure globally with the WEBHOOK_URL env var, or per session with webhookUrl at creation. Failed deliveries retry 3 times (5s → 30s → 2m). Timeouts are 10s — respond fast, process async.

EventWhendata payload
message.receivedInbound messageid, from, fromJid, pushName, text, type, timestamp
message.sentAny outbound messageid, to, text, type, source: api (this gateway) or phone (you typed it) — use phone to pause a bot when a human takes over
message.ackStatus change on a sent messageid, to, status: sent → delivered → read (played for voice)
poll.voteSomeone votes on your pollpollId, question, votes[] with per-option voter lists
message.failedA sent message errored (ack status 0)id, to — also fires an alert
chat.labeled / chat.unlabeledLabel added to / removed from a chat (phone or API)chat, chatJid, labelId, labelName
session.connectedNumber linked / reconnectedphoneNumber
session.disconnectedConnection dropped or removedreason: connection_lost · logged_out · deleted

Example delivery — message.received

POST your-webhook-url
Content-Type: application/json
X-Relay-Signature: sha256=7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069

{
  "event": "message.received",
  "session": "main",
  "timestamp": "2026-08-16T09:41:00.000Z",
  "data": {
    "id": "3EB0538DA65A59F6D8A251",
    "from": "15550001111",
    "fromJid": "15550001111@s.whatsapp.net",
    "pushName": "Sara",
    "text": "hey! is this still available?",
    "type": "conversation",
    "timestamp": 1786786860
  }
}

Example delivery — poll.vote

{
  "event": "poll.vote", "session": "main", "timestamp": "…",
  "data": {
    "pollId": "3EB0…",
    "question": "Confirm your appointment?",
    "votes": [
      { "option": "✅ Confirm", "voters": ["15550001111"] },
      { "option": "📅 Reschedule", "voters": [] }
    ]
  }
}

Verifying signatures

Set WEBHOOK_SECRET on your deployment and every delivery is signed: the X-Relay-Signature header carries sha256= + HMAC-SHA256 of the raw body. Verify before trusting the payload:

// Node / Express — capture the RAW body, then compare digests
import crypto from "node:crypto";

app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const expected = "sha256=" + crypto
    .createHmac("sha256", process.env.WEBHOOK_SECRET)
    .update(req.body)
    .digest("hex");
  const given = req.get("x-relay-signature") || "";
  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given)))
    return res.sendStatus(401);

  const event = JSON.parse(req.body);
  res.sendStatus(200);              // ack fast, process async
});

Sending hygiene

On by default and tuned by environment variables on your deployment. These exist to protect the people you message — and, as a side effect, your numbers.

VariableDefault
RATE_PER_MINUTE20Sliding-window per-session send limit. Exceeding → 429 with retryAfterSeconds.
WARMUP_SCHEDULE50,100,200,400,800,1500,3000Daily caps for a session's first week — day 1 gets 50, day 2 gets 100, and so on. Sessions report warming: true during this.
DAILY_CAP0Cap after warmup. 0 = uncapped.
QUIET_HOURSoffe.g. 22-08 — no sends overnight. QUIET_TZ_OFFSET shifts the window from UTC.
DUPLICATE_WINDOW_SECONDS120Identical send to the same recipient inside this window → 409 duplicate.
OPTOUT_KEYWORDSstop, unsubscribe, optout, opt outInbound words (exact, case-insensitive) that opt the sender out.
Straight talk: Relay uses WhatsApp's web protocol, which is against WhatsApp's terms of service. Good hygiene lowers the chance of a number being banned — it does not remove it. Don't put a number you can't afford to lose behind any unofficial gateway, ours included.

Events (activity)

Every webhook event is also kept in a rolling in-gateway log, so you can see recent activity without standing up a webhook consumer.

curl "$BASE/v1/events?limit=100&event=message.received" -H "Authorization: Bearer $KEY"
curl "$BASE/v1/ops" -H "Authorization: Bearer $KEY"   # one-glance ops: sessions, sent/failed today, queues, alerts

Alerts. Session trouble (logged out, repeated reconnects, recovery, failed sends) fires the alert channel. Two options, combinable: ALERT_WEBHOOK_URL (Slack/Discord/any webhook), and — the one you'll actually see — WhatsApp itself: set ALERT_SESSION (a second linked number to send from) and ALERT_NUMBER (your personal phone). Never use the business number as its own alert sender — when it drops it can't tell you.

Machine-readable API spec: /openapi.yaml (OpenAPI 3.0 — import into Postman/Insomnia). n8n starter workflows are in the repo's integrations/ folder.