Connect is how another product plugs into Customer Pro Staging. 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.
A newly registered application can only be approved by the organization that registered it, which is what makes registration self-service and safe — a name chosen to look like somebody else buys nothing, because the only account that can approve it is your own. That is also how you test the handshake end to end before anyone else sees it. When it works, publish it.
Publish it so other businesses can connect #
-
Add a one-line description and a start URL
The description is what a business reads in their Integrations directory. The start URL is your own page — the one that builds the authorize URL — because only you can mint thestateand the PKCE challenge the handshake is checked against. -
Press Publish on Developer → Applications
It takes effect immediately. There is no review queue and no deploy on our side. -
Send businesses to your start URL
Or let them find you: a published application is listed under Integrations for every business on Customer Pro Staging.
Your organization stays on the application as its publisher, and the consent screen says so: Published by {your organization}, plus a plain note that Customer Pro Staging has not reviewed it. Once we have checked who you are, that note is replaced by a Verified badge. Verification is a badge, not a gate — publishing alone is enough to be connectable.
Unpublishing stops new connections and nothing else. The connections you already hold keep working; ending those is the separate Revoke connections button, which reaches into other businesses' accounts and is deliberately never a side effect of tidying up.
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://www.staging.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://www.staging.customerpro.in/connect/customerpro.js" defer></script>
<button data-customerpro-connect
data-authorize-url="{{ $authorizeUrl }}">Connect Customer Pro Staging</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://www.staging.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://www.staging.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. |
| The consent screen says it belongs to another organization | The application has not been published, so only the workspace that registered it can approve it. Publish it from Developer → Applications and the same link works for everyone. |
| 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 from Integrations, or 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://www.staging.customerpro.in/connect/skill.md -o .claude/skills/customerpro-connect/SKILL.md