# Zendesk

- Tool: FlutterFlow
- Difficulty: Intermediate
- Time required: 45 minutes
- Last updated: July 2026

## TL;DR

Connect FlutterFlow to Zendesk Support using the REST v2 API with a subdomain-specific base URL. The API token is Base64-encoded into a Basic auth header with a mandatory `/token` suffix in the username. For production, proxy the token through a Firebase Cloud Function rather than embedding it in the app bundle — and watch for the `ticket`-wrapped POST body that Zendesk requires for creating tickets.

## Building a Mobile Support Desk App with FlutterFlow and Zendesk

Zendesk's Support API v2 is one of the more approachable CRM-category APIs — it uses a clean RESTful noun structure (`tickets.json`, `users.json`), standard HTTP status codes, and a subdomain-based URL scheme (`https://yoursubdomain.zendesk.com/api/v2/`). For FlutterFlow integration, the main things to know before you write a single API call are: how authentication works and what the correct body format is for creating tickets.

Authentication uses HTTP Basic auth. You take your Zendesk user email, append the literal string `/token` (not just a slash), then a colon, then your API token: `youremail@company.com/token:APITOKEN`. You Base64-encode this entire string and put it in the `Authorization` header as `Basic [base64string]`. Omitting the `/token` suffix is the single most common error in Zendesk integrations — it causes a 401 that looks like a wrong password even when both the email and token are correct.

The API token itself is a server credential. For a prototype or internal tool, you can Base64-encode it and put it directly in the FlutterFlow API Group header — but for any app distributed to end users, the encoded token must not live in the app bundle (it can be decoded in seconds). A Firebase Cloud Function proxy holds the token server-side and forwards requests to Zendesk, keeping the credential secure.

Zendesk Support Team starts at around $19 per agent per month (verify current pricing at zendesk.com/pricing). The API rate limit is approximately 700 requests per minute (plan-dependent) shared across the organization, with a 429 response and `Retry-After` header when exceeded. For a mobile support app with a small team, this is generous — but ListViews that auto-poll on many devices simultaneously can approach it.

## Before you start

- A Zendesk account (trial or paid) with admin access to enable API tokens in Admin Center
- Your Zendesk subdomain (the part before .zendesk.com in your account URL)
- A Firebase project on the Blaze (pay-as-you-go) plan with Cloud Functions enabled (required for the production proxy pattern)
- A FlutterFlow project on at least the Starter plan to access API Calls and App State
- Basic familiarity with FlutterFlow's left-nav panels and API Calls section

## Step-by-step guide

### 1. Enable the Zendesk API token and compute the Basic auth header

In Zendesk, click the grid icon (top left) → Admin Center. In the left nav, go to 'Apps and integrations' → 'APIs' → 'Zendesk API'. On the Settings tab, toggle 'Token Access' to 'Enabled' and click 'Save'. Then click 'Add API token' — give it a description like 'FlutterFlow Integration' and click 'Create'. Copy the token string immediately; Zendesk only shows it once.

Now compute your Basic auth credential. The format is: `{your_email}/token:{api_token}`. For example, if your email is `admin@company.com` and your API token is `abc123`, the credential string is `admin@company.com/token:abc123`. Note the literal text `/token` between your email and the colon — this tells Zendesk you're authenticating with an API token rather than your password.

Base64-encode this string. You can do this in any browser's developer console: open DevTools → Console → type `btoa('admin@company.com/token:abc123')` → press Enter. Copy the resulting Base64 string. The final Authorization header value is `Basic [your_base64_string]`.

For a prototype or internal tool, this static Base64 value can go directly into the FlutterFlow API Group header. For a production app distributed to end users, continue to Step 2 (the Cloud Function proxy) — the Base64 value can be decoded by anyone who inspects the app bundle, so it must not be embedded in the app.

Write down three values for use in the remaining steps: your Zendesk subdomain, your agent email, and your API token.

```
// Compute Basic auth in browser DevTools Console:
// Paste this line, replace with your own values:
btoa('admin@company.com/token:YOUR_API_TOKEN_HERE')

// Result example: 'YWRtaW5AY29tcGFueS5jb20vdG9rZW46QVBJX1RPS0VO'
// Final header value: 'Basic YWRtaW5AY29tcGFueS5jb20vdG9rZW46QVBJX1RPS0VO'
```

