# Zendesk

- Tool: Bubble
- Difficulty: Beginner
- Time required: 2–3 hours
- Last updated: July 2026

## TL;DR

Connect Bubble to Zendesk using Basic Auth with a crucial formatting detail: your username must be `your_email/token` (literally append `/token` to your email address) and the password is your API token string. Base64-encode the combined string and place it in a Private Authorization header in Bubble's API Connector. For inbound ticket events, a Backend Workflow endpoint receives Zendesk webhooks — but this requires a Bubble paid plan.

## The `/token` suffix that trips up every Zendesk integration in Bubble

Zendesk offers two authentication paths: OAuth (for building apps where end users log in with their Zendesk accounts) and Basic Auth with an API token (for server-to-server integrations). For Bubble, Basic Auth is the correct choice — but Zendesk's Basic Auth format has a specific requirement that catches out almost every developer the first time.

The username is NOT just your email address. It is your email address with `/token` appended: `ops@company.com/token`. Then the password field holds the API token string. These two values are combined as `email/token:api_token_string`, base64-encoded, and passed as `Basic [encoded_string]`.

Bubble's API Connector does not have a native Basic Auth mode that handles this format automatically. The solution is to pre-encode the credentials outside of Bubble (any online base64 encoder or a quick browser console command works) and store the resulting Base64 string as the value of the Authorization header — marked Private so it stays server-side.

The high-value Bubble use case for Zendesk is a bi-directional custom support portal: Bubble reads tickets from Zendesk via the API Connector, lets customers submit new tickets through a Bubble form, and receives real-time ticket status changes via Zendesk webhooks hitting a Bubble Backend Workflow endpoint.

## Before you start

- A Zendesk account with admin access to the Admin Center (for generating an API token and setting up webhooks)
- The API Connector plugin installed in your Bubble app (Plugins tab → Add plugins → search 'API Connector' by Bubble)
- A Bubble app on Starter plan or above if you plan to receive inbound Zendesk webhooks via Backend Workflows (Free plan cannot receive POST webhooks)
- Access to a base64 encoder (browser developer console or any online tool) to pre-encode your credentials

## Step-by-step guide

### 1. Generate a Zendesk API token and encode your credentials

In your Zendesk account, click the Admin Center icon (gear icon) in the left sidebar, then navigate to Apps and Integrations → APIs → Zendesk API. Toggle 'Token Access' to enabled if it is not already. Click 'Add API token,' give it a description like 'Bubble Integration,' and click 'Create.' Zendesk will show you the token once — copy it immediately as it cannot be retrieved again after closing this dialog.

Now construct your Basic Auth credential string. The format is:
```
email/token:your_api_token
```
For example, if your email is `ops@company.com` and your token is `abc123xyz`, the credential string is:
```
ops@company.com/token:abc123xyz
```
Base64-encode this string. You can do this in your browser's developer console (press F12 → Console tab) and type:
```
btoa('ops@company.com/token:abc123xyz')
```
This returns the base64 string you will use in the Authorization header. Prepend `Basic ` (with a space) to get the complete header value:
```
Basic b3BzQGNvbXBhbnkuY29tL3Rva2VuOmFiYzEyM3h5eg==
```

Store this full `Basic [encoded_string]` value securely — you will paste it into Bubble in the next step.

```
// Credential format: email/token:api_token_string
// Example:
// email: ops@company.com
// api token: abc123xyz789
// credential string: ops@company.com/token:abc123xyz789

// Base64 encode in browser console:
btoa('ops@company.com/token:abc123xyz789')
// Returns: 'b3BzQGNvbXBhbnkuY29tL3Rva2VuOmFiYzEyM3h5ejc4OQ=='

// Full Authorization header value:
// Basic b3BzQGNvbXBhbnkuY29tL3Rva2VuOmFiYzEyM3h5ejc4OQ==

// Important: /token is PART OF THE USERNAME, not separate
// Wrong: ops@company.com : api_token (omits /token suffix)
// Correct: ops@company.com/token : api_token
```

