Connect FlutterFlow to Jira using a FlutterFlow API Call with HTTP Basic authentication (your Atlassian email plus an API token from id.atlassian.com). Create an API Group pointed at your-domain.atlassian.net/rest/api/3, POST JQL queries to /search to fetch issues, and bind the response to a ListView — but flatten Jira's Atlassian Document Format description fields with a Custom Function before binding.
| Fact | Value |
|---|---|
| Tool | JIRA |
| Category | Productivity |
| Method | FlutterFlow API Call |
| Difficulty | Intermediate |
| Time required | 60 minutes |
| Last updated | July 2026 |
Build a Mobile Jira Issue Dashboard in FlutterFlow
Jira is the default issue tracker for most development and product teams, but its mobile web experience can feel heavy on smaller screens. Building a lightweight mobile Jira front end in FlutterFlow lets your team view, filter, and create issues in a native mobile interface without switching to a browser. With JQL (Jira Query Language), you can filter by project, assignee, sprint, or status and display results in a clean ListView tailored to your team's workflow.
Jira Cloud's REST API v3 is included on all plan tiers including Free (up to 10 users). Authentication uses HTTP Basic where your Atlassian account email is the username and an API token from id.atlassian.com is the password — not your login password. The API token is scoped to your account, so it has access to every project you can see in Jira. This makes it a sensitive credential: treat it the same way you'd treat a password.
One Jira-specific challenge you'll encounter: issue descriptions and comments come back as Atlassian Document Format (ADF) — a deeply nested JSON structure, not a plain string. Binding the description field directly to a Text widget will show [object Object] or nothing. You'll need a short Custom Function to flatten the ADF content array into a readable string before binding it to the UI.
Integration method
FlutterFlow connects to Jira Cloud via the Atlassian REST API v3 using HTTP Basic authentication where your Atlassian email serves as the username and an API token serves as the password. You create an API Group with your instance's base URL and a shared Authorization header, then POST JQL queries to the /search endpoint to fetch and filter issues. Rich-text description fields require a Custom Function to convert Atlassian Document Format JSON into readable strings.
Prerequisites
- A Jira Cloud account (any plan tier including Free) with access to the projects you want to display
- An Atlassian API token generated at id.atlassian.com/manage-profile/security/api-tokens
- Your Jira Cloud instance subdomain (e.g., yourcompany from yourcompany.atlassian.net)
- A FlutterFlow project with the API Calls panel available
- A Firebase project for the optional issue-creation Cloud Function proxy
Step-by-step guide
Generate your Atlassian API token
Go to id.atlassian.com/manage-profile/security/api-tokens in your browser. Click Create API token, give it a descriptive label (e.g., 'FlutterFlow integration'), and click Create. Copy the token immediately — Atlassian only shows it once. If you lose it, you'll need to revoke it and generate a new one. The token serves as the password in HTTP Basic authentication. Your username is your full Atlassian account email address. This is different from Jira Server/Data Center which uses a username field — in Jira Cloud, it's always your email. Do not confuse the API token with your Jira login password. They are separate credentials. The API token is safe to use in read-only API calls (stored in App Constants), but you should still treat it carefully because it has the same permissions as your account — it can read and write any project you can access. Also note your Jira Cloud subdomain. If your Jira URL is yourcompany.atlassian.net, your subdomain is yourcompany. You'll need this to build the base URL in the next step.
Pro tip: Copy the API token immediately after generating it. Atlassian only shows it once and you cannot retrieve it later — you would need to generate a new one.
Expected result: You have your Atlassian API token and your Jira Cloud subdomain ready to use.
Store credentials and domain as App Constants
In FlutterFlow, click the Settings icon in the left sidebar → App Values (App Constants). You'll create two constants: First constant: JIRA_AUTH. This is the Base64-encoded Basic auth header value. Compute it by Base64-encoding the string youremail@company.com:your_api_token (your Atlassian email, a colon, then your API token). Use any online Base64 encoder. The App Constant value should be: Basic [your base64 result] — include the word 'Basic' and a space before the encoded string. Second constant: JIRA_DOMAIN. Set this to your Jira subdomain only (e.g., yourcompany, without .atlassian.net). You'll use this when constructing the base URL so that switching between staging and production Jira instances is as simple as changing one constant. By storing these as App Constants (rather than typing them directly in API Call headers), you make the integration easier to maintain. If you need to rotate the API token, you update the JIRA_AUTH constant once and all API calls pick up the new value without any configuration changes.
1// App Constants to create in FlutterFlow → Settings → App Values2// JIRA_AUTH: "Basic [Base64(email:api_token)]"3// Example: "Basic amlyYUB5b3VyY29tcGFueS5jb206eW91cl9hcGlfdG9rZW4="4// JIRA_DOMAIN: "yourcompany"5// (just the subdomain, no .atlassian.net)Pro tip: Include 'Basic ' (with a trailing space) in the constant value so you can reference it directly as the Authorization header value.
Expected result: Two App Constants — JIRA_AUTH and JIRA_DOMAIN — are saved in your FlutterFlow project.
Create the Jira API Group in FlutterFlow
Click API Calls in the left navigation sidebar of FlutterFlow. Click + Add → Create API Group. Name the group Jira. Set the Base URL to https://{{JIRA_DOMAIN}}.atlassian.net/rest/api/3. This uses your App Constant to make the domain configurable — but note that FlutterFlow App Constants in Base URLs may need to be entered as a static string rather than a variable placeholder in some FlutterFlow versions. If the {{JIRA_DOMAIN}} substitution doesn't work in the base URL, type the full URL directly: https://yourcompany.atlassian.net/rest/api/3. Add the group-level headers: - Authorization: {{JIRA_AUTH}} (this uses the App Constant you created) - Accept: application/json - Content-Type: application/json All child API Calls inside this group will inherit these headers automatically. You won't need to set the Authorization header per-call. Note: if you also need to call Jira's Agile API (for sprint data), that lives at a different base path: /rest/agile/1.0/. Create a second API Group (JiraAgile) with base URL https://yourcompany.atlassian.net/rest/agile/1.0 for those calls rather than adding them to the same group.
1// API Group configuration reference2// Group name: Jira3// Base URL: https://yourcompany.atlassian.net/rest/api/34// Headers (inherited by all child calls):5// Authorization: {{JIRA_AUTH}} (Basic Base64(email:api_token))6// Accept: application/json7// Content-Type: application/jsonPro tip: If you need sprint and board data, create a separate API Group named JiraAgile with base URL https://yourcompany.atlassian.net/rest/agile/1.0 — those endpoints are a completely separate path from the core REST API.
Expected result: A Jira API Group appears in the API Calls panel with the base URL and shared Authorization, Accept, and Content-Type headers configured.
Add the POST /search call with a JQL body
Inside the Jira API Group, click Add API Call. Name it SearchIssues. Set the method to POST and the endpoint to /search. Jira's search endpoint accepts a JSON body with your JQL query, not URL parameters. Go to the Body tab in the API Call editor. Set the body type to JSON and enter a body with the following structure: {"jql": "{{jqlQuery}}", "maxResults": 50, "fields": ["summary", "status", "assignee", "description", "priority", "issuetype"]}. Now go to the Variables tab and add a variable named jqlQuery with type String and a default value like project = YOUR_PROJECT_KEY ORDER BY updated DESC. This lets you pass different JQL queries from different pages in your app without creating multiple API Calls. Go to the Response & Test tab. Paste a sample Jira search response (a JSON object with an 'issues' array). Click Generate from Response to create JSON Paths. Key paths to use: - Issue key: $.issues[*].key - Summary: $.issues[*].fields.summary - Status: $.issues[*].fields.status.name - Priority: $.issues[*].fields.priority.name - Assignee: $.issues[*].fields.assignee.displayName Do NOT try to bind $.issues[*].fields.description directly — the description is ADF JSON, not a string. You'll handle that in the next step. Click Test API Call. If Jira returns issues, you'll see them in the Response panel. A 401 means your Basic auth credentials are wrong. A 403 means the API token doesn't have access to the project. A 400 with 'The value ... does not exist for the field project' means your project key is incorrect.
1// POST /search request body2{3 "jql": "{{jqlQuery}}",4 "maxResults": 50,5 "startAt": 0,6 "fields": [7 "summary",8 "status",9 "assignee",10 "description",11 "priority",12 "issuetype",13 "created",14 "updated"15 ]16}1718// Sample response structure (paste into Response & Test for JSON Path generation)19{20 "total": 42,21 "issues": [22 {23 "id": "10001",24 "key": "ABC-123",25 "fields": {26 "summary": "Fix login button on mobile",27 "status": { "name": "In Progress" },28 "priority": { "name": "High" },29 "assignee": { "displayName": "Jane Doe" },30 "issuetype": { "name": "Bug" },31 "description": {32 "type": "doc",33 "version": 1,34 "content": [35 {36 "type": "paragraph",37 "content": [38 { "type": "text", "text": "Steps to reproduce..." }39 ]40 }41 ]42 }43 }44 }45 ]46}Pro tip: Use the 'fields' array in the request body to request only the fields you need — Jira's full issue response can be very large, and omitting unused fields speeds up the response significantly.
Expected result: The SearchIssues API Call returns a JSON response with an 'issues' array. JSON Paths for key, summary, status, assignee, and priority are generated successfully in the Response & Test tab.
Write a Custom Function to flatten ADF description fields
Jira's description and comment fields use Atlassian Document Format (ADF) — a rich-text JSON structure, not a plain string. The format wraps text inside nested content arrays: { "type": "doc", "content": [{ "type": "paragraph", "content": [{ "type": "text", "text": "your actual text" }] }] }. If you bind $.issues[*].fields.description directly to a Text widget, FlutterFlow will try to display the raw JSON object, which either shows nothing or shows '[object]'. You need a Custom Function to extract just the text nodes. In FlutterFlow, go to Custom Code in the left nav → + Add → Function. Name it flattenADF. Set the return type to String. Add one input parameter named adfJson of type dynamic (or JSON/String). Paste the Dart function below. It recursively walks the ADF content tree, extracts all text nodes, and returns them joined with newlines. This gives you a readable plain-text version of any ADF field. After creating the function, in your issue detail page, add a Text widget for the description. Set its value via Set from Variable → Custom Function → flattenADF, passing the raw description JSON Path as the adfJson argument. Note: Custom Functions in FlutterFlow are synchronous Dart code — no async, no pub.dev dependencies needed. The flattenADF function below is self-contained.
1// Custom Function: flattenADF2// Add via Custom Code → + Add → Function in FlutterFlow3// Return type: String4// Parameter: adfJson (dynamic)56String flattenADF(dynamic adfJson) {7 if (adfJson == null) return '';8 9 // If it's already a string, return as-is10 if (adfJson is String) return adfJson;11 12 final StringBuffer buffer = StringBuffer();13 14 void extractText(dynamic node) {15 if (node == null) return;16 17 if (node is Map) {18 final type = node['type'];19 final text = node['text'];20 21 // Text nodes contain the actual content22 if (type == 'text' && text != null) {23 buffer.write(text);24 return;25 }26 27 // Add newline after paragraph/heading/listItem blocks28 if (type == 'paragraph' || type == 'heading' || type == 'listItem') {29 final content = node['content'];30 if (content is List) {31 for (final child in content) {32 extractText(child);33 }34 }35 buffer.write('\n');36 return;37 }38 39 // Recurse into content arrays for other node types40 final content = node['content'];41 if (content is List) {42 for (final child in content) {43 extractText(child);44 }45 }46 } else if (node is List) {47 for (final item in node) {48 extractText(item);49 }50 }51 }52 53 extractText(adfJson);54 return buffer.toString().trim();55}Pro tip: This Custom Function is synchronous and has no package dependencies, so it works in FlutterFlow's preview (Test Mode) without needing a device or APK build.
Expected result: A flattenADF Custom Function is available in your FlutterFlow project. Binding it to a description field on your issue detail page displays readable text instead of raw JSON.
Add issue creation via a Firebase Cloud Function proxy
Read operations (GET, POST /search) are safe with credentials in App Constants because the data flows one way and the worst case of credential exposure is read access to your Jira projects. However, creating issues (POST /rest/api/3/issue) is a write operation and sends your API token in the client bundle where it can be extracted. For security, route issue creation through a Firebase Cloud Function. The Cloud Function stores your Atlassian email and API token in server-side environment variables, calls the Jira API on behalf of the app, and returns only the new issue key to the FlutterFlow client. In your FlutterFlow project, create a new API Group named JiraProxy with the base URL pointing to your Firebase Cloud Function endpoint (e.g., https://us-central1-your-project.cloudfunctions.net/createJiraIssue). Add a CreateIssue POST call inside it. The body accepts the issue fields your form collects: summary, description, project key, and issue type. No Authorization header goes in this FlutterFlow API Call — the Cloud Function handles authentication internally. In your create-issue form page, wire the Submit button action to Backend/API Call → JiraProxy → CreateIssue, passing the form field values as body parameters. On success, navigate back to the issue list and trigger a refresh. Also handle the rate limit: Jira Cloud allows approximately 300 requests per minute per user token. If your app polls for issue updates on a timer, add sufficient delay between polls (at minimum 10-20 seconds) to avoid hitting the limit. The API returns a 429 response with a Retry-After header when the limit is exceeded.
1// Firebase Cloud Function: createJiraIssue/index.js2const functions = require('firebase-functions');3const fetch = require('node-fetch');45exports.createJiraIssue = functions.https.onRequest(async (req, res) => {6 res.set('Access-Control-Allow-Origin', '*');7 res.set('Access-Control-Allow-Methods', 'POST, OPTIONS');8 res.set('Access-Control-Allow-Headers', 'Content-Type');910 if (req.method === 'OPTIONS') {11 res.status(204).send('');12 return;13 }1415 const JIRA_EMAIL = process.env.JIRA_EMAIL;16 const JIRA_TOKEN = process.env.JIRA_API_TOKEN;17 const JIRA_DOMAIN = process.env.JIRA_DOMAIN;18 const basicAuth = Buffer.from(`${JIRA_EMAIL}:${JIRA_TOKEN}`).toString('base64');1920 const { summary, description, projectKey, issueType } = req.body;2122 try {23 const response = await fetch(24 `https://${JIRA_DOMAIN}.atlassian.net/rest/api/3/issue`,25 {26 method: 'POST',27 headers: {28 'Authorization': `Basic ${basicAuth}`,29 'Accept': 'application/json',30 'Content-Type': 'application/json'31 },32 body: JSON.stringify({33 fields: {34 project: { key: projectKey },35 summary: summary,36 description: {37 type: 'doc',38 version: 1,39 content: [{40 type: 'paragraph',41 content: [{ type: 'text', text: description || '' }]42 }]43 },44 issuetype: { name: issueType || 'Task' }45 }46 })47 }48 );49 const data = await response.json();50 res.status(response.status).json(data);51 } catch (error) {52 res.status(500).json({ error: error.message });53 }54});Pro tip: When creating issues via the Jira API, the description must be sent in ADF format — not as a plain string. The Cloud Function above wraps the plain text from your form into a minimal ADF paragraph structure.
Expected result: A Firebase Cloud Function handles issue creation, the FlutterFlow create-issue form submits successfully, and the new issue key is returned for display or navigation.
Common use cases
Mobile sprint board for a development team
Build a FlutterFlow app that lets developers see their current sprint's issues on their phone. The app posts a JQL query like 'project = ABC AND sprint in openSprints() AND assignee = currentUser()' to /rest/api/3/search and displays issue key, summary, and status in a list. Developers can tap an issue to see the description and comments.
Show all open Jira issues assigned to the current user in the active sprint for project ABC, with issue key, summary, and status.
Copy this prompt to try it in FlutterFlow
QA bug tracker mobile companion
Build a mobile app for a QA team that surfaces all open bugs in a given Jira project. The app queries for issues with type = Bug AND status != Done, ordered by priority. Testers can see bug summaries grouped by priority and navigate to detail views that show the bug description (flattened from ADF) and steps to reproduce.
Display all open bugs in Jira project XYZ ordered by priority (highest first), showing issue key, summary, and priority icon.
Copy this prompt to try it in FlutterFlow
Client-facing project status app
Build a read-only status dashboard for clients showing the progress of their project. The app fetches all issues in a designated client-facing Jira project, groups them by status (To Do, In Progress, Done), and shows completion percentages. A custom color scheme maps status to visual badges without exposing any internal Jira details.
Show a project completion dashboard for Jira project CLIENT-001 with issue counts grouped by status and an overall percentage done.
Copy this prompt to try it in FlutterFlow
Troubleshooting
401 Unauthorized on all API calls
Cause: The HTTP Basic auth credentials are incorrect. In Jira Cloud, the 'username' must be your Atlassian account email address, and the 'password' must be an API token — not your login password. Using your actual login password or a Jira username (from older server versions) will fail on Jira Cloud.
Solution: Go to id.atlassian.com/manage-profile/security/api-tokens, revoke the old token, and generate a new one. Re-encode email:new_api_token using Base64 and update the JIRA_AUTH App Constant in FlutterFlow. Make sure the constant value starts with 'Basic ' (the word Basic followed by a space).
Description field shows '[object]' or empty text in the UI
Cause: Jira's description field is Atlassian Document Format (ADF) — a nested JSON object, not a plain string. Binding it directly to a Text widget renders the JSON object reference instead of the text content.
Solution: Create the flattenADF Custom Function described in step 5 and bind the description Text widget through the Custom Function rather than directly from the JSON Path. Pass the raw description JSON Path as the adfJson parameter.
JQL query returns 400 Bad Request with 'does not exist for the field project'
Cause: The project key in the JQL query is incorrect, misspelled, or refers to a project the API token's account doesn't have access to. Jira project keys are case-sensitive uppercase strings (e.g., ABC, MYPROJECT).
Solution: Open Jira in a browser and check the project key from the project URL (it appears after /projects/ in the path). Update your JQL query with the correct key. Also confirm that the Atlassian account associated with the API token has at least Browse permission on the project.
429 Too Many Requests during normal usage
Cause: Jira Cloud's rate limit is approximately 300 requests per minute per user token. Apps that poll for updates frequently or fan out searches across many projects can hit this limit quickly.
Solution: Add caching: store the last fetch timestamp and issue list in App State. Only re-fetch if more than 30-60 seconds have passed since the last call. Respect the Retry-After header in 429 responses — parse its value and wait that many seconds before retrying. Avoid polling on a tight loop; use explicit refresh triggers (a pull-to-refresh gesture) instead.
Best practices
- Store the Base64-encoded auth string and Jira domain as App Constants so you can update credentials in one place without touching individual API Call configurations.
- Always specify the 'fields' array in your /search POST body to request only the fields you display — omitting it returns the full issue object which can be very large and slow.
- Never bind ADF description or comment fields directly to Text widgets; always pass them through the flattenADF Custom Function first.
- Create a separate JiraAgile API Group (base URL: /rest/agile/1.0) for sprint and board data — mixing it with the core REST API group leads to confusing endpoint errors.
- Proxy issue creation (POST /issue), issue transitions (POST /issue/{key}/transitions), and comment creation through a Firebase Cloud Function to prevent the API token from appearing in the compiled app.
- Cache the issue list in App State and refresh only on explicit user action (pull-to-refresh) to stay well within the ~300 req/min rate limit.
- Display the issue key (e.g., ABC-123) prominently in your UI — it's the universal Jira identifier that team members use in conversation and is required for deep-linking to the Jira web interface.
- Handle 403 responses separately from 401 — a 403 means the token is valid but the account lacks permission on that project, while a 401 means the credentials themselves are wrong.
Alternatives
Choose Asana if your team is non-technical and prefers a task-focused UI without JQL — Asana's REST API uses Bearer tokens and simpler flat task objects without ADF rich-text parsing.
Choose Teamwork if you work with external clients on project health and need milestone/time-log tracking — Teamwork's API is simpler with plain JSON and no query language.
Frequently asked questions
Does FlutterFlow have a native Jira connector?
No. There is no native Jira integration in FlutterFlow's Settings & Integrations panel. The integration is built manually using FlutterFlow's API Calls panel with an API Group configured for HTTP Basic authentication pointing at your Jira Cloud instance. This tutorial covers the full setup including JQL search, ADF description handling, and a Cloud Function for write operations.
What is Atlassian Document Format and why does it matter for FlutterFlow?
Atlassian Document Format (ADF) is a JSON-based rich-text format Jira uses for description and comment fields. Instead of returning a plain string like 'Steps to reproduce...', the API returns a nested object like {"type":"doc","content":[...]}. FlutterFlow's Text widgets can only display strings, not JSON objects, so you need the flattenADF Custom Function from step 5 to extract the plain text before binding it to a widget.
Can I connect to Jira Server or Data Center, not Jira Cloud?
Yes, but with differences. Jira Server and Data Center use a different API base path (typically https://your-server-url/rest/api/2) and support Basic auth with your actual username rather than email. API token authentication is Jira Cloud-specific. The steps in this tutorial apply to Jira Cloud; for Jira Server, adjust the base URL and use your username instead of email in the auth header.
My app works in FlutterFlow Test Mode but breaks on the published web version — why?
FlutterFlow's Test Mode routes API calls through FlutterFlow's own proxy servers, bypassing CORS enforcement. When you publish to web, the browser makes direct calls to atlassian.net which may block certain cross-origin requests. The solution is to route calls through a Firebase Cloud Function for any endpoint that encounters CORS issues on the published web build.
How do I implement pagination for large Jira projects with hundreds of issues?
Jira's /search endpoint supports startAt and maxResults parameters. Add a startAt variable to your SearchIssues API Call (default: 0). To implement pagination in FlutterFlow, store the current page offset in App State and increment it by maxResults each time the user reaches the end of the list or taps a 'Load more' button. The total field in the response tells you how many issues match the query so you can show page indicators.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation