Skip to content

Onboarding Clients via API

Programmatically create partner invitations to let clients connect their WhatsApp Business account or Facebook Page

View .md

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.

connectionTypeClient authorizesResult
whatsapp_wabaMeta's embedded signupA sender with the WhatsApp channel
messengerFacebook, then picks a Page they administerA 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

ParameterTypeRequiredDescription
clientNamestringNoName to identify the client (max 100 chars)
clientEmailstringNoClient's email address
clientPhonestringNoClient's phone number (E.164 format)
connectionTypestringNowhatsapp_waba (default) or messenger
phoneNumberIdstringNoPre-assign a Zavu phone number. whatsapp_waba only
allowedPhoneCountriesstring[]NoISO country codes for allowed numbers. whatsapp_waba only
expiresInDaysintegerNoLink 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

ParameterTypeDescription
statusstringFilter by status: pending, in_progress, completed, expired, cancelled
limitintegerResults per page (default: 50, max: 100)
cursorstringPagination 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

FieldTypeDescription
idstringUnique invitation ID
urlstringFull invitation URL to share with client
tokenstringURL token (part of the URL)
clientNamestringClient name
clientEmailstringClient email
phoneNumberIdstringPre-assigned phone number ID (if any)
connectionTypestringwhatsapp_waba or messenger
statusstringCurrent status
senderIdstringCreated sender ID (when completed)
connectedAccountobjectWhat was linked, once completed: { channel, id, name }. The WhatsApp number, or the Facebook Page. null before completion
failureReasonstringWhy the last attempt failed, when status is failed
failedAtstringWhen the last attempt failed
expiresAtstringExpiration timestamp
viewedAtstringWhen client first viewed the invitation
startedAtstringWhen client started the setup
completedAtstringWhen 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:

TypeScript
async 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:

TypeScript
async 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.

terminal
curl -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

TypeScript
try { 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

CodeDescription
not_foundPhone number or invitation not found
invalid_requestInvalid parameters
phone_number_wrong_teamPhone number belongs to a different team
already_completedCannot cancel a completed invitation

Best Practices

Store Invitation IDs

Save the invitation ID in your database to track status and link to your customer records.

Set Reminders

If an invitation is pending for several days, send a reminder to your client.

Handle Expiration

Expiry sends no event. Check expiresAt on pending invitations and resend or recreate them.

Use Webhooks

Subscribe to invitation.status_changed to know the moment a client connects or fails.

Rate Limits

The Invitations API has the following rate limits:

EndpointLimit
Create invitation100 requests/minute
List invitations200 requests/minute
Get invitation200 requests/minute
Cancel invitation100 requests/minute

Next Steps