**Expected result:** You have a Zendesk API token and a pre-computed `Basic [base64_string]` value ready to paste into Bubble's API Connector.

### 2. Configure the Zendesk API Connector in Bubble

In your Bubble editor, go to Plugins → API Connector. Click 'Add another API' and name it `Zendesk`. Your Zendesk base URL includes your unique account subdomain — set it to `https://yoursubdomain.zendesk.com/api/v2`. Replace `yoursubdomain` with your actual Zendesk subdomain (the part before `.zendesk.com` in your account URL). If you do not know your subdomain, log into Zendesk and look at the URL in your browser's address bar.

Add a shared header for this API group:
- Header name: `Authorization`
- Header value: `Basic [your_base64_encoded_credentials_from_step_1]`
- Check 'Private' — this keeps the credentials server-side and out of browser DevTools

Add a second shared header:
- Header name: `Content-Type`
- Header value: `application/json`

Now add your first API call. Click 'Add another call' and name it `Get Tickets`. Set Method to GET and URL to `/tickets.json`. Add URL parameters:
- `status` (dynamic, optional) — filter by ticket status: open, pending, solved, closed
- `sort_by` = `created_at`
- `sort_order` = `desc`
- `per_page` = `50`

Set 'Use as' to Data. Click Initialize call — Bubble will call your Zendesk API and auto-detect the response shape. If successful, you will see the response fields including `tickets` (array), `count`, and `next_page`.

```
// Zendesk API Connector configuration:
{
  "api_name": "Zendesk",
  "base_url": "https://yoursubdomain.zendesk.com/api/v2",
  "shared_headers": {
    "Authorization": "Basic <your_base64_encoded_string>",  // mark Private
    "Content-Type": "application/json"
  }
}

// Call 1: Get Tickets
{
  "method": "GET",
  "url": "/tickets.json",
  "params": {
    "status": "<status_filter>",  // open | pending | solved | closed
    "sort_by": "created_at",
    "sort_order": "desc",
    "per_page": "50",
    "page[after]": "<cursor>"  // for cursor-based pagination (optional)
  }
}

// Response structure:
// {
//   "tickets": [ { "id": 1, "subject": "...", "status": "open", "requester_id": 123 } ],
//   "meta": { "has_more": true, "after_cursor": "abc123", "before_cursor": "xyz" },
//   "links": { "next": "https://...", "prev": "https://..." }
// }
```

**Expected result:** The Zendesk API Connector shows a successful Initialize call response with a `tickets` array. You can see the auto-detected response fields in the API Connector UI, including ticket id, subject, status, requester_id, and created_at.

### 3. Add ticket search and ticket creation calls

Now add two more API calls to handle search and new ticket creation:

Call 2 — Search Tickets: In the Zendesk API group, click 'Add another call.' Name it `Search Tickets`. Method: GET. URL: `/search.json`. Add a URL parameter `query` (dynamic) — Zendesk's search query string uses syntax like `type:ticket status:open subject:billing`. Note: the search endpoint returns results in a `results` array (not `tickets`), and includes a `result_type` field per item. When binding to a Repeating Group, use the `results` path. Set 'Use as' to Data and initialize with a test query like `type:ticket status:open`.

Call 3 — Create Ticket: Click 'Add another call.' Name it `Create Ticket`. Method: POST. URL: `/tickets.json`. Set body type to JSON. Body structure:
```json
{
  "ticket": {
    "subject": "<subject>",
    "comment": {
      "body": "<description>"
    },
    "requester": {
      "name": "<requester_name>",
      "email": "<requester_email>"
    },
    "priority": "<priority>",
    "tags": ["<tag1>", "<tag2>"]
  }
}
```
Mark each `<dynamic>` value as a parameter. Set 'Use as' to Action.

Call 4 — Update Ticket: Method: PUT. URL: `/tickets/<ticket_id>.json` (mark ticket_id as dynamic parameter). Body:
```json
{
  "ticket": {
    "status": "<new_status>",
    "comment": { "body": "<internal_note>", "public": false }
  }
}
```
Set 'Use as' to Action.