**Expected result:** You have a Base64 string that encodes `email/token:APITOKEN`. Putting `Basic [base64string]` in an HTTP client like Postman against `https://yoursubdomain.zendesk.com/api/v2/tickets.json` returns a 200 with ticket data.

### 2. (Production) Deploy a Firebase Cloud Function proxy for the API token

If you are building an app for real end users — not just yourself or your team — the Zendesk API token must not be embedded in the app bundle. A Firebase Cloud Function acts as a thin proxy: FlutterFlow calls the function with the action and any input data, the function calls Zendesk with the embedded API token, and returns the result.

Open your Firebase project → Build → Functions. Create the function below. It accepts GET requests to list tickets and POST requests to create tickets, proxying both to Zendesk with the stored API token. Set the config with your Firebase CLI: `firebase functions:config:set zendesk.subdomain="yoursubdomain" zendesk.basic_auth="Basic YWRtaW5A..."` (use the full Base64 Basic auth header value computed in Step 1). Deploy with `firebase deploy --only functions` and copy the HTTPS trigger URL.

Once deployed, update your FlutterFlow API Group base URL to point to the Cloud Function URL instead of the Zendesk URL directly. The Cloud Function acts as a drop-in intermediary that strips the secret before it ever reaches the app.

For prototype builds (internal tools, personal use), skip this step and put the Base64 Basic auth header directly into the FlutterFlow API Group, keeping the security tradeoff clearly documented.

```
const functions = require('firebase-functions');
const axios = require('axios');

exports.zendeskProxy = functions.https.onRequest(async (req, res) => {
  res.set('Access-Control-Allow-Origin', '*');
  if (req.method === 'OPTIONS') {
    res.set('Access-Control-Allow-Methods', 'GET, POST');
    res.set('Access-Control-Allow-Headers', 'Content-Type');
    return res.status(204).send('');
  }

  const config = functions.config().zendesk;
  const zendeskBase = `https://${config.subdomain}.zendesk.com/api/v2`;
  const path = req.query.path || '/tickets.json';
  const url = zendeskBase + path;

  try {
    const response = await axios({
      method: req.method,
      url: url,
      headers: {
        'Authorization': config.basic_auth,
        'Content-Type': 'application/json'
      },
      data: req.body || undefined
    });
    return res.status(response.status).json(response.data);
  } catch (err) {
    const status = err.response?.status || 500;
    return res.status(status).json(err.response?.data || { error: 'Proxy error' });
  }
});
```

**Expected result:** Calling `https://your-function-url?path=/tickets.json` returns a 200 with Zendesk ticket data. The API token is not visible anywhere in the FlutterFlow project.

### 3. Create the Zendesk API Group in FlutterFlow

Open FlutterFlow, go to left nav → API Calls → '+ Add' → 'Create API Group'. Name it 'Zendesk'.

For the Base URL:
- If using the direct approach (prototype): `https://yoursubdomain.zendesk.com/api/v2`
- If using the Cloud Function proxy: your Firebase Cloud Function's HTTPS trigger URL

Add a shared header. For the direct approach: key `Authorization`, value `Basic [your_base64_auth_string]` (paste the full static value — it does not change). Add another header `Content-Type: application/json`.

For the proxy approach: no Authorization header needed — the Cloud Function adds it. Pass a query param `path` to the proxy to route requests to different Zendesk endpoints.

Now add individual API Calls:

1. 'getTickets' — Method GET, endpoint `/tickets.json?sort_by=created_at&sort_order=desc&per_page=25`. Parse response: `$.tickets` (array of tickets). In the Response & Test tab, paste a sample Zendesk ticket list response and click 'Generate JSON Paths'.

2. 'createTicket' — Method POST, endpoint `/tickets.json`. Body (Content-Type: application/json): wrap the ticket in the required `ticket` object with variables for `subject` and `body`. The POST body must be `{"ticket":{"subject":"[subject]","comment":{"body":"[body]"},"priority":"normal"}}`.

3. 'getTicketById' — Method GET, endpoint `/tickets/[ticketId].json`. Add a Variable named `ticketId` of type Integer. Parse `$.ticket` (singular — Zendesk uses singular for single-record responses and plural for lists).

