waiaconnect

Documentation / Complete

Integrator journey: onboarding and webhooks 100 % by API

GET /v1/onboarding-links/{id}

Markdown

If you onboard your customers from your own system, without opening the dashboard, this is the whole path, in five steps.

1. One link per customer. POST /v1/onboarding-links with your externalId (your id for that customer) and webhook: { url }. We return the url for your customer and the webhook's whsec_… secret once: store it now, it signs everything about that customer. An externalId already used by another number of your account returns 409 right here, not at the end of onboarding.

2. Your customer connects. They open the url, log in with their Meta account and pick their number. When they finish, the number is born with its webhook (the link's URL) and onboarding.completed arrives there, with connection.id (conn_…), your externalId, reconnected and the webhookId. A new number also brings connection.created, as always.

3. If they don't finish, you still find out. Every attempt that stops (failed, cancelled or abandoned) sends onboarding.failed to the link webhook's URL, signed with the same secret: reason (meta_error, incomplete, timeout, cancelled, abandoned…), metaErrorCode and metaMessage if Meta gave them, and linkUsable (whether your customer can retry with the same link). If you'd rather ask: GET /v1/onboarding-links?externalId=… or GET /v1/onboarding-links/{id} return outcome (pending, completed or failed, with the same reason).

4. The number's webhook is yours. See and change it with /v1/webhooks?externalId=…: URL, events, static header, rotate the secret, test it.

5. Offboarding. When the number disconnects (from the dashboard, because the business removed our access in Meta, or because the permission was revoked), its webhook is switched off (active: false, disabledReason: "connection_disconnected") and stops receiving. If the same number reconnects, it comes back on by itself; the ones you paused or we switched off for failing don't.

# 1) one link per end-customer: your id + the webhook its number will have
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", "label": "Pharmacy 42", "mode": "coexistence",
       "webhook": {"url": "https://api.yourcompany.com/hooks/connect"}}'
# → 201 { "publicId": "onb_…", "url": "https://…/c#<token>", "expiresAt": "…",
#         "webhook": { "secret": "whsec_…", "createdOnCompletion": true, … } }   ← store the secret NOW

# 2) send "url" to your customer. Then, on https://api.yourcompany.com/hooks/connect:
#    { "type": "onboarding.completed", "connection": { "id": "conn_…", "externalId": "pharmacy-42", … },
#      "data": { "onboardingLinkId": "onb_…", "reconnected": false, "webhookId": "wh_…", … } }
#    { "type": "onboarding.failed", "connection": { "id": null, … },
#      "data": { "externalId": "pharmacy-42", "reason": "meta_error", "metaErrorCode": 2655093,
#                "metaMessage": "The business is already sharing…", "linkUsable": true, … } }

# 3) or ask (polling) by your id
curl "https://api.waiaconnect.com/v1/onboarding-links?externalId=pharmacy-42" -H "Authorization: Bearer wc_live_YOUR_API_KEY"
# → { "links": [ { "id": "onb_…", "status": "used", "connectionId": "conn_…",
#       "outcome": { "state": "completed", "connectionId": "conn_…", "webhookId": "wh_…", … } } ] }
curl https://api.waiaconnect.com/v1/onboarding-links/onb_… -H "Authorization: Bearer wc_live_YOUR_API_KEY"

# 4) the number's webhook, by your id (PATCH to change url/events, rotate-secret, test)
curl "https://api.waiaconnect.com/v1/webhooks?externalId=pharmacy-42" -H "Authorization: Bearer wc_live_YOUR_API_KEY"

# 5) offboarding: disconnecting the number switches its webhook off
#    (active: false, disabledReason: "connection_disconnected"); reconnecting it switches it back on
const H = { "Authorization": "Bearer wc_live_YOUR_API_KEY", "Content-Type": "application/json" };

// 1) one link per end-customer: your id + the webhook its number will have
const link = await (await fetch("https://api.waiaconnect.com/v1/onboarding-links", {
  method: "POST", headers: H,
  body: JSON.stringify({ externalId: "pharmacy-42", label: "Pharmacy 42", mode: "coexistence",
    webhook: { url: "https://api.yourcompany.com/hooks/connect" } })
})).json();
await saveSecret("pharmacy-42", link.webhook.secret); // shown ONCE — it signs this customer's events
await sendToCustomer(link.url);

// 2) your webhook handler (verify the signature first — see «Verify the signature»)
app.post("/hooks/connect", express.raw({ type: "application/json" }), async (req, res) => {
  const evt = JSON.parse(req.body);
  if (evt.type === "onboarding.completed") await markConnected(evt.data.externalId, evt.connection.id);
  if (evt.type === "onboarding.failed") await tellCustomer(evt.data.externalId, evt.data.reason, evt.data.metaErrorCode, evt.data.linkUsable);
  res.sendStatus(200);
});

// 3) or poll by your id
const { links } = await (await fetch("https://api.waiaconnect.com/v1/onboarding-links?externalId=pharmacy-42", { headers: H })).json();
console.log(links[0]?.outcome); // { state: "pending" | "completed" | "failed", reason, metaErrorCode, … }
<?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) one link per end-customer: your id + the webhook its number will have
$link = connect("POST", "https://api.waiaconnect.com/v1/onboarding-links", [
  "externalId" => "pharmacy-42", "label" => "Pharmacy 42", "mode" => "coexistence",
  "webhook" => ["url" => "https://api.yourcompany.com/hooks/connect"],
]);
save_secret("pharmacy-42", $link["webhook"]["secret"]); // shown ONCE

// 2) in your webhook handler: onboarding.completed / onboarding.failed (data.reason, data.metaErrorCode)
// 3) or poll by your id
$links = connect("GET", "https://api.waiaconnect.com/v1/onboarding-links?externalId=pharmacy-42")["links"];
$outcome = $links[0]["outcome"] ?? null; // state: pending | completed | failed
import requests
H = {"Authorization": "Bearer wc_live_YOUR_API_KEY"}

# 1) one link per end-customer: your id + the webhook its number will have
link = requests.post("https://api.waiaconnect.com/v1/onboarding-links", headers=H, json={
    "externalId": "pharmacy-42", "label": "Pharmacy 42", "mode": "coexistence",
    "webhook": {"url": "https://api.yourcompany.com/hooks/connect"},
}).json()
save_secret("pharmacy-42", link["webhook"]["secret"])  # shown ONCE

# 2) in your webhook handler: onboarding.completed / onboarding.failed (data.reason, data.metaErrorCode)
# 3) or poll by your id
links = requests.get("https://api.waiaconnect.com/v1/onboarding-links", headers=H, params={"externalId": "pharmacy-42"}).json()["links"]
outcome = links[0]["outcome"] if links else None  # state: pending | completed | failed

Without webhook in the link, onboarding.completed and onboarding.failed go to your account webhooks (in onboarding.failed, connection.id is null: your externalId is in data).

A webhook with an explicit events list that doesn't include these types won't receive them; one with all events starts receiving them.

In onboarding links, connectionId now comes as conn_…; the internal id stays in legacyConnectionId, which will be removed.

outcome.state = failed also when the link expired (link_expired) or you revoked it (link_revoked) before your customer finished.