```
// Call 2: Search Tickets
{
  "method": "GET",
  "url": "/search.json",
  "params": {
    "query": "<search_query>"  // e.g., 'type:ticket status:open subject:billing'
  }
}
// Note: search response uses 'results' array, not 'tickets'
// Results also include result_type: "ticket" to filter non-ticket results

// Call 3: Create Ticket
{
  "method": "POST",
  "url": "/tickets.json",
  "body": {
    "ticket": {
      "subject": "<subject>",
      "comment": { "body": "<description>" },
      "requester": {
        "name": "<requester_name>",
        "email": "<requester_email>"
      },
      "priority": "<priority>",  // low | normal | high | urgent
      "type": "question",       // question | incident | problem | task
      "tags": ["bubble-portal"]
    }
  }
}

// Call 4: Update Ticket
{
  "method": "PUT",
  "url": "/tickets/<ticket_id>.json",
  "body": {
    "ticket": {
      "status": "<new_status>",     // open | pending | solved | closed
      "comment": {
        "body": "<note_text>",
        "public": false             // false = internal note, true = public reply
      }
    }
  }
}
```

**Expected result:** All four API Connector calls (Get Tickets, Search Tickets, Create Ticket, Update Ticket) are configured. Initialize calls for Get Tickets and Search Tickets return real ticket data. The Create and Update calls are set as Actions ready to be triggered from Bubble workflows.

### 4. Set up a Backend Workflow endpoint for inbound Zendesk webhooks

This step requires a Bubble Starter plan or above — Backend Workflows are not available on the Free plan. If you only need outbound calls (reading and creating tickets), skip this step.

In your Bubble editor, go to Backend Workflows in the left sidebar. If you do not see this section, your app is on the Free plan. Click 'Add workflow' and name it `Zendesk Webhook Receiver`. Toggle the 'Expose as a public API' switch to ON.

In the workflow, click 'Detect request data.' Zendesk will send a test payload to this endpoint — but first you need to copy the endpoint URL. Find the Backend Workflow URL at the top of the workflow editor — it looks like `https://yourapp.bubbleapps.io/api/1.1/wf/zendesk-webhook-receiver`. Note: if you see a URL ending in `/initialize`, that is only for schema detection — the production webhook URL does NOT include `/initialize`.

Go to your Zendesk Admin Center → Apps and Integrations → Webhooks → Create webhook. Set the endpoint URL to your Backend Workflow URL (the one WITHOUT `/initialize`). Set the request format to JSON and the request method to POST. Under 'Authentication,' if you want to verify webhook signatures, add a signing secret and store it as a Bubble app-level config or Custom State.

Back in Bubble, send a test event from Zendesk (Admin Center → Webhooks → your webhook → Actions → Test webhook). Click 'Detect' in the Backend Workflow to capture the incoming payload schema. Zendesk's webhook payload includes fields like `id`, `subject`, `status`, `priority`, `requester`, and `description`.

After detection, add workflow steps to process the incoming data — for example, create or update a Bubble 'Ticket' data record that mirrors the Zendesk ticket, then trigger additional automations.

```
// Zendesk webhook payload (typical ticket.created event):
{
  "id": 12345,
  "subject": "Billing question for account #987",
  "status": "new",
  "priority": "normal",
  "type": "question",
  "requester": {
    "id": 456789,
    "name": "Jane Customer",
    "email": "jane@customer.com"
  },
  "assignee_id": null,
  "tags": ["billing", "new-customer"],
  "created_at": "2026-07-10T12:00:00Z",
  "updated_at": "2026-07-10T12:00:00Z",
  "description": "Hi, I have a question about my last invoice..."
}

// Backend Workflow endpoint URL format:
// Production: https://yourapp.bubbleapps.io/api/1.1/wf/zendesk-webhook-receiver
// Detection:  https://yourapp.bubbleapps.io/api/1.1/wf/zendesk-webhook-receiver/initialize
// IMPORTANT: Register the production URL (without /initialize) in Zendesk
```

