waiaconnect

Documentation / Complete

Webhooks with the API (and the one born with the onboarding)

POST /v1/webhooks

Markdown

The same webhooks as the panel, managed with your API key at /v1/webhooks: create (POST /v1/webhooks), list (GET /v1/webhooks), get one (GET /v1/webhooks/:id), change it (PATCH /v1/webhooks/:id), delete it (DELETE /v1/webhooks/:id), test it (POST /v1/webhooks/:id/test, a signed event sent right away) and rotate its secret (POST /v1/webhooks/:id/rotate-secret). A webhook without connectionId is account-wide; with it, it receives only that number's events and replaces the account webhook for that number. The number is given as its conn_… or your externalId, and that is how it comes back in every response (with its externalId next to it) — never an internal id. The signing secret (whsec_…) and the header token, if you asked for one with headerToken: true, are shown once.

The webhook born with the onboarding. If you integrate with onboarding links, send webhook: { url, events } in POST /v1/onboarding-links: we validate it when the link is created and return its secret right then. When your customer finishes, the number is born with its webhook, signed with THAT secret, and before connection.created — that event already reaches it. Find it later with GET /v1/webhooks?externalId=….

# 1) onboarding link + the webhook of the number it will connect
curl -X POST https://api.waiaconnect.com/v1/onboarding-links \
  -H "Authorization: Bearer wc_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"externalId": "pharmacy-42", "webhook": {"url": "https://api.yourcompany.com/hooks/connect", "events": ["connection.created", "message.received", "message.status"]}}'
# → { "url": "https://…/c/<token>", "publicId": "onb_…",
#     "webhook": { "secret": "whsec_…", "createdOnCompletion": true, … } }   ← store the secret NOW

# 2) after your customer connects: the number's webhooks, by your externalId
curl "https://api.waiaconnect.com/v1/webhooks?externalId=pharmacy-42" -H "Authorization: Bearer wc_live_YOUR_API_KEY"
# → { "webhooks": [ { "id": "wh_…", "connectionId": "conn_…", "externalId": "pharmacy-42", … } ] }

# 3) create one directly (connectionId = conn_… or your externalId; omit = account-wide)
curl -X POST https://api.waiaconnect.com/v1/webhooks \
  -H "Authorization: Bearer wc_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://api.yourcompany.com/hooks/connect-2", "connectionId": "pharmacy-42"}'

# rotate the secret (the old one stops verifying at once)
curl -X POST https://api.waiaconnect.com/v1/webhooks/wh_…/rotate-secret -H "Authorization: Bearer wc_live_YOUR_API_KEY"
const H = { "Authorization": "Bearer wc_live_YOUR_API_KEY", "Content-Type": "application/json" };

// 1) onboarding link + the webhook of the number it will connect
const link = await (await fetch("https://api.waiaconnect.com/v1/onboarding-links", {
  method: "POST", headers: H,
  body: JSON.stringify({
    externalId: "pharmacy-42",
    webhook: { url: "https://api.yourcompany.com/hooks/connect", events: ["connection.created", "message.received", "message.status"] }
  })
})).json();
await saveSecret("pharmacy-42", link.webhook.secret); // shown ONCE — it signs this number's events

// 2) later: the number's webhooks, by your externalId
const { webhooks } = await (await fetch("https://api.waiaconnect.com/v1/webhooks?externalId=pharmacy-42", { headers: H })).json();

// rotate the secret (the old one stops verifying at once)
const rotated = await (await fetch(`https://api.waiaconnect.com/v1/webhooks/${webhooks[0].id}/rotate-secret`, { method: "POST", headers: H })).json();
await saveSecret("pharmacy-42", rotated.secret);
<?php
function connect($method, $url, $body = null) {
  $ch = curl_init($url);
  curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => $method,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer wc_live_YOUR_API_KEY", "Content-Type: application/json"],
    CURLOPT_POSTFIELDS => $body ? json_encode($body) : null,
  ]);
  return json_decode(curl_exec($ch), true);
}
// 1) onboarding link + the webhook of the number it will connect
$link = connect("POST", "https://api.waiaconnect.com/v1/onboarding-links", [
  "externalId" => "pharmacy-42",
  "webhook" => ["url" => "https://api.yourcompany.com/hooks/connect", "events" => ["connection.created", "message.received", "message.status"]],
]);
save_secret("pharmacy-42", $link["webhook"]["secret"]); // shown ONCE

// 2) later: the number's webhooks, by your externalId
$list = connect("GET", "https://api.waiaconnect.com/v1/webhooks?externalId=pharmacy-42");
// rotate the secret (the old one stops verifying at once)
$rotated = connect("POST", "https://api.waiaconnect.com/v1/webhooks/" . $list["webhooks"][0]["id"] . "/rotate-secret");
import requests
H = {"Authorization": "Bearer wc_live_YOUR_API_KEY"}

# 1) onboarding link + the webhook of the number it will connect
link = requests.post("https://api.waiaconnect.com/v1/onboarding-links", headers=H, json={
    "externalId": "pharmacy-42",
    "webhook": {"url": "https://api.yourcompany.com/hooks/connect",
                "events": ["connection.created", "message.received", "message.status"]},
}).json()
save_secret("pharmacy-42", link["webhook"]["secret"])  # shown ONCE

# 2) later: the number's webhooks, by your externalId
hooks = requests.get("https://api.waiaconnect.com/v1/webhooks", headers=H, params={"externalId": "pharmacy-42"}).json()["webhooks"]

# rotate the secret (the old one stops verifying at once)
rotated = requests.post(f"https://api.waiaconnect.com/v1/webhooks/{hooks[0]['id']}/rotate-secret", headers=H).json()

Limit: 2 webhooks per number (one live and one to migrate without a gap), 2 × your numbers for all number webhooks together, and 5 account-wide; disabled ones count too. Going over it when creating or moving one to a number → 422 WEBHOOK_LIMIT_REACHED with details.scope, used and limit. Webhooks you already had above it keep working: you can edit, test, rotate and delete them.

List filters: ?connectionId=conn_… (or your externalId), ?connectionId=none (account-wide only) or ?externalId= (empty if that number does not exist yet). A wh_… or a number of another account gives the same 404 as one that does not exist.

Reconnecting the same number with a link carrying the same URL does not add another webhook: it updates that one, with the new link's secret. If the onboarding fails, no webhook is left behind.

Rotating the secret takes effect at once: from then on we sign with the new one and the old one stops verifying. Update it in your system right away.