Skip to content

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.

  1. JS Jira Service ManagementTickets, chats, calls, reviews
  2. ⟳ Your jobPull new · map fields · every 15 min
  3. { } Universal APIPOST /track · /trackhistorical
  4. 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.

  • 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”
  1. Signed in as the integration account, go to id.atlassian.com/manage-profile/security/api-tokens.
  2. 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.
  3. Select Create, then Copy to clipboard. You can’t see the token again later.
  4. 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.

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.

Terminal window
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 issues and, unless it is the last page, a nextPageToken. Pass it back as nextPageToken for the next page.
  • description comes 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).

Dimension LabsJira Service ManagementNotes
textsummary and description for the first message; comment body after thatOnly comments returned with public=true&internal=false. Internal comments are never requested.
userIdissue reporter.accountIdAtlassian’s account id for the customer, on every message of the request. Account ids can contain a colon: the code replaces it with -.
incomingcomment author.accountId is the reporter or a participantThe request itself is incoming. Comments from anyone else (your agents) are outgoing.
sessionIdissue keyPrefix it, for example jsm-HELP-123.
conversationIdissue keySame value as sessionId.
dimensionlabs_timestampissue created; comment created.epochMillisConvert with toMillis. epochMillis is already in milliseconds.
platformJsonstatus, priority, issuetype, labels, project keyAdd 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.

// 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.

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-01 sends 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.

Checked against Jira Service Management’s documentation on 2026-10-26.