Skip to main content

Zendesk OAuth local test playbook

This playbook reproduces the local Zendesk OAuth validation used for the OpenCX Zendesk integration. It covers a fresh install, an existing legacy customer switching to OAuth, OAuth-only operation, same-account fallback, cross-account isolation, inbound messaging, replies, media, handoff, Agent Workspace, closing and reopening, uninstall, and reinstall. Use a Zendesk development account and a disposable OpenCX organization. Do not run destructive lifecycle steps against a customer account.

What must pass

Prerequisites

  • Node.js 24.
  • Docker and the services in backend/docker-compose.yml.
  • cloudflared for a public webhook origin.
  • Google Chrome for the local full-stack browser test.
  • A Zendesk development account with Support, Messaging, Agent Workspace, a Web Widget channel, and admin access.
  • An approved OpenCX Marketplace bot for Messaging OAuth.
  • An approved Zendesk global OAuth client for Ticketing OAuth. URL-construction tests can run with a test client ID, but successful consent requires the real approved client.
  • An OpenCX development user with owner/admin access to a disposable organization.

Keep credentials out of files

Never paste OAuth secrets, access tokens, callback codes, signed webhook URLs, widget keys, or Zendesk passwords into this document, a shell history entry, a test file, a commit, or CI output. Store local secrets in macOS Keychain and expose them only to the process that needs them. For example:
Load values without printing them:
The backend uses these variables:
Use literal values in backend/.env only when that ignored file is already the team’s approved local secret store. Never add it to Git.

Prepare the worktree