**Expected result:** The Backend Workflow endpoint is created and shows as 'Exposed as public API.' Zendesk's test webhook successfully triggers the workflow and the 'Detect' panel shows the incoming ticket fields (id, subject, status, requester, etc.).

### 5. Build the support portal UI and wire everything together

Now connect all the API calls to a Bubble UI for your support portal. Create a new Bubble page named `support-portal`.

For the ticket list view:
1. Add a Repeating Group. Set the data source to 'Get data from an external API → Zendesk → Get Tickets.' Pass the status filter from a Dropdown element ('All Tickets,' 'Open,' 'Pending,' 'Solved'). 
2. In each Repeating Group cell, show: a Text element for the ticket subject, a Text element for the status (use 'Conditional' tab to change its color based on status value), and a 'View Details' button.
3. Add a 'Load More' button below the Repeating Group. When clicked, update a Custom State `cursor` to the last response's `meta.after_cursor` value, then refresh the Repeating Group with the new cursor parameter.

For the ticket submission form:
1. Add a Popup or a separate section with text inputs for subject, description, and priority dropdown.
2. On 'Submit' button click: add a workflow action 'API Connector → Zendesk → Create Ticket' and map the form inputs to the ticket body parameters.
3. After successful submission, close the popup and show a confirmation message. Optionally refresh the Repeating Group to show the new ticket.

For the ticket detail view:
1. Use a state to store the selected ticket_id when a user clicks 'View Details.'
2. Show ticket subject, full description, status, and a message thread if you are using Zendesk comments. Create an 'Add Reply' form that calls a Comment creation action (POST /tickets/{id}/comments).

RapidDev's team has built custom Zendesk portal integrations for Bubble apps — if you want guidance on the data model and conversation history display, visit rapidevelopers.com/contact for a free scoping call.

```
// Repeating Group data source (pseudo-expression):
// Type of content: Zendesk API result
// Data source: Get data from external API → Zendesk - Get Tickets
//   status param: Dropdown_Status's value (if 'All' selected, leave blank)

// Custom State for pagination cursor:
// Name: pagination_cursor, Type: text
// Initial value: (blank)
// 'Load More' button workflow:
//   Step 1: Set State pagination_cursor = Repeating Group's last item's next_page cursor
//   Step 2: Refresh data for Repeating Group

// Submit Ticket button workflow:
// Action: API Connector → Zendesk → Create Ticket
//   subject: Input_Subject's value
//   description: Input_Description's value
//   requester_name: Current User's name
//   requester_email: Current User's email
//   priority: Dropdown_Priority's value
// On success: Show confirmation popup / Close submission form
// On error: Show error message from API response
```

**Expected result:** The support portal page shows a list of tickets fetched from Zendesk, filtered by status. Users can submit new tickets via the form, and the list updates to include the new ticket. The 'Load More' button loads the next page of results using cursor pagination.

## Best practices

- Always mark the Authorization header as Private in Bubble's API Connector. The base64-encoded credentials can be decoded by anyone who sees them in browser DevTools — Private headers stay server-side and never appear in network requests.
- Use cursor-based pagination (`page[after]` parameter) rather than offset pagination for ticket lists. Zendesk's cursor pagination is consistent and fast; offset pagination can miss tickets when new ones are added between page loads.
- Filter ticket searches by requester email when building customer portals to ensure each user sees only their own tickets. Without this filter, your portal exposes all company support data to any logged-in user.
- Cache ticket data in Bubble's database when displaying read-only ticket lists. Calling the Zendesk API on every page load burns Bubble WU and can hit Zendesk's rate limit on high-traffic portals. Schedule a Backend Workflow to sync tickets periodically instead.
- Apply Bubble privacy rules to any Bubble data type that stores Zendesk ticket data — especially internal notes or customer email addresses. Set 'Everyone else → no access' and restrict read access to the ticket owner or admin users.
- Use Zendesk's ticket type field to distinguish questions, incidents, problems, and tasks in your Bubble UI. Show different UI treatments per type (e.g., a progress tracker for problems, an expected resolution date for tasks).
- For inbound webhooks, validate the Zendesk webhook signing secret in your Backend Workflow before processing the event. This prevents unauthorized actors from sending fake ticket data to your Bubble endpoint.
- Test your integration on Zendesk's sandbox environment if available on your plan before connecting to your production Zendesk account. This prevents test data from polluting your live ticket queue.

