Skip to content

02 Get your data in /Integrations

Help Scout

Send Help Scout conversations and their threads to Dimension Labs with the universal API.

  1. HS Help ScoutTickets, 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 Help Scout conversation, its customer messages and your team’s replies across email, chat and phone threads. You run it on your side: a small script pulls from the Help Scout Inbox API and posts each thread to the universal API.

  • An active, invited Help Scout user to own the app. Help Scout returns 401 for credentials not tied to one.
  • An OAuth2 app’s application ID and application secret, used with the client credentials flow.
  • A Dimension Labs API key for a new Universal API source. See Your API key.
  1. Go to Your Profile > My apps and click Create My App.
  2. Help Scout asks for a redirection URL. For the client credentials flow any valid URL works, such as https://example.com.
  3. Copy the application ID and application secret.
  4. Request an access token (authentication):
Terminal window
curl -X POST https://api.helpscout.net/v2/oauth2/token \
--data "grant_type=client_credentials" \
--data "client_id=$HELPSCOUT_APP_ID" --data "client_secret=$HELPSCOUT_APP_SECRET"

The token is valid for 2 days. Request a new one when the API answers 401. Send it as Authorization: Bearer ACCESS_TOKEN.

Use List Conversations (v3), which Help Scout recommends over v2 for performance, with modifiedSince. Add status=all, because only active conversations are listed by default.

Terminal window
curl -H "Authorization: Bearer $HELPSCOUT_TOKEN" \
"https://api.helpscout.net/v3/conversations?status=all&modifiedSince=2026-10-13T00:00:00Z"

v3 uses cursor pagination: request _links.next.href until there is no next link. Results are always newest first.

For each conversation, List Threads (v3): GET /v3/conversations/{id}/threads. Follow _links.next.href when present (pagination). Use this endpoint rather than embed=threads, which returns chat threads truncated.

Help Scout’s rate limit depends on your plan and is shared by all users on the account. Write requests count double, and a 429 response carries X-RateLimit-Retry-After (rate limiting).

Dimension LabsHelp ScoutNotes
textthread bodyCan contain HTML: strip it. Skip lineitem threads (status changes, no body) and note threads (internal notes), and threads whose state is draft.
userIdconversation primaryCustomer.idHelp Scout’s internal customer id, on both their threads and your replies. Fall back to the sessionId. Don’t use primaryCustomer.email.
incomingthread createdBy.typetrue when it is customer. Other values are user and, in v3, system_user for AI-powered system users.
sessionIdconversation idPrefix it, for example helpscout-10.
conversationIdconversation idSame value as sessionId.
dimensionlabs_timestampthread createdAtConvert with toMillis.
platformJsonthread type, source, rating; conversation number, mailboxId, status, tags, customFieldscustomFields such as account type help with segmenting.

Personal data. Email and chat threads hold names, signatures and phone numbers: redact them before you send. See PII redaction.

// help-scout.mjs. Environment: HELPSCOUT_APP_ID, HELPSCOUT_APP_SECRET, DL_API_KEY.
import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
const HOUR = 60 * 60 * 1000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const stripHtml = (html) => html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
const iso = (ms) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, 'Z');
async function token() {
const body = new URLSearchParams({ grant_type: 'client_credentials', client_id: process.env.HELPSCOUT_APP_ID, client_secret: process.env.HELPSCOUT_APP_SECRET });
const res = await fetch('https://api.helpscout.net/v2/oauth2/token', { method: 'POST', body });
if (!res.ok) throw new Error(`Help Scout token request returned ${res.status}: ${await res.text()}`);
return (await res.json()).access_token;
}
// Sends every thread written at or after `since` (Unix ms). Returns the new `since`.
export async function syncHelpScout({ since, historical = false }) {
const headers = { Authorization: `Bearer ${await token()}` }; // valid for 2 days, so one per run is enough
// Retries Help Scout's rate limits (429) and server errors (5xx), honoring X-RateLimit-Retry-After.
const get = async (url) => {
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, { headers });
if (res.ok) return res.json();
if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Help Scout returned ${res.status}: ${await res.text()}`);
await sleep((Number(res.headers.get('x-ratelimit-retry-after')) || 2 ** attempt) * 1000);
}
};
let newest = since;
for (let url = `https://api.helpscout.net/v3/conversations?status=all&modifiedSince=${iso(since)}`; url; ) {
const page = await get(url);
for (const conv of page._embedded?.conversations ?? []) {
for (let t = `https://api.helpscout.net/v3/conversations/${conv.id}/threads`; t; ) {
const res = await get(t);
for (const th of res._embedded?.threads ?? []) {
const ts = toMillis(th.createdAt);
const text = th.body ? stripHtml(th.body) : '';
if (['lineitem', 'note'].includes(th.type) || th.state === 'draft' || ts < since || !text) continue;
await sendToDimensionLabs({
text,
userId: conv.primaryCustomer?.id ? String(conv.primaryCustomer.id) : `helpscout-${conv.id}`,
incoming: th.createdBy?.type === 'customer',
sessionId: `helpscout-${conv.id}`,
conversationId: `helpscout-${conv.id}`,
dimensionlabs_timestamp: ts,
platformJson: { thread_type: th.type, source: th.source?.type, rating: th.rating, number: conv.number, mailbox_id: conv.mailboxId, status: conv.status, tags: conv.tags?.map((x) => x.tag) },
}, { historical: historical || ts < Date.now() - HOUR });
newest = Math.max(newest, ts);
}
t = res._links?.next?.href;
}
}
url = page._links?.next?.href;
}
return newest;
}

Save the code as help-scout.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncHelpScout as sync } from './help-scout.mjs';.

  • History first: node run.mjs 2025-01-01 sends every thread since that date to /trackhistorical. Then start a backfill from the Backfill tab.
  • Then every 15 minutes: the scheduled job sends new threads to /track, and they are enriched automatically every day. A thread 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.

You can also upload a file of exported conversations (check it with the file checker), then run a backfill.

Checked against Help Scout’s documentation on 2026-10-13.