In the Response & Test tab for `getTickets`, paste a real Zendesk API response and click 'Generate JSON Paths' to auto-create paths like `$.tickets[0].id`, `$.tickets[0].subject`, `$.tickets[0].status`.

```
{
  "method": "POST",
  "endpoint": "/tickets.json",
  "headers": {
    "Authorization": "Basic [base64AuthString]",
    "Content-Type": "application/json"
  },
  "body": {
    "ticket": {
      "subject": "[subject]",
      "comment": {
        "body": "[body]"
      },
      "priority": "normal",
      "tags": ["mobile-app"]
    }
  }
}
```

**Expected result:** The FlutterFlow API Calls panel shows a 'Zendesk' group with getTickets, createTicket, and getTicketById calls. Testing getTickets in the Response & Test tab returns a 200 with a `tickets` array.

### 4. Build a tickets ListView with pagination and a create-ticket form

With the API Group configured, build the support desk UI.

On the main page, drag a ListView onto the canvas. Select it → Backend Query → API Call → choose `getTickets` from the Zendesk group. Under Response JSON Path, enter `$.tickets`. Inside the ListView, add a Column widget. Add Text widgets bound to `$.subject` (ticket subject), `$.status` (use a Badge or Chip for status color-coding: green for 'open', gray for 'pending', blue for 'solved'), and `$.created_at` (formatted as a date). Add a secondary Text for `$.id` prefixed with '#' for the ticket number.

Zendesk paginates via a `next_page` URL field in the response root. The response includes `$.next_page` (a full URL string for the next page of results) and `$.meta.after_cursor` for cursor-based pagination. To add 'Load More', store the `next_page` value in a Page State String variable after the first fetch. Show a 'Load More' button at the bottom of the list; tapping it calls `getTickets` with the full `next_page` URL as the endpoint (override the base URL). Append the new results to the existing list in App State.

For the create-ticket form, add a new page 'NewTicketPage'. Add TextField widgets for Subject and Description (a multi-line TextField with max lines set to 5). Add a Submit button. In the button's Actions:
1. 'Backend/API Call' → `createTicket` from Zendesk group.
2. Map `subject` TextField text → `subject` variable.
3. Map `description` TextField text → `body` variable.
4. After success (status 201), show Snack Bar: 'Ticket #[$.ticket.id] created — we'll be in touch!' Navigate back.
5. After 400/422 error, show Alert Dialog with 'Check that all fields are filled in and try again'.

Zendesk returns 201 Created for new tickets with the full ticket object in the response body at `$.ticket.id`.

**Expected result:** The tickets ListView shows real Zendesk tickets in FlutterFlow Run mode. Submitting the create-ticket form produces a 201 response and the new ticket appears in your Zendesk account within seconds.

### 5. Handle authentication errors and 429 rate limiting

Two categories of errors need handling in your Zendesk FlutterFlow integration: authentication failures and rate-limit throttling.

Authentication errors (401 Unauthorized): the most common cause is omitting the `/token` suffix from the username part of the Basic auth credential. Double-check that your Base64 string encodes `email/token:APITOKEN` and not `email:APITOKEN`. If you rotate or regenerate the API token in Zendesk Admin Center, the old Base64 credential becomes invalid immediately — update the FlutterFlow header (or Firebase Cloud Function config) with the new Base64 value.

In your Action Flows, add a Conditional Action after API Calls that checks status code 401. Show an Alert Dialog: 'Authentication failed. Please contact your administrator.' This prevents a raw 401 error from showing to end users.

Rate limiting (429 Too Many Requests): Zendesk returns HTTP 429 with a `Retry-After` header when you exceed the rate limit (approximately 700 requests per minute, plan-dependent). FlutterFlow cannot read HTTP headers from an API Call response natively. The practical approach is to detect 429 status codes in Conditional Actions and show a brief waiting message: 'Too many requests — please wait a moment before trying again.' Avoid polling ticket lists faster than every 30 seconds on any single screen.

For web builds of your FlutterFlow app, Zendesk's API may block cross-origin browser requests with a CORS error ('XMLHttpRequest error'). This is why the Cloud Function proxy from Step 2 is valuable for web builds — the proxy sets CORS headers and the browser never makes a direct call to Zendesk's servers. For mobile builds (iOS/Android), CORS is not enforced.

