02 Get your data in /Integrations
Jira Service Management
Send Jira Service Management requests and their public comments to Dimension Labs with the universal API.
- JS Jira Service ManagementTickets, chats, calls, reviews
- ⟳ Your jobPull new · map fields · every 15 min
- { } Universal APIPOST /track · /trackhistorical
- Dimension LabsRead in full · enriched daily
This brings in each Jira Service Management request as one conversation: the customer’s original request and every public comment on it, from the customer and from your agents. Internal comments stay out. You run it on your side: a small script pulls from the Jira Cloud REST APIs and posts each message to the universal API.
What you need
Section titled “What you need”- A Jira Cloud site with one or more service projects, and their project keys (for example
HELP). - An Atlassian account with agent access to those projects. The script sees only what this account can see: Jira needs the Browse projects permission to return an issue, and permission to view the request to return its comments.
- An API token for that account.
- A Dimension Labs API key for a new Universal API source. See Your API key.
1. Get API access in Jira Service Management
Section titled “1. Get API access in Jira Service Management”- Signed in as the integration account, go to id.atlassian.com/manage-profile/security/api-tokens.
- Select Create API token, give it a name that says what it does, and pick an expiration date. Tokens last 1 to 365 days, so note when to replace it.
- Select Create, then Copy to clipboard. You can’t see the token again later.
- Authenticate with Basic auth: the account’s email address and the token, against your site, for example
https://your-site.atlassian.net(manage API tokens).
Use the plain Create API token option. A token with scopes calls a different base URL (api.atlassian.com/ex/jira/{cloudId}) and this guide doesn’t cover it.
2. Pull new requests
Section titled “2. Pull new requests”Find requests that changed since your cursor with enhanced JQL search, GET /rest/api/3/search/jql. The older GET /rest/api/3/search is being removed: don’t build on it.
curl -u "$JSM_EMAIL:$JSM_API_TOKEN" -G \ "https://your-site.atlassian.net/rest/api/3/search/jql" \ --data-urlencode 'jql=project in ("HELP") AND updated >= 1760313600000 ORDER BY updated ASC' \ --data-urlencode 'fields=summary,description,reporter,created,status,priority,issuetype,labels' \ --data-urlencode 'maxResults=100'- A number without quotes in a date clause is read as milliseconds since 1970, so the cursor needs no time zone. A quoted date such as
"2026-10-01 09:00"is read in the account’s Jira time zone. - The query must be bounded.
project in (...)does that. - Each page holds
issuesand, unless it is the last page, anextPageToken. Pass it back asnextPageTokenfor the next page. descriptioncomes back in Atlassian Document Format (JSON), not plain text. The code below turns it into text.
For each request, get the public comments from the Jira Service Management API: GET /rest/servicedeskapi/request/{issueIdOrKey}/comment?public=true&internal=false. Each comment has a plain-text body, a public flag, author.accountId and created.epochMillis. Page with start and limit (default 50) until isLastPage is true. To tell customers from agents, also read the request’s participants with GET /rest/servicedeskapi/request/{issueIdOrKey}/participant.
Rate limits, per Atlassian: API token traffic is governed by burst (per-second) limits. Going over returns 429 with a Retry-After header in seconds (rate limiting).
3. Map the fields
Section titled “3. Map the fields”| Dimension Labs | Jira Service Management | Notes |
|---|---|---|
text | summary and description for the first message; comment body after that | Only comments returned with public=true&internal=false. Internal comments are never requested. |
userId | issue reporter.accountId | Atlassian’s account id for the customer, on every message of the request. Account ids can contain a colon: the code replaces it with -. |
incoming | comment author.accountId is the reporter or a participant | The request itself is incoming. Comments from anyone else (your agents) are outgoing. |
sessionId | issue key | Prefix it, for example jsm-HELP-123. |
conversationId | issue key | Same value as sessionId. |
dimensionlabs_timestamp | issue created; comment created.epochMillis | Convert with toMillis. epochMillis is already in milliseconds. |
platformJson | status, priority, issuetype, labels, project key | Add a request type or other custom field if you report on it. |
Personal data. Don’t send the reporter’s emailAddress or displayName. Requests and comments often hold names, emails and phone numbers: redact them before you send. See PII redaction.
4. Send it
Section titled “4. Send it”// jira-service-management.mjs. Environment: JSM_SITE (your-site.atlassian.net), JSM_EMAIL, JSM_API_TOKEN,// JSM_PROJECTS (project keys, for example HELP,IT), DL_API_KEY.import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
const HOUR = 60 * 60 * 1000;const sleep = (ms) => new Promise((r) => setTimeout(r, ms));const base = `https://${process.env.JSM_SITE}`;const auth = 'Basic ' + Buffer.from(`${process.env.JSM_EMAIL}:${process.env.JSM_API_TOKEN}`).toString('base64');const noColon = (id) => String(id).replace(/:/g, '-');
// Retries Atlassian's rate limits (429) and server errors (5xx), honoring Retry-After.async function get(url) { for (let attempt = 0; ; attempt++) { const res = await fetch(url, { headers: { Authorization: auth, Accept: 'application/json' } }); if (res.ok) return res.json(); if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Jira returned ${res.status}: ${await res.text()}`); await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000); }}
// Reads every page of a Jira Service Management list (start and limit, until isLastPage).async function paged(path) { const items = []; for (let start = 0; ; ) { const page = await get(`${base}${path}${path.includes('?') ? '&' : '?'}start=${start}&limit=50`); items.push(...page.values); if (page.isLastPage || page.values.length === 0) return items; start += page.values.length; }}
// Atlassian Document Format to plain text: the text nodes in order, a line break after each block.function adfToText(node) { if (!node) return ''; if (node.type === 'text') return node.text ?? ''; if (node.type === 'hardBreak') return '\n'; const inner = (node.content ?? []).map(adfToText).join(''); return ['paragraph', 'heading', 'codeBlock'].includes(node.type) ? `${inner}\n` : inner;}
// Sends every request and public comment written at or after `since` (Unix ms). Returns the new `since`.export async function syncJsm({ since, historical = false }) { const projects = process.env.JSM_PROJECTS.split(',').map((p) => `"${p.trim()}"`).join(','); const jql = `project in (${projects}) AND updated >= ${Math.floor(since)} ORDER BY updated ASC`; const fields = 'summary,description,reporter,created,status,priority,issuetype,labels'; let newest = since; let token = null; do { const page = await get(`${base}/rest/api/3/search/jql?jql=${encodeURIComponent(jql)}&fields=${fields}&maxResults=100${token ? `&nextPageToken=${encodeURIComponent(token)}` : ''}`); for (const issue of page.issues) { const f = issue.fields; const reporter = f.reporter?.accountId; const participants = await paged(`/rest/servicedeskapi/request/${issue.key}/participant`); const customers = new Set([reporter, ...participants.map((p) => p.accountId)].filter(Boolean)); const send = async (text, incoming, ts) => { await sendToDimensionLabs({ text, userId: reporter ? noColon(reporter) : `jsm-${issue.key}`, incoming, sessionId: `jsm-${issue.key}`, conversationId: `jsm-${issue.key}`, dimensionlabs_timestamp: ts, platformJson: { project: issue.key.split('-')[0], status: f.status?.name, priority: f.priority?.name, issuetype: f.issuetype?.name, labels: f.labels }, }, { historical: historical || ts < Date.now() - HOUR }); newest = Math.max(newest, ts); };
const opened = toMillis(f.created); const request = [f.summary, adfToText(f.description)].filter((s) => s?.trim()).join('\n\n').trim(); if (opened >= since && request) await send(request, true, opened);
for (const c of await paged(`/rest/servicedeskapi/request/${issue.key}/comment?public=true&internal=false`)) { const ts = c.created?.epochMillis; if (!c.public || !ts || ts < since || !c.body?.trim()) continue; await send(c.body, customers.has(c.author?.accountId), ts); } } token = page.nextPageToken ?? null; } while (token); return newest;}Atlassian notes that very recent changes might not show up in search results straight away. The next run picks them up, and anything sent twice is removed automatically.
5. Run it
Section titled “5. Run it”Save the code as jira-service-management.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncJsm as sync } from './jira-service-management.mjs';.
- History first:
node run.mjs 2025-01-01sends every request and public comment since that date to/trackhistorical. Then start a backfill from the Backfill tab. - Then every 15 minutes: the scheduled job sends new messages to
/track, and they are enriched automatically every day. A message that is more than an hour old when the job sees it goes to/trackhistorical, and needs a backfill. - Re-runs are safe: duplicates are removed automatically.
Each request costs at least two extra calls (participants and comments), so a history load takes a while; the code waits whenever Jira asks it to. Replace the API token before it expires, or the job stops with a 401. You can also upload a file with one row per comment (check it with the file checker), then run a backfill.
Jira Service Management reference
Section titled “Jira Service Management reference”- Manage API tokens for your Atlassian account
- Issue search (enhanced JQL search)
- Jira Service Management REST API: Request
- Jira Service Management REST API: introduction
- JQL fields
- Atlassian Document Format
- Rate limiting
Checked against Jira Service Management’s documentation on 2026-10-26.