Run every command from the OAuth PR worktree, not a different checkout:
The branch must be the Zendesk OAuth PR branch and Node must report v24.x. Install dependencies separately because backend and dashboard pin different pnpm versions:
If a private @opencx-labs/* package returns 401 or 403, repair the Keychain-backed GitHub Packages token first. Do not share another worktree’s node_modules; cross-worktree symlinks caused the incorrect local font and incomplete dependency tree seen during the original validation.

Start local infrastructure

The required local ports are:

Create a narrow public webhook proxy

Do not tunnel the entire local backend. The proxy below exposes only the signed Zendesk webhook route and Marketplace uninstall route, while capturing the latest event for the live test. Create /tmp/opencx-zendesk-webhook-proxy.mjs:
Start it and the tunnel:
Copy only the generated HTTPS origin, without a trailing slash:
Start the backend after SERVER_BASE_URL is set. The OAuth callback uses it to provision the real Zendesk webhook target.
Confirm the public proxy is narrow:
A HEAD request returns success for tunnel health. An unrelated GET or POST path must return 404.

Start the dashboard

Set the local backend URL in dashboard/.env:
Then start the dashboard:
Open http://localhost:3000/auth, use Dev login, select the disposable org, then open Settings → Integrations → Zendesk. The first page must show exactly two product choices: Zendesk Ticketing and Zendesk Messaging. Selecting a product must show exactly two authorization choices: OAuth and Legacy credentials. OAuth carries the New and Recommended badges.

Run the full-stack modal test

This test uses the real local backend and system Chrome without downloading a Playwright browser revision:
It verifies the two-stage selector, both Legacy forms, both OAuth panels, invalid Ticketing subdomains, real start URL construction, callback-error routing, status isolation, the Ticketing reply webhook, and trigger-instructions link.

Ticketing OAuth: Zendesk-side setup

  1. In OpenCX choose Zendesk → Ticketing → OAuth.
  2. Enter only the Zendesk subdomain, such as acme.
  3. Click Continue to Zendesk.
  4. Confirm the Zendesk hostname matches the entered subdomain.
  5. Sign in as an agent or admin and click Allow.
  6. Confirm OpenCX returns to the OAuth panel with Connected and the correct subdomain.
  7. Copy the signed Reply webhook URL shown in the panel.
  8. In Zendesk Admin Center open Apps and integrations → Webhooks and create a POST JSON webhook with no extra authentication.
  9. Open Objects and rules → Triggers and create the following trigger:
    • Meet ANY: Ticket is Created; Ticket is Updated.
    • Meet ALL: Comment is Present (public).
    • Action: notify the OpenCX active webhook.
    • Body:
OAuth authenticates OpenCX’s outbound Support API calls. The classic trigger is independently required for inbound public-comment delivery. Never put comment text in the webhook body; OpenCX fetches and filters public comments itself. If the global client has not yet been approved, stop after the automated start-URL test. A test client ID can prove URL construction but cannot prove consent, token exchange, or refresh rotation.

Messaging OAuth: Zendesk-side setup

  1. In OpenCX choose Zendesk → Messaging → OAuth and click Continue to Zendesk.
  2. Zendesk opens the OpenCX Marketplace bot authorization. Click Allow once.
  3. For a local backend, Zendesk may redirect to the registered production callback origin. Before that callback is consumed, replace only the origin with http://localhost:8080, preserving /backend/zendesk-sunshine-oauth/callback and the entire query string byte-for-byte. Do not reload or reuse a callback code.
  4. Confirm the local callback returns to http://localhost:3000/settings/integrations and shows Connected.
  5. In Zendesk Admin Center open AI → AI agents → AI agents → Marketplace bots → OpenCX.
  6. Assign OpenCX to one disposable Web Widget or staging channel and save.
  7. In Channels → Messaging and social → Messaging, make OpenCX the test channel’s default responder.
  8. Set Conversation control to Release control so a closed Agent Workspace conversation returns to OpenCX on the next customer message.
The OAuth callback automatically provisions the integration-scoped webhook. Do not manually create a second Sunshine webhook for the OAuth path.

Create the local Web Widget page

Get the test channel’s Web Widget key from Zendesk Admin Center, then create /tmp/opencx-zendesk-widget.html without committing it:
Serve and open it:
Send a unique message such as OAuth live inbound <UUID>. Wait for /tmp/opencx-last-zendesk-webhook.json, then extract the live values without printing the whole webhook:

Run the real Messaging OAuth lifecycle

CI_SKIP_FLAKY_TESTS must be absent. Setting it to false still skips tests because any nonempty value is truthy in the test environment.
The test proves:
  • the active auth is a Marketplace bearer;
  • the database value is encrypted at rest;
  • Zendesk has the expected signed webhook target;
  • the real inbound webhook created a contact, session, conversation mapping, and history row;
  • replaying the captured webhook does not duplicate the inbound message;
  • an OpenCX business reply and image reach the real Zendesk conversation;
  • handoff transfers switchboard control to zd:agentWorkspace;
  • a post-handoff OpenCX agent reply reaches Zendesk and remains one local agent message.

Manual Agent Workspace and reopen checks

After the live spec:
  1. Open the real conversation in Zendesk Agent Workspace.
  2. Send a unique public agent reply.
  3. Confirm the customer sees it in the Web Widget and OpenCX stores it once as an agent message.
  4. Solve and close the Agent Workspace conversation.
  5. Confirm the OpenCX session closes and the Zendesk ticket reaches the expected terminal state.
  6. Send another unique customer message from the widget.
  7. Confirm routing returns to OpenCX, a new OpenCX session is created, and the new lifecycle does not append to the sealed session.

Legacy, hybrid, and isolation regressions

Run the focused real Zendesk matrix with flaky skipping unset:
Then run the local DB/HTTP defensive matrix:

Uninstall and reinstall

Zendesk’s Marketplace uninstall callback is unauthenticated. OpenCX accepts cleanup only after the stored bearer proves revoked. Therefore:
  1. Remove the OpenCX Marketplace bot through Zendesk first.
  2. Wait for Zendesk’s Remove URL callback.
  3. If the saved Remove URL points to production and cannot reach the local backend, replay the same uninstall notification to the local public tunnel only after the real Zendesk removal has revoked the bearer:
  1. Confirm Messaging OAuth status is disconnected.
  2. Confirm any Legacy Sunshine credentials still exist and remain usable.
  3. Confirm an OAuth-only org no longer routes through Zendesk when neither Ticketing nor Legacy Sunshine auth remains.
  4. Start Messaging OAuth again, click Allow, reassign the bot to the staging channel, and confirm a new inbound message works.
Never send the uninstall callback while the bearer is active. OpenCX must retain the connection because an active bearer means the callback is forged or stale.

Final verification before pushing

Also run scoped formatter and linter checks for every changed backend, dashboard, test, and docs file. Review the entire diff, then run an independent PR review before committing. The completion record should include:
  • exact test commands and pass counts;
  • proof that CI_SKIP_FLAKY_TESTS was unset for live tests;
  • the test Zendesk subdomain and disposable OpenCX org, but no secrets;
  • confirmation that the installed token was encrypted at rest;
  • confirmation that webhook replay deduplicated;
  • confirmation that Agent Workspace reply, close, reopen, uninstall, and reinstall worked;
  • CI and reviewer status on the pushed PR.

Troubleshooting

Partner in query parameter 'client_id' not found

The Marketplace client ID is not approved, not published to the selected Zendesk environment, or does not match ZENDESK_SUNSHINE_OAUTH_CLIENT_ID. This is a Zendesk Marketplace registration problem, not a customer API-token problem. Confirm the global OAuth client is approved for the exact client ID and callback URL. A tenant-local OAuth client cannot behave as the global client used by this integration.

Callback lands on api.open.cx during local testing

Before consuming the one-time callback, replace only the origin with http://localhost:8080. Preserve the path, code, state, and encoding. Never paste the callback URL into logs or a ticket.

Callback says state is invalid or expired

Start a new OAuth flow in the same authenticated browser session. State is intentionally one-time, short-lived, and bound to the exact OpenCX user, org, and session.

No Messaging webhook arrives

Check all of the following:
  • backend started after SERVER_BASE_URL was set to the current tunnel;
  • the Marketplace bot is assigned to the test channel;
  • the channel’s default responder is OpenCX;
  • the OAuth status is connected and does not require reconnect;
  • the public proxy receives only the expected signed webhook path;
  • Conversation control is set to Release control for return routing.

Ticketing agent replies do not return to OpenCX

OAuth does not replace the Zendesk trigger. Confirm the signed Reply webhook URL, POST JSON webhook, Created-or-Updated public-comment trigger, and exact ticketId/event_type body.

Dashboard font or components look different from production

Verify the dashboard uses branch-local dependencies. Remove a broken node_modules symlink and reinstall with the correct dashboard pnpm version and a working private-package token. The expected UI font is Inter.

A real test is reported as skipped

Run it with env -u CI_SKIP_FLAKY_TESTS. Do not set the variable to false.

AI reply assertions fail with an OpenRouter authentication error

Repair the local OpenRouter test credential. That error is independent of Zendesk OAuth and should not be hidden by changing OAuth expectations.

Cleanup

  1. Delete every temporary Zendesk ticket, user, webhook, trigger, and channel assignment created by the run.
  2. Leave the Marketplace bot installed only if the disposable org is intended to remain connected; otherwise uninstall it through Zendesk and verify local cleanup.
  3. Stop dashboard, backend, widget server, webhook proxy, and cloudflared.
  4. Remove only these explicit temporary files:
  1. Stop Docker services when they are no longer needed:
Do not delete shared development databases, volumes, credentials, or another worktree’s dependencies as part of cleanup.