Onboarding Clients via API
Programmatically create partner invitations to let clients connect their WhatsApp Business account or Facebook Page
This guide shows you how to automate client onboarding using the Zavu API to create partner invitations programmatically.
Overview
The Partner Invitations API allows you to:
- Create invitation links programmatically
- Onboard a client on WhatsApp (
whatsapp_waba) or Messenger (messenger) - Integrate client onboarding into your own application
- Optionally pre-assign phone numbers to WhatsApp invitations
- Track invitation status and completion
Choosing a channel
connectionType decides what the client connects. It defaults to whatsapp_waba, so existing integrations keep working unchanged.
connectionType | Client authorizes | Result |
|---|---|---|
whatsapp_waba | Meta's embedded signup | A sender with the WhatsApp channel |
messenger | Facebook, then picks a Page they administer | A sender with the Messenger channel, receiving Page and Marketplace chats |
One invitation connects one channel. To onboard a client on both, create two invitations; each completes into its own sender.
Creating an Invitation
Basic Invitation
Create an invitation where the client provides their own phone number:
import Zavudev from '@zavudev/sdk';
const zavu = new Zavudev({
apiKey: process.env["ZAVUDEV_API_KEY"],
});
const invitation = await zavu.invitations.create({
clientName: 'Acme Corp',
clientEmail: 'contact@acme.com',
expiresInDays: 7,
});
console.log('Invitation URL:', invitation.url);
console.log('Token:', invitation.token);
console.log('Expires:', invitation.expiresAt);
Response
JSON{ "invitation": { "id": "jh7am5bng9p3v2x1k4r8", "url": "https://dashboard.zavu.dev/invite/abc123xyz", "token": "abc123xyz", "clientName": "Acme Corp", "clientEmail": "contact@acme.com", "phoneNumberId": null, "status": "pending", "senderId": null, "expiresAt": "2025-01-22T00:00:00.000Z", "createdAt": "2025-01-15T12:00:00.000Z" } }
With Pre-assigned Phone Number
Pre-assign a Zavu phone number that the client will register with WhatsApp:
// First, purchase a phone number
const phoneNumber = await zavu.phoneNumbers.purchase({
phoneNumber: '+14155551234',
name: 'Client: Acme Corp',
});
// Then create the invitation with the phone number
const invitation = await zavu.invitations.create({
clientName: 'Acme Corp',
clientEmail: 'contact@acme.com',
phoneNumberId: phoneNumber.id,
expiresInDays: 14,
});
console.log('Invitation URL:', invitation.url);
Messenger Invitation
Create an invitation for a Facebook Page. There is no phone number involved, so the WhatsApp-only options are rejected with 400.
const { invitation } = await zavu.invitations.create({
clientName: 'Acme Corp',
clientEmail: 'contact@acme.com',
connectionType: 'messenger',
});
console.log(invitation.url);
Create Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
clientName | string | No | Name to identify the client (max 100 chars) |
clientEmail | string | No | Client's email address |
clientPhone | string | No | Client's phone number (E.164 format) |
connectionType | string | No | whatsapp_waba (default) or messenger |
phoneNumberId | string | No | Pre-assign a Zavu phone number. whatsapp_waba only |
allowedPhoneCountries | string[] | No | ISO country codes for allowed numbers. whatsapp_waba only |
expiresInDays | integer | No | Link validity in days (1-30, default: 7) |
Listing Invitations
Retrieve all invitations for your project:
// List all invitations
const { items } = await zavu.invitations.list({});
// Filter by status
const pending = await zavu.invitations.list({
status: 'pending',
});
for (const inv of pending.items) {
console.log(inv.clientName, inv.status, inv.url);
}
List Parameters
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by status: pending, in_progress, completed, expired, cancelled |
limit | integer | Results per page (default: 50, max: 100) |
cursor | string | Pagination cursor |
Getting Invitation Details
Retrieve details for a specific invitation:
const invitation = await zavu.invitations.retrieve({
invitationId: 'jh7am5bng9p3v2x1k4r8',
});
console.log('Status:', invitation.status);
console.log('Client:', invitation.clientName);
if (invitation.status === 'completed') {
console.log('Sender ID:', invitation.senderId);
}
Response Fields
| Field | Type | Description |
|---|---|---|
id | string | Unique invitation ID |
url | string | Full invitation URL to share with client |
token | string | URL token (part of the URL) |
clientName | string | Client name |
clientEmail | string | Client email |
phoneNumberId | string | Pre-assigned phone number ID (if any) |
connectionType | string | whatsapp_waba or messenger |
status | string | Current status |
senderId | string | Created sender ID (when completed) |
connectedAccount | object | What was linked, once completed: { channel, id, name }. The WhatsApp number, or the Facebook Page. null before completion |
failureReason | string | Why the last attempt failed, when status is failed |
failedAt | string | When the last attempt failed |
expiresAt | string | Expiration timestamp |
viewedAt | string | When client first viewed the invitation |
startedAt | string | When client started the setup |
completedAt | string | When setup was completed |
Cancelling an Invitation
Cancel an invitation to prevent it from being used:
await zavu.invitations.cancel({
invitationId: 'jh7am5bng9p3v2x1k4r8',
});
Invitation Lifecycle
stateDiagram-v2
[*] --> pending: Create invitation
pending --> in_progress: Client starts setup
pending --> cancelled: Cancel invitation
in_progress --> completed: Setup successful
in_progress --> failed: Client cancelled, denied, or abandoned
in_progress --> cancelled: Cancel invitation
failed --> in_progress: Client retries the same link
failed --> cancelled: Cancel invitation
failed --> pending: Resend from the dashboard
cancelled --> pending: Resend from the dashboard
completed --> [*]: Sender created
A failed invitation is not dead: the link stays usable and moves back to in_progress when the client tries again. failureReason carries a stable code — fb_cancelled, fb_not_authorized, signup_abandoned, meta_no_pages (the client administers no Facebook Page), and others. Treat unknown codes as a generic failure.
A client who starts the connection and does not finish within an hour is marked failed with signup_abandoned.
Expiry is not a transition. A pending invitation past expiresAt is reported as expired when you read it, and its link stops working, but its stored status never changes. That means no invitation.status_changed event is sent when an invitation expires. Resend it from the dashboard, or create a new one.
Every arrow above except expiry sends an invitation.status_changed event. See Webhook Integration.
Integration Examples
SaaS Onboarding Flow
Integrate WhatsApp setup into your SaaS onboarding:
TypeScriptasync function onboardNewCustomer(customer: Customer) { // 1. Create the customer in your system await createCustomer(customer); // 2. Create a Zavu invitation const invitation = await zavu.invitations.create({ clientName: customer.companyName, clientEmail: customer.email, expiresInDays: 14, }); // 3. Send onboarding email with the invitation link await sendEmail({ to: customer.email, subject: 'Connect Your WhatsApp Business', body: ` Welcome to our platform! To enable WhatsApp messaging, please complete the setup: ${invitation.url} This link expires in 14 days. `, }); // 4. Store the invitation ID for tracking await saveInvitationId(customer.id, invitation.id); }
Polling for Completion
Check if a client has completed the setup:
TypeScriptasync function checkInvitationStatus(invitationId: string) { const invitation = await zavu.invitations.retrieve({ invitationId }); switch (invitation.status) { case 'completed': console.log('Client connected! Sender ID:', invitation.senderId); // Start sending messages through this sender break; case 'pending': case 'in_progress': console.log('Still waiting for client to complete setup'); break; case 'expired': console.log('Invitation expired, creating new one...'); // Create a new invitation break; case 'cancelled': console.log('Invitation was cancelled'); break; } return invitation; }
Webhook Integration
Subscribe your project webhook to invitation.status_changed to follow each client without polling. Configure it on the project whose API key creates the invitations: with sub-accounts, that is each sub-account, since a parent project does not receive its sub-accounts' events.
terminalcurl -X POST https://api.zavu.dev/v1/invitations/webhook \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.com/webhooks/zavu", "events": ["invitation.status_changed"] }'
TypeScript// In your webhook handler. Verify the X-Zavu-Signature header first. app.post('/webhooks/zavu', async (req, res) => { const event = req.body; if (event.type === 'invitation.status_changed') { const { invitationId, currentStatus, senderId, connectedAccount, failureReason } = event.data; if (currentStatus === 'completed') { // The client is connected; senderId is ready to send with await markCustomerConnected(invitationId, senderId, connectedAccount); } else if (currentStatus === 'failed') { // The same link still works, so a reminder is usually enough await remindCustomer(invitationId, failureReason); } } res.status(200).send('OK'); });
See the event reference for every field and transition.
Error Handling
TypeScripttry { const invitation = await zavu.invitations.create({ clientName: 'Acme Corp', phoneNumberId: 'pn_invalid', }); } catch (error) { if (error.code === 'not_found') { console.error('Phone number not found'); } else if (error.code === 'invalid_request') { console.error('Invalid request:', error.message); } else { console.error('Unexpected error:', error); } }
Common Errors
| Code | Description |
|---|---|
not_found | Phone number or invitation not found |
invalid_request | Invalid parameters |
phone_number_wrong_team | Phone number belongs to a different team |
already_completed | Cannot cancel a completed invitation |
Best Practices
Save the invitation ID in your database to track status and link to your customer records.
If an invitation is pending for several days, send a reminder to your client.
Expiry sends no event. Check expiresAt on pending invitations and resend or recreate them.
Subscribe to invitation.status_changed to know the moment a client connects or fails.
Rate Limits
The Invitations API has the following rate limits:
| Endpoint | Limit |
|---|---|
| Create invitation | 100 requests/minute |
| List invitations | 200 requests/minute |
| Get invitation | 200 requests/minute |
| Cancel invitation | 100 requests/minute |