If your team needs a custom mobile support app with full Zendesk integration and you'd prefer not to build the proxy layer yourself, RapidDev's team does exactly this — book a free scoping call at rapidevelopers.com/contact.

**Expected result:** When a 401 occurs, the app shows a clear error message rather than a crash. When a 429 occurs, the app shows a friendly 'please wait' message and does not retry immediately.

## Best practices

- Never embed the Base64-encoded API token directly in a FlutterFlow app intended for distribution to end users — the Base64 encoding provides no security and can be decoded instantly by anyone who decompiles the app.
- Always include the literal `/token` suffix in the Basic auth username — the format `email/token:APITOKEN` is Zendesk-specific and differs from standard Basic auth with a password.
- Wrap all ticket create/update POST bodies in the `ticket` object — `{"ticket":{...}}` — even for simple creates; a flat body returns 422 without this envelope.
- Use `$.tickets` (plural) as the JSON Path for list responses and `$.ticket` (singular) for single-record responses — mixing them causes empty data bindings in FlutterFlow widgets.
- Rate limit awareness: Zendesk's per-minute limit is org-wide, not per user or per app — implement load-on-demand rather than polling to keep well within the limit across all users.
- For pagination, use Zendesk's `next_page` URL rather than offset arithmetic — Zendesk's cursor-based pagination handles large ticket volumes more reliably and prevents duplicate results at page boundaries.
- Test your integration using a Zendesk trial account before configuring against a live production instance — this protects real customer ticket data during development and lets you freely create and delete test tickets.
- If you need webhook-triggered ticket updates in your FlutterFlow app (e.g. real-time notification when a ticket is updated), set up the Zendesk webhook to call a Firebase Cloud Function, which then updates a Firestore document that FlutterFlow can listen to via a Realtime Query.

## Use cases

### Mobile helpdesk app — view and respond to support tickets in the field

A FlutterFlow mobile app for support agents shows open tickets from Zendesk, sorted by creation date. Agents can tap a ticket to view its details and comments, and tap 'Reply' to add a comment — all without opening a browser tab. The app pulls from the Zendesk API in real time so the ticket list is always current.

Prompt example:

```
Build a Flutter mobile support desk app that fetches open tickets from Zendesk and displays them in a scrollable list with subject, requester, status, and creation date. Tapping a ticket shows the full description and comment thread. Agents can add a reply via a text field and submit button.
```

### Customer-facing ticket submission form — let users report issues in-app

A FlutterFlow app adds a 'Contact Support' screen where end users fill out a subject and description. Submitting the form creates a new Zendesk ticket with the user's email as the requester. The user sees the Zendesk ticket ID on the confirmation screen so they can reference it in follow-up communication.

Prompt example:

```
Build a Flutter in-app support form with fields for subject and description. When submitted, create a new Zendesk ticket via the REST API and display the resulting ticket ID and a thank-you message to the user.
```

### Operations dashboard — monitor ticket queue health on a tablet

A FlutterFlow tablet app shows a live summary of the support queue: total open tickets, tickets opened today, and a list of urgent-priority tickets. Managers can see queue health at a glance and tap an urgent ticket to view its status and assignee — then escalate by updating the priority from the app.

Prompt example:

```
Build a Flutter tablet dashboard showing the count of open Zendesk tickets, tickets created today, and a list of high-priority open tickets. Include the ability to change a ticket's priority from the detail view.
```

## Troubleshooting

### All Zendesk API calls return 401 Unauthorized even with a valid API token

Cause: The Basic auth username is missing the required `/token` suffix. The correct format is `email/token:APITOKEN` — omitting the literal `/token` causes Zendesk to treat the credential as a password, which fails.

Solution: Recompute the Base64 string with the correct format. In a browser console, run `btoa('youremail@domain.com/token:YOUR_API_TOKEN')`. The `/token` part is literal text — not related to any URL path. Update the Authorization header in the FlutterFlow API Group (or Firebase Cloud Function config) with the corrected Base64 value prefixed by `Basic `.

### POST to `/tickets.json` returns 422 Unprocessable Entity

