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.

Copy
Copied
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.

VanillaReact
Copy
Copied
<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>
Copy
Copied
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.

Copy
Copied
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.

Copy
Copied
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.

VanillaReact
Copy
Copied
<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>
Copy
Copied
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.

Copy
Copied
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.

Copy
Copied
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.

Copy
Copied
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.

State transitions for ES Senders

IN_PROGRESS
ERROR
ACTIVE
SUSPENDED
INACTIVE
Will stay in INACTIVE for up to 30 days