Embedded Signup for creating a WhatsApp Sender
The Embedded Signup (ES) sender creation flow is a mixture of API calls as well as an embedded browser element. The end customer must have access to a "Login with Facebook" button to trigger the ES flow with Meta.
Embed the browser element
First retrieve a temporary URL for the login button. This URL is valid for one hour and should match the region that the user is intending to create the sender in.
Warning!
The Access Key and Secret should be kept confidential. Avoid performing these steps in the browser and use a backend service instead.
You can set up a flow for one WABA or for multiple WABAs.
Flow for only one WABA
The ES flow will create the necessary resources and return a code. The code needs to be registered with Sinch to be able to create the Sender and start messaging.
After 1st of February query parameter type
will no longer be required.
async function getTemporaryUrl() {
const resp = await fetch(
`https://provisioning.api.sinch.com/v1/projects/${PROJECT_ID}/whatsapp/login?region=${REGION}&type=CODE`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization:
'Basic ' +
Buffer.from(ACCESS_KEY + ':' + ACCESS_SECRET).toString('base64'),
},
}
);
const { url } = await resp.json();
return url;
}
The URL should be used with an iFrame to the browser user. The parent page and the iFrame communicates through Cross-document messaging (see Window.postMessage and Window.message_event).
Once the Login URL has been propagated to the frontend, it should be added to the page as well as event listeners. Depending on the framework used, the way this is done is different. Below we give an example for vanilla Javascript and HTML as well as a React component.
<html>
<body>
<!-- Insert any form for setup values here. Should trigger `setSetupData` when edited. -->
<script>
/**
* Send setup information to Login button iFrame.
*/
var button = document.querySelector('#sendMessage');
function setSetupData(value) {
const iframe = document.querySelector('iframe');
iframe.contentWindow.postMessage(
{
setup: value,
},
'*'
);
}
/**
* Receive result from Embedded Signup.
* Either the user cancelled or the code has been returned.
*/
function onMessageHandler(event) {
if (event.data) {
if (event.data.cancelled) {
console.log('Embedded Signup was cancelled');
} else {
console.log(`Received code: ${event.data.code}`);
}
}
}
/**
* Set the iFrame URL from previous call.
*/
function setIFrameUrl(url) {
var iFrame = document.querySelector('#iframe');
iFrame.src = url;
}
window.addEventListener('message', onMessageHandler);
</script>
<iframe
id="iframe"
style="
margin-top: 1em;
width: 100%;
height: 50px;
border: solid 1px #ccc;
overflow: hidden;
"
title="embedded signup"
src="about:blank"
></iframe>
</body>
</html>
import { useEffect, useRef, useCallback } from 'react';
export interface Setup {
business?: {
name?: string;
about?: string;
email?: string;
phone?: {
code?: number;
number?: string;
};
website?: string;
address?: {
streetAddress1?: string;
city?: string;
state?: string;
zipPostal?: string;
country?: string;
};
timezone?: string;
};
phone?: {
displayName?: string;
category?: string;
description?: string;
photoUrl?: string;
};
}
interface EmbeddedSignupProps {
onCancel?: () => void;
onSubmit?: (code: string) => void;
setup?: Setup;
region: 'EU' | 'US';
loginUrl: string;
}
function EmbeddedSignup(props: EmbeddedSignupProps) {
const { setup, onCancel, onSubmit, loginUrl } = props;
const ESIFrameRef = useRef<HTMLIFrameElement>(null);
if (ESIFrameRef.current && ESIFrameRef.current.contentWindow) {
ESIFrameRef.current.contentWindow.postMessage({ setup }, '*');
}
const messageEvent = useCallback(
(event: MessageEvent<any>) => {
if (event.data) {
if (event.data.cancelled && onCancel) {
onCancel();
} else if (event.data.code && onSubmit) {
onSubmit(event.data.code);
}
}
},
[onCancel, onSubmit]
);
useEffect(() => {
window.removeEventListener('message', messageEvent);
window.addEventListener('message', messageEvent);
}, [onCancel, onSubmit, messageEvent]);
return (
<>
<iframe
title="es"
src={loginUrl}
width="100%"
height="50px"
id="loginButton"
className=""
scrolling="no"
frameBorder={0}
style={{
marginTop: '1em',
width: '100%',
height: '50px',
border: '0px',
overflow: 'hidden',
}}
ref={ESIFrameRef}
></iframe>
</>
);
}
export default EmbeddedSignup;
Creating a sender with a code
WABA details will be fetched automatically when calling the create sender endpoint.
async function createSender() {
const resp = await fetch(
`https://provisioning.api.sinch.com/v1/projects/${PROJECT_ID}/whatsapp/senders`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization:
'Basic ' +
Buffer.from(ACCESS_KEY + ':' + ACCESS_SECRET).toString('base64'),
},
body: JSON.stringify({
region: REGION, // Same region as ES login URL was used
facebookCode: CODE, // facebookCode from ES popup
name: 'Example ES Sender',
details: {
displayName: 'Example Sender',
vertical: 'OTHER',
about: 'A simple example sender',
photoUrl: 'https://www.example.com/some_image.jpg',
},
}),
}
);
const data = await resp.json();
return data;
}
Flow for multiple WABAs
The ES flow will create the necessary resources and return a code. This code should be used to request long lived access token which is needed to list WABAs, create sender and start messaging.
After 1st of February query parameter type
will no longer be required.
async function getTemporaryUrl() {
const resp = await fetch(
`https://provisioning.api.sinch.com/v1/projects/${PROJECT_ID}/whatsapp/login?region=${REGION}&type=CODE`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization:
'Basic ' +
Buffer.from(ACCESS_KEY + ':' + ACCESS_SECRET).toString('base64'),
},
}
);
const { url } = await resp.json();
return url;
}
The URL should be used with an iFrame to the browser user. The parent page and the iFrame communicates through Cross-document messaging (see Window.postMessage and Window.message_event).
Once the Login URL has been propagated to the frontend, it should be added to the page as well as event listeners. Depending on the framework used, the way this is done is different. Below we give an example for vanilla Javascript and HTML as well as a React component.
<html>
<body>
<!-- Insert any form for setup values here. Should trigger `setSetupData` when edited. -->
<script>
/**
* Send setup information to Login button iFrame.
*/
var button = document.querySelector('#sendMessage');
function setSetupData(value) {
const iframe = document.querySelector('iframe');
iframe.contentWindow.postMessage(
{
setup: value,
},
'*'
);
}
/**
* Receive result from Embedded Signup.
* Either the user cancelled or the code has been returned.
*/
function onMessageHandler(event) {
if (event.data) {
if (event.data.cancelled) {
console.log('Embedded Signup was cancelled');
} else {
console.log(`Received code: ${event.data.code}`);
}
}
}
/**
* Set the iFrame URL from previous call.
*/
function setIFrameUrl(url) {
var iFrame = document.querySelector('#iframe');
iFrame.src = url;
}
window.addEventListener('message', onMessageHandler);
</script>
<iframe
id="iframe"
style="
margin-top: 1em;
width: 100%;
height: 50px;
border: solid 1px #ccc;
overflow: hidden;
"
title="embedded signup"
src="about:blank"
></iframe>
</body>
</html>
import { useEffect, useRef, useCallback } from 'react';
export interface Setup {
business?: {
name?: string;
about?: string;
email?: string;
phone?: {
code?: number;
number?: string;
};
website?: string;
address?: {
streetAddress1?: string;
city?: string;
state?: string;
zipPostal?: string;
country?: string;
};
timezone?: string;
};
phone?: {
displayName?: string;
category?: string;
description?: string;
photoUrl?: string;
};
}
interface EmbeddedSignupProps {
onCancel?: () => void;
onSubmit?: (code: string) => void;
setup?: Setup;
region: 'EU' | 'US';
loginUrl: string;
}
function EmbeddedSignup(props: EmbeddedSignupProps) {
const { setup, onCancel, onSubmit, loginUrl } = props;
const ESIFrameRef = useRef<HTMLIFrameElement>(null);
if (ESIFrameRef.current && ESIFrameRef.current.contentWindow) {
ESIFrameRef.current.contentWindow.postMessage({ setup }, '*');
}
const messageEvent = useCallback(
(event: MessageEvent<any>) => {
if (event.data) {
if (event.data.cancelled && onCancel) {
onCancel();
} else if (event.data.code && onSubmit) {
onSubmit(event.data.code);
}
}
},
[onCancel, onSubmit]
);
useEffect(() => {
window.removeEventListener('message', messageEvent);
window.addEventListener('message', messageEvent);
}, [onCancel, onSubmit, messageEvent]);
return (
<>
<iframe
title="es"
src={loginUrl}
width="100%"
height="50px"
id="loginButton"
className=""
scrolling="no"
frameBorder={0}
style={{
marginTop: '1em',
width: '100%',
height: '50px',
border: '0px',
overflow: 'hidden',
}}
ref={ESIFrameRef}
></iframe>
</>
);
}
export default EmbeddedSignup;
Getting long-lived access token
A long-lived access token is required to select WABA details for the WhatsApp Sender.
async function createLongLivedAccessToken() {
const resp = await fetch(
`https://provisioning.api.sinch.com/v1/projects/${PROJECT_ID}/whatsapp/longLivedAccessToken`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization:
'Basic ' +
Buffer.from(ACCESS_KEY + ':' + ACCESS_SECRET).toString('base64'),
},
body: JSON.stringify({
facebookCode: CODE, // facebookCode from ES popup
}),
}
);
const data = await resp.json();
return data;
}
createLongLivedAccessToken();
Specifying WABA and phone number
As part of the Embedded Signup process, the user will select or create a new WABA as well as the phone number to be used for the WhatsApp Sender. To be able to pass that to the Provisioning API you will need the WABA ID and the phone number ID.
async function listWhatsAppBusinessAccount() {
const resp = await fetch(
`https://provisioning.api.sinch.com/v1/projects/${PROJECT_ID}/whatsapp/wabaDetails`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization:
'Basic ' +
Buffer.from(ACCESS_KEY + ':' + ACCESS_SECRET).toString('base64'),
},
body: JSON.stringify({
facebookToken: LONG_LIVED_ACCESS_TOKEN, // longLivedAccessToken from the previous call
}),
}
);
const data = await resp.json();
return data;
}
listWhatsAppBusinessAccount();
If this call returns more than one WABA or more than one phone number ID, then the user needs to be presented with an option to select which one they want to use to create the sender.
Creating a sender with a long lived access token
Once the necessary information has been collected, call the create sender endpoint.
async function createSender() {
const resp = await fetch(
`https://provisioning.api.sinch.com/v1/projects/${PROJECT_ID}/whatsapp/senders`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization:
'Basic ' +
Buffer.from(ACCESS_KEY + ':' + ACCESS_SECRET).toString('base64'),
},
body: JSON.stringify({
region: REGION, // Same region as ES login URL was used
wabaId: WABA_ID, // Id from the list WABA step
phoneNumberId: PHONE_ID, // Id from the list WABA step
facebookAccessToken: LONG_LIVED_ACCESS_TOKEN, // longLivedAccessToken from the previous call
name: 'Example ES Sender',
details: {
displayName: 'Example Sender',
vertical: 'OTHER',
about: 'A simple example sender',
photoUrl: 'https://www.example.com/some_image.jpg',
},
}),
}
);
const data = await resp.json();
return data;
}
Next steps
If the call is successful, the sender will move through the state transitions and eventually become active. Once active, you can crate templates for the sender.