Cause: The POST body is not wrapped in the required `ticket` object. Sending a flat JSON body (e.g. `{"subject":"...","comment":{"body":"..."}}`) without the outer `ticket` key causes a 422 validation error.

Solution: Ensure the POST body structure is `{"ticket":{"subject":"[subject]","comment":{"body":"[body]"}}}`. The outer `ticket` key is mandatory. Also verify that the `comment` field is present and contains a `body` key — Zendesk requires at least a comment body on all new tickets.

### Tickets ListView is empty — no tickets appear in FlutterFlow Run mode

Cause: The JSON Path for the ListView is set to the wrong path. Zendesk wraps list responses in `$.tickets` (plural), while single-record responses use `$.ticket` (singular).

Solution: In the FlutterFlow API Call → Response & Test tab, paste a real Zendesk response and click 'Generate JSON Paths'. Set the ListView's Response JSON Path to `$.tickets` (with the 's'). Confirm the API call is returning data by checking the Response & Test tab — a 200 with no records means there are no open tickets in your Zendesk account, not a path issue.

### FlutterFlow web build shows 'XMLHttpRequest error' when calling Zendesk

Cause: Zendesk's API does not include CORS headers that allow cross-origin browser requests from arbitrary origins. Browser-based (web) FlutterFlow builds are blocked by the browser's CORS enforcement.

Solution: Use the Firebase Cloud Function proxy from Step 2 instead of calling Zendesk's API directly from the browser. The Cloud Function runs on a server (not in the browser), so CORS does not apply. Set the FlutterFlow API Group base URL to the Cloud Function URL rather than the Zendesk API URL. Mobile (iOS/Android) builds are not affected by CORS.

## Frequently asked questions

### Does FlutterFlow have a native Zendesk connector?

No. FlutterFlow does not have a built-in Zendesk integration. You connect using the API Calls panel to configure the Zendesk REST v2 endpoints manually. The integration is straightforward compared to OAuth-heavy CRMs, but the API token should be stored in a Firebase Cloud Function proxy for any production app.

### Why do I need the `/token` suffix in the Zendesk username?

Zendesk supports two Basic auth modes: password authentication (`email:password`) and token authentication (`email/token:apitoken`). The `/token` suffix tells Zendesk's API server to validate the credential against an API token rather than checking a user password. Without it, Zendesk rejects the request with 401 even when the API token itself is correct.

### Can I use OAuth instead of an API token for Zendesk?

Yes. Zendesk supports OAuth 2.0 via an OAuth application in Admin Center → Apps and integrations → APIs → OAuth Clients. OAuth is better for apps where each user authenticates with their own Zendesk account. API tokens authenticate as a single admin account. For an internal ops tool where all users share one Zendesk agent identity, an API token is simpler. For a multi-tenant app where different users need different access levels, OAuth is the correct choice.

### How do I assign a ticket to a specific Zendesk agent from FlutterFlow?

Include the `assignee_id` field in the POST body when creating a ticket, or use a PATCH request to `/tickets/[id].json` with `{"ticket":{"assignee_id": 12345}}`. The `assignee_id` is the Zendesk user ID of the agent, available via the `GET /users.json` endpoint. You can pre-fetch the list of agents and display them in a dropdown in your FlutterFlow app to let dispatchers assign tickets.

### How does Zendesk pagination work and how do I implement 'Load More' in FlutterFlow?

Zendesk uses cursor-based pagination. The first page response includes a `$.next_page` field containing a complete URL for the next page (e.g. `https://yoursubdomain.zendesk.com/api/v2/tickets.json?page%5Bafter%5D=...`). Store this URL in Page State and use it as the endpoint for the next API call when the user taps 'Load More'. Continue until `$.next_page` is null, which signals the last page.

### Can I receive real-time ticket updates in my FlutterFlow app?

FlutterFlow does not support native WebSocket listeners for Zendesk events. For real-time updates, the recommended pattern is: configure a Zendesk webhook (Admin Center → Objects and rules → Business rules → Webhooks) that fires on ticket updates, pointing to a Firebase Cloud Function. The Cloud Function writes the update to a Firestore document. Your FlutterFlow app uses a Firebase Realtime Query to listen for changes to that document and refreshes the ticket list when the document updates.

---

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