## Use cases

### Custom white-labeled support portal

Replace Zendesk's default Help Center with a fully branded Bubble portal where customers can view their open tickets, submit new requests, and see status updates — all without knowing Zendesk is in the back end. The portal reads tickets filtered by the customer's email address and posts new tickets directly to Zendesk.

Prompt example:

```
Show a Repeating Group of tickets from the Zendesk API filtered by requester_email = Current User's email. Each row shows the ticket subject, status badge (color-coded: open=red, pending=yellow, solved=green), and a 'View Details' button. Include a 'Submit New Request' form that POSTs to Zendesk /tickets.
```

### Internal support dashboard for agents

Build an internal Bubble app for support agents that shows all open tickets, allows filtering by priority and assignee, and lets agents update ticket status and add internal notes — all through Zendesk's API. Faster than the native Zendesk UI for teams with a specific workflow.

Prompt example:

```
Create a Bubble page with a Dropdown for ticket status filter (open, pending, solved) and a Repeating Group bound to the Zendesk /tickets.json GET call with status filter parameter. Add a 'Close Ticket' button that sends a PUT request to update status to 'solved'.
```

### Real-time Slack notifications for new high-priority tickets

Use a Zendesk webhook pointing to a Bubble Backend Workflow endpoint. When Zendesk creates a high-priority ticket (priority = urgent or high), the webhook fires, Bubble's Backend Workflow receives the ticket data and immediately posts a message to a Slack channel with the ticket details.

Prompt example:

```
Backend Workflow receives Zendesk webhook POST, extracts ticket_id, subject, priority, and requester from the request body. If priority is 'urgent' or 'high', trigger an API Connector call to Slack Incoming Webhooks with a message: 'New urgent ticket #[id]: [subject] from [requester]'.
```

## Troubleshooting

### Initialize call returns 401 Unauthorized with 'Couldn't authenticate you' message

Cause: The most common cause is the missing `/token` suffix in the username portion of the credentials. If you encoded `ops@company.com:api_token` instead of `ops@company.com/token:api_token`, Zendesk treats it as a password authentication attempt and fails. Another common cause is extra whitespace in the encoded string.

Solution: Re-encode your credentials carefully. In your browser console, type: `btoa('your@email.com/token:your_api_token_here')` — note `/token` after your email and before the colon. Paste the result (prefixed with `Basic `) as the Authorization header value. Ensure there is no trailing whitespace in the token when you copy it from Zendesk Admin Center.

```
// Correct credential string before encoding:
// your@email.com/token:actual_api_token_value

// Wrong (missing /token — causes 401):
// your@email.com:actual_api_token_value

// Browser console to encode:
btoa('your@email.com/token:actual_api_token_value')
```

### 'There was an issue setting up your call' when initializing Get Tickets

Cause: The Initialize call needs a successful response from Zendesk to detect the response schema. If your credentials are correct but there are no tickets in your Zendesk account, the tickets array is empty and Bubble cannot detect the ticket fields. Alternatively, the subdomain in the base URL is incorrect.

Solution: First verify the subdomain in your API Connector base URL matches your Zendesk account URL exactly. Then create at least one test ticket in Zendesk's interface before running Initialize call. An empty `tickets` array makes it impossible for Bubble to auto-detect the field schema. If needed, add a query parameter `per_page=1` and use the search endpoint with a broad query to ensure a non-empty response.

### Search returns results for all tickets but I only want the current user's tickets

Cause: The Zendesk search API does not automatically filter by the logged-in portal user. Without a requester filter, it returns tickets from all users in your Zendesk account.

