02 Get your data in /Integrations
Medallia Experience Cloud
Send survey feedback from Medallia Experience Cloud to Dimension Labs with the universal API.
- ME MedalliaTickets, chats, calls, reviews
- ⟳ Your jobPull new · map fields · every 15 min
- { } Universal APIPOST /track · /trackhistorical
- Dimension LabsRead in full · enriched daily
This brings in completed feedback records from Medallia Experience Cloud: each comment question and the customer’s answer, with scores and record fields alongside. You run it on your side with the universal API, using Medallia’s Query API.
What you need
Section titled “What you need”- Experience Programs set up in your Medallia instance. Medallia lists this as a requirement for Query API.
- A Medallia OAuth 2.0 client ID and client secret, with a role that has the Query API capability.
- The field IDs of the comment questions and scores you want, for example your NPS field.
- A Dimension Labs API key for a new Universal API source. See Your API key.
1. Get API access in Medallia
Section titled “1. Get API access in Medallia”Medallia’s public steps are in Authentication. The steps for creating the client ID are in Medallia’s product documentation, which you reach by logging in to your Experience Cloud instance.
- Ask your Medallia administrator for an OAuth 2.0 client ID and secret. The role assigned to the client decides what data it can read, and that role must have the Query API capability. Credentials are per instance: production, sandbox and developer instances each need their own.
- Request a token from your reporting instance with the client credentials grant, sending the client ID and secret as HTTP Basic authentication:
curl -X POST -u "$MEDALLIA_CLIENT_ID:$MEDALLIA_CLIENT_SECRET" \ -d "grant_type=client_credentials" \ "https://YOUR_INSTANCE.medallia.com/oauth/YOUR_TENANT/token"- The response has
access_token,token_typeandexpires_in(3600 in Medallia’s example). Send it asAuthorization: Bearer ACCESS_TOKENto the Medallia API gateway. Medallia notes that tokens come from the reporting instance, while API calls go to the gateway.
2. Pull new feedback
Section titled “2. Pull new feedback”Query API is GraphQL. Every request is a POST to /data/v0/query on your API gateway, with a query and variables.
curl -X POST -H "Authorization: Bearer $MEDALLIA_TOKEN" -H "Content-Type: application/json" \ --data-binary '{"query":"query { fields(ids: [\"YOUR_COMMENT_FIELD_ID\"]) { nodes { id name } } }","variables":{}}' \ "https://YOUR_API_GATEWAY/data/v0/query"- Use
feedback, which holds only completed records. Medallia says queries on it are optimized. It takesfirst,after,filterandorderBy. - Incremental: order by
e_responsedateascending and filter on it withgte. Medallia’s streaming example also orders on the survey ID (a_surveyid) so that records with the same date come back in a stable order. - Each record’s
idmaps toa_surveyid. Read any field withfieldData(fieldId: "...") { values labels }. - Pagination: request
pageInfo { hasNextPage endCursor }. WhilehasNextPageis true, passendCursorasafterin the next query. - Find field IDs and their names with the
fieldsquery shown above. - Rate limits (Medallia’s): 70 requests per second, 975,000 requests per 24 hours, and a 3,000,000 cost-unit limit per query.
- Medallia’s data is eventually consistent, usually within seconds to minutes, so a record can appear shortly after its response date.
3. Map the fields
Section titled “3. Map the fields”Each comment question becomes an outgoing message (the field’s name) followed by an incoming message (the customer’s answer), under the record ID. Skip blank comments.
| Dimension Labs | Medallia | Notes |
|---|---|---|
text | field name from fields (outgoing), the comment field’s values (incoming) | Field IDs differ between instances. Use your own. |
userId | id | Use the record ID unless you have an internal customer ID field you trust (not an email). |
incoming | false for the question, true for the answer. | |
sessionId | id (a_surveyid) | |
conversationId | id | |
dimensionlabs_timestamp | e_responsedate | Convert with toMillis, which reads a date without a time zone as UTC. Add 1,000 ms per message. |
platformJson | score fields such as NPS, e_unitid, and any other fields you query | Unit and program fields are useful for joins. |
Personal data. Query only the fields you need: Medallia records often hold the customer’s name, email and phone in other fields. Redact comments too. See PII redaction.
4. Send it
Section titled “4. Send it”// medallia.mjs. Environment: MEDALLIA_GATEWAY (your API gateway host), MEDALLIA_TOKEN_URL// (https://YOUR_INSTANCE.medallia.com/oauth/YOUR_TENANT/token), MEDALLIA_CLIENT_ID, MEDALLIA_CLIENT_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 COMMENTS = ['YOUR_COMMENT_FIELD_ID']; // your comment field IDsconst EXTRA = ['YOUR_NPS_FIELD_ID', 'e_unitid']; // scores and metadata for platformJsonconst ids = [...COMMENTS, ...EXTRA, 'e_responsedate'];const select = ids.map((f, i) => `f${i}: fieldData(fieldId: "${f}") { values }`).join(' ');const QUERY = `query feed($filter: Filter, $after: ID) { feedback(first: 100, after: $after, filter: $filter, orderBy: [{ fieldId: "e_responsedate", direction: ASC }, { fieldId: "a_surveyid", direction: ASC }]) { nodes { id ${select} } pageInfo { hasNextPage endCursor } } }`;// Match the format your instance returns for e_responsedate: check one record first.const medalliaDate = (ms) => new Date(ms).toISOString().slice(0, 19).replace('T', ' ');let token;
async function newToken() { const res = await fetch(process.env.MEDALLIA_TOKEN_URL, { method: 'POST', body: new URLSearchParams({ grant_type: 'client_credentials' }), headers: { Authorization: `Basic ${btoa(`${process.env.MEDALLIA_CLIENT_ID}:${process.env.MEDALLIA_CLIENT_SECRET}`)}` } }); if (!res.ok) throw new Error(`Medallia token request returned ${res.status}: ${await res.text()}`); token = (await res.json()).access_token;}
// Gets a fresh token on 401 (tokens last about an hour), and retries 429 and 5xx with a growing pause.async function gql(query, variables = {}) { for (let attempt = 0; ; attempt++) { if (!token) await newToken(); const res = await fetch(`https://${process.env.MEDALLIA_GATEWAY}/data/v0/query`, { method: 'POST', headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ query, variables }) }); if (res.ok) return (await res.json()).data; if (res.status === 401 && attempt === 0) { token = null; continue; } if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Medallia returned ${res.status}: ${await res.text()}`); await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000); }}
// Sends every completed record with a response date at or after `since` (Unix ms). Returns the new `since`.export async function syncMedallia({ since, historical = false }) { token = null; const names = Object.fromEntries((await gql(`query { fields(ids: ${JSON.stringify(COMMENTS)}) { nodes { id name } } }`)).fields.nodes.map((f) => [f.id, f.name])); const filter = { fieldIds: ['e_responsedate'], gte: medalliaDate(since) }; let after = null, newest = since; do { const { feedback } = await gql(QUERY, { filter, after }); for (const n of feedback.nodes) { const val = (i) => n[`f${i}`]?.values?.[0]; const responded = toMillis(val(ids.length - 1)); const base = { userId: n.id, sessionId: n.id, conversationId: n.id, platformJson: Object.fromEntries(EXTRA.map((f, i) => [f, val(COMMENTS.length + i)])) }; const old = historical || responded < Date.now() - HOUR; let t = responded; for (const [i, f] of COMMENTS.entries()) { const answer = String(val(i) ?? '').trim(); if (!answer) continue; await sendToDimensionLabs({ ...base, text: names[f] ?? f, incoming: false, dimensionlabs_timestamp: t }, { historical: old }); await sendToDimensionLabs({ ...base, text: answer, incoming: true, dimensionlabs_timestamp: t + 1000 }, { historical: old }); t += 2000; } newest = Math.max(newest, responded); } after = feedback.pageInfo.hasNextPage ? feedback.pageInfo.endCursor : null; } while (after); return newest;}Each run asks for records from the cursor’s date on (gte), so the records at the cursor come back each time. That’s fine: duplicates are removed automatically. Medallia’s guide also points to a fuller streaming example in Java, medallia/query-api-data-extract on GitHub.
5. Run it
Section titled “5. Run it”Save the code as medallia.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncMedallia as sync } from './medallia.mjs';.
- History first:
node run.mjs 2024-01-01sends every completed record since that date to/trackhistorical. Then start a backfill from the Backfill tab. - Then every 15 minutes: the scheduled job sends new records to
/track, and they are enriched automatically every day. Medallia’s data is eventually consistent, so a record can show up a little after its response date; one 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.
Medallia also describes batch exports over SFTP, S3 or similar; you can upload such a file and check it first with the file checker, then run a backfill.
Medallia reference
Section titled “Medallia reference”Checked against Medallia’s public developer documentation on 2026-10-13.