Connect is how another product plugs into Customer Pro. A business using your app clicks one button, sees exactly what you are asking for, picks which of their WhatsApp numbers you may use, and approves. You get a credential scoped to that one business and nothing else — no password, no API key pasted into a settings field, no support ticket.
Register your application #
-
Open Developer → Applications
Register the application that will be connecting. You name it, and that name is what your customers see on the consent screen. -
List your redirect URIs
The addresses we may send an authorization code to. They are matched by exact string equality — no wildcards, no subdomain matching. Add every environment you will use, including localhost while you build. -
Choose the scopes you may ask for
A scope you do not register is one you can never request. Ask for the fewest you need; the consent screen shows each one in plain language, and a long list costs you approvals. -
Copy the client secret
It is shown once, at registration. It is what proves the code exchange is really you, so it lives on your server and nowhere near a browser.
An application registered by an organization can only be approved by that organization, which is what makes registration self-service and safe. To be connectable by any business, an application has to be verified by hand — get in touch once yours is working.
Build the authorization URL on your server #
Two of its parameters must never exist in a browser: the PKCE verifier you keep to redeem the code, and the state you check on the way back. So your server builds the URL and renders it into the page.
$verifier = rtrim(strtr(base64_encode(random_bytes(64)), '+/', '-_'), '=');
$state = bin2hex(random_bytes(16));
session(['cp_verifier' => $verifier, 'cp_state' => $state]);
$authorizeUrl = 'https://customerpro.in/connect/authorize?'.http_build_query([
'client_id' => config('services.customerpro.client_id'),
'redirect_uri' => 'https://your-app.example/integrations/customerpro/callback',
'scope' => 'messages.send templates.read',
'state' => $state,
'code_challenge' => rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '='),
'code_challenge_method' => 'S256',
]);
Drop in the button #
One script tag and one button. The script opens the consent screen in a popup, so your customer never leaves your page, and hands the authorization code to the callback route you already have.
<script src="https://customerpro.in/connect/customerpro.js" defer></script>
<button data-customerpro-connect
data-authorize-url="{{ $authorizeUrl }}">Connect Customer Pro</button>
That is the whole browser-side integration. When the business approves, the popup posts the result back to your page and the script navigates to your redirect_uri with code and state appended — the same request your callback would have received from a plain redirect.
| Field | What it does |
|---|---|
| data-authorize-url | Required. The URL your server built above. The script appends display=popup to it. |
| data-redirect-uri | Optional. Where to send the result, if not the redirect_uri already inside the authorize URL. |
| data-unstyled | Optional. Suppresses the default button styling so your own CSS applies. |
If you would rather handle the code yourself — a single-page app that should not reload — cancel the event the button fires:
button.addEventListener('customerpro:connect', function (event) {
event.preventDefault();
// event.detail is { code, state, error }
});
Exchange the code for credentials #
Your callback receives code and state. Check the state matches the one you stored, then exchange the code from your server, within 60 seconds, exactly once.
POST https://customerpro.in/connect/token
{
"code": "...",
"code_verifier": "the verifier you stored",
"client_id": "your client id",
"client_secret": "your client secret"
}
The response carries the connection's own client_id and client_secret — one pair per business — plus their organization, the WhatsApp number they chose, the scopes they granted and their plan. Store the secret: it is shown once and cannot be retrieved afterwards. Those are the credentials you send to the REST API from then on.
Connected, but the plan cannot use it #
This is the one that catches every integration, so handle it before you ship. A successful handshake proves the business authorized you. It does not prove their plan sells what you are about to call — consent is theirs to give, API access is something they buy. The two are separate on purpose.
So your very first call after a brand-new connection can answer 403 with nothing wrong anywhere:
{
"error": "This organization's plan does not include API access.",
"code": "plan_upgrade_required",
"resource": "api_access",
"upgrade_url": "https://customerpro.in/subscription"
}
-
Keep the credential
It is real and it stays valid. This is not a failed connection and re-running the handshake will not change the answer. -
Do not retry
Nothing about waiting helps. Only the business moving to a plan that includes API access does. -
Tell the business, not yourself
They are the only one who can fix it. Show them the message and link them to theupgrade_urlin the body. -
Find out immediately
The plan check runs at authentication, so every endpoint refuses identically — you cannot call/meto test first, the 403 body is the answer. Make that/mecall right after the exchange anyway, so you can say this while the business is still looking at your connect screen.
When it does not work #
| Field | What it does |
|---|---|
| The error page says the redirect address does not match | Redirect URIs are compared by exact string equality — no wildcards, no subdomain matching, and a trailing slash counts. Copy the URI out of your authorize URL and paste it into the application's registered list verbatim. |
| invalid_grant on the exchange | Deliberately says nothing about which check failed. In order of likelihood: the PKCE encoding (unpadded base64url of the raw digest, not a hex string), a code older than 60 seconds, a code already exchanged once, or the wrong client_secret. The code is burned even by a failed attempt, so start the handshake again rather than retrying the exchange. |
| The consent screen says this application has not been approved yet | The application has no owning organization, which makes it a platform application any business could approve — so it has to be verified by hand first. An application registered by an organization and approved by that same organization never hits this. |
| 403 plan_upgrade_required | The business's plan does not include API access — see above. Not a bug in your integration. |
| 403 plan_limit_exceeded | Read resource before deciding what to do: api_calls is a daily ceiling that resets at midnight, messages is a monthly allowance that resets on the 1st. Treating one as the other means telling the business to wait weeks for something that clears overnight. |
| 401 after it was working | The connection ended — the business deleted the key, you revoked it, or a reconnect superseded it. Treat as disconnected and prompt them to reconnect. If you have just handled a reconnect, check you stored the new secret: a superseded key answers 401 too. |
| Nothing happens when the button is clicked | Open the console. The script logs when data-authorize-url is missing, and a popup the browser refuses falls back to a full-page redirect rather than failing silently. |
Disconnecting #
A business can end a connection at any time by deleting its key on Developer → API keys; your next call answers 401, which means disconnected, not broken. Stop retrying and prompt them to reconnect.
When you are the one ending it, say so: DELETE /api/v1/me hands the credential back and marks the connection disconnected on the business's screen. To end every connection your application holds — winding the integration down, or a leaked secret — use POST /api/v1/applications/{client_id}/revoke-connections. Deleting an application does not revoke its connections.
Let your coding agent build it #
The complete handshake is packaged as a skill file. Drop it into your own project and your agent writes the integration without anyone reading this page:
mkdir -p .claude/skills/customerpro-connect
curl -sL https://customerpro.in/connect/skill.md -o .claude/skills/customerpro-connect/SKILL.md