Solution: Include the requester's email in the search query parameter: set the `query` param to `type:ticket requester:` + Current User's email. Bubble's text composition in the API Connector parameter value field supports this: `type:ticket requester:<email_param>` where email_param is the current user's email passed as a dynamic parameter. This returns only tickets where the email address matches.

```
// Search query with requester filter:
// query = 'type:ticket requester:' + Current_User_email
// Example: type:ticket requester:jane@customer.com status:open
```

### Backend Workflow option does not appear in the Bubble editor sidebar

Cause: Backend Workflows (API Workflows) are only available on Bubble Starter plan or above. Free plan accounts do not have this feature, so the section is hidden entirely.

Solution: Upgrade your Bubble app to the Starter plan. If receiving Zendesk webhook events is not critical for your use case, you can skip the Backend Workflow and rely entirely on outbound API Connector calls (ticket reads and creates) which work on all plans.

### Zendesk webhook fires but Backend Workflow does not process the request

Cause: The webhook URL registered in Zendesk ends with `/initialize` — which is only used for schema detection in Bubble, not for production requests. Production events sent to the `/initialize` URL are ignored.

Solution: Update the webhook URL in Zendesk Admin Center → Webhooks to the production URL (without `/initialize`). The correct format is: `https://yourapp.bubbleapps.io/api/1.1/wf/your-workflow-name` — no `/initialize` suffix.

```
// Production URL (register this in Zendesk):
// https://yourapp.bubbleapps.io/api/1.1/wf/zendesk-webhook-receiver

// Detection URL (use only to detect request schema in Bubble):
// https://yourapp.bubbleapps.io/api/1.1/wf/zendesk-webhook-receiver/initialize
```

## Frequently asked questions

### Can I use Zendesk with Bubble's Free plan?

Yes, for outbound calls (reading and creating tickets). The API Connector works on all Bubble plans. However, receiving inbound Zendesk webhook events in a Backend Workflow requires the Bubble Starter plan or above. If you only need to read tickets and submit new ones from a Bubble form, the Free plan is sufficient.

### Why does Zendesk return 401 even though my API token looks correct?

The most common reason is the missing `/token` suffix in the username. Zendesk Basic Auth requires the username to be formatted as `your@email.com/token` — not just your email address. The `/token` suffix signals to Zendesk that you are using API token authentication rather than your account password. Re-encode your credentials with the correct format.

### How do I display Zendesk ticket comments or conversation history in Bubble?

Create a separate API Connector call: GET `/tickets/{ticket_id}/comments.json`. This returns all public and internal comments on a ticket in a `comments` array. Each comment includes `body`, `author_id`, `created_at`, and `public` (boolean). Display public comments in a Repeating Group inside your ticket detail view, and filter out internal notes for customer-facing portals by checking `public = true`.

### What is Zendesk's API rate limit?

Most Zendesk Suite plans allow 700 API requests per minute. This is generous for typical Bubble app usage patterns. However, if you have 70+ users simultaneously refreshing a ticket list every 10 seconds, each refresh generating one API call, you approach the limit. Use Bubble's 'Schedule API Workflow' as a polling layer and cache results in Bubble's database to reduce direct API calls per user.

### Can I let customers attach files when submitting tickets from Bubble?

Yes, but it requires a two-step process: first upload the file to Zendesk's upload endpoint (POST /uploads.json with the file content) to get an upload token, then include the token in the ticket creation body's `comment.uploads` array. Bubble can handle file uploads from a File Uploader element, but the upload-to-Zendesk step must go through a Backend Workflow since file handling requires the same Private auth credentials.

### How do I handle multiple Zendesk accounts (multi-tenant app)?

For multi-tenant apps where different customers have their own Zendesk accounts, you need to make the subdomain and credentials dynamic. Store each tenant's base64-encoded credentials and subdomain in a Bubble data type. In each API call, pass the current tenant's credentials as dynamic parameters rather than hardcoding them as shared header values. This requires more careful workflow design but is fully supported by Bubble's API Connector.

---

Source: https://www.rapidevelopers.com/bubble-integrations/zendesk
© RapidDev — https://www.rapidevelopers.com/bubble-integrations/zendesk
