# Oracle B2C Service

> Send Oracle B2C Service incidents and their threads to Dimension Labs with the universal API.

This brings in each Oracle B2C Service (formerly Service Cloud and RightNow) incident as one conversation: the customer's entries and your staff's responses, across email, chat, web forms and the other channels your site uses. You run it on your side: a small script pulls from the Connect REST API and posts each message to the [universal API](/api/tracker).

## What you need

- Your site's REST server URL, for example `https://mysite.example.com`. Oracle sends it to your site administrator in the welcome email.
- A staff account whose profile has permission to read incidents through the REST API, and its user name and password.
- A Dimension Labs API key for a new **Universal API** source. See [Your API key](/api/tracker#your-api-key).

## 1. Get API access in Oracle B2C Service

1. Ask your site administrator for the REST server URL and a staff account for the integration. Its profile needs access to the incidents resource ([quick start](https://docs.oracle.com/en/cloud/saas/b2c-service/cxsvc/c_osvc_quick_start.html)).
2. Authenticate with Basic auth: the staff account's user name and password. Session tokens and OAuth Bearer tokens from an external identity provider also work ([authenticate and authorize](https://docs.oracle.com/en/cloud/saas/b2c-service/cxsvc/c_osvc_authentication_and_authorization.html)).
3. Send the `OSvC-CREST-Application-Context` header on every request, with a short label such as `Dimension Labs sync`. From version 1.4 it is required: without it, every request fails with `400 Bad Request`. It can be at most 40 characters ([custom headers](https://docs.oracle.com/en/cloud/saas/b2c-service/cxsvc/c_osvc_custom_headers.html)).

Your administrator can map that label to a resource pool, which caps how many requests it can run at once ([API throttling](https://docs.oracle.com/en/cloud/saas/b2c-service/cxsvc/c_osvc_api_throttling.html)).

## 2. Pull new incidents

[Get incidents](https://docs.oracle.com/en/cloud/saas/b2c-service/cxsvc/op-services-rest-connect-v1.4-incidents-get.html) ordered by when they last changed, newest first, and stop once you reach incidents older than your cursor:

```bash
curl -u "$OSVC_USERNAME:$OSVC_PASSWORD" \
  -H "OSvC-CREST-Application-Context: Dimension Labs sync" \
  "https://mysite.example.com/services/rest/connect/v1.4/incidents?orderBy=updatedTime:desc&limit=100&offset=0"
```

Each item has `id`, `lookupName` (the reference number), `createdTime` and `updatedTime`. Page with `limit` and `offset` while the response has `hasMore: true`.

For each incident, [get the incident](https://docs.oracle.com/en/cloud/saas/b2c-service/cxsvc/op-services-rest-connect-v1.4-incidents-id-get.html) with `?expand=threads`. A plain GET doesn't include threads; with `expand=threads`, the `threads.items` list holds a link to each thread, such as `/incidents/35/threads/24`. Get each thread for its `text`, `entryType`, `contentType`, `contact`, `account` and `createdTime` ([supported HTTP methods](https://docs.oracle.com/en/cloud/saas/b2c-service/cxsvc/c_osvc_http_methods.html)).

Oracle doesn't publish a fixed rate limit. Each response carries two headers: `osvc-crest-next-request-after`, the milliseconds to wait before your next request, and `osvc-crest-api-access-token`, which you pass back unchanged on the next request. Leaving the token out can degrade performance and cause errors. A request over your resource pool's limit returns `503`.

Oracle says not to build integrations from the swagger.json on its help center, which is a sample for documentation only. Work from the reference pages.

## 3. Map the fields

| Dimension Labs | Oracle B2C Service | Notes |
|---|---|---|
| `text` | thread `text` | Strip HTML when `contentType` is HTML. Skip private notes (see `incoming`). |
| `userId` | incident `primaryContact.id` | The contact's internal id, on both their entries and your responses. Fall back to the `sessionId`. |
| `incoming` | thread `entryType.lookupName` | Oracle's entry types are Customer Entry, Response and Private Note. Customer entries are incoming, responses outgoing, and private notes are staff-only, so skip them. The code keeps only names with "customer" or "response". Check the names on your site with one thread GET before the history load. |
| `sessionId` | incident `id` | Prefix it, for example `oracle-b2c-service-67`. |
| `conversationId` | incident `id` | Same value as `sessionId`. |
| `dimensionlabs_timestamp` | thread `createdTime` | Convert with `toMillis`. Threads with the same time are ordered by `displayOrder` and get 1,000 ms added each. |
| `platformJson` | incident `lookupName`, `statusWithType`, `channel`, `source`; thread `channel` | The thread's `channel` is the channel of that entry. |

**Personal data.** Don't send the contact's name or email, the thread's `mailHeader`, or the staff account's name. Threads can hold addresses, phone numbers and account numbers: redact them before you send. See [PII redaction](/data/pii-redaction).

## 4. Send it

```js
// oracle-b2c-service.mjs. Environment: OSVC_SITE (https://mysite.example.com), OSVC_USERNAME, OSVC_PASSWORD, DL_API_KEY.


const HOUR = 60 * 60 * 1000;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const base = `${process.env.OSVC_SITE}/services/rest/connect/v1.4`;
const auth = 'Basic ' + Buffer.from(`${process.env.OSVC_USERNAME}:${process.env.OSVC_PASSWORD}`).toString('base64');
const stripHtml = (html) => String(html ?? '').replace(/<br\s*\/?>|<\/p>/gi, '\n').replace(/<[^>]+>/g, ' ')
  .replace(/&nbsp;/g, ' ').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, '&')
  .replace(/[ \t]+/g, ' ').replace(/\s*\n\s*/g, '\n').trim();

let accessToken = null; // osvc-crest-api-access-token, passed from each response to the next request
let waitMs = 0; // osvc-crest-next-request-after

// Honors Oracle's pacing headers, and retries rate limits (429) and server errors (5xx, including 503 Server Busy).
async function get(url) {
  for (let attempt = 0; ; attempt++) {
    await sleep(waitMs);
    const headers = { Authorization: auth, 'OSvC-CREST-Application-Context': 'Dimension Labs sync' };
    if (accessToken) headers['osvc-crest-api-access-token'] = accessToken;
    const res = await fetch(url, { headers });
    accessToken = res.headers.get('osvc-crest-api-access-token') ?? accessToken;
    waitMs = Number(res.headers.get('osvc-crest-next-request-after')) || 0;
    if (res.ok) return res.json();
    if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Oracle B2C Service returned ${res.status}: ${await res.text()}`);
    await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
  }
}

// Incidents changed at or after `since`, newest first.
async function changedIncidents(since) {
  const found = [];
  for (let offset = 0; ; offset += 100) {
    const page = await get(`${base}/incidents?orderBy=updatedTime:desc&limit=100&offset=${offset}`);
    const fresh = page.items.filter((i) => toMillis(i.updatedTime) >= since);
    found.push(...fresh);
    if (!page.hasMore || fresh.length < page.items.length) return found;
  }
}

const kind = (t) => {
  const name = t.entryType?.lookupName ?? '';
  if (/private/i.test(name)) return null;
  if (/customer/i.test(name)) return 'in';
  if (/response/i.test(name)) return 'out';
  return null;
};

// Sends every customer entry and response written at or after `since` (Unix ms). Returns the new `since`.
export async function syncOracleB2cService({ since, historical = false }) {
  const started = Date.now();
  let newest = since;
  for (const { id } of await changedIncidents(since)) {
    const incident = await get(`${base}/incidents/${id}?expand=threads`);
    const threads = [];
    for (const link of incident.threads?.items ?? []) threads.push(await get(link.href));
    threads.sort((a, b) => toMillis(a.createdTime) - toMillis(b.createdTime) || a.displayOrder - b.displayOrder);
    let last = 0;
    for (const t of threads) {
      const written = toMillis(t.createdTime);
      const ts = written <= last ? last + 1000 : written;
      last = ts;
      const direction = kind(t);
      const text = /html/i.test(t.contentType?.lookupName ?? '') ? stripHtml(t.text) : String(t.text ?? '').trim();
      if (!direction || written < since || !text) continue;
      await sendToDimensionLabs({
        text,
        userId: incident.primaryContact?.id ? String(incident.primaryContact.id) : `oracle-b2c-service-${id}`,
        incoming: direction === 'in',
        sessionId: `oracle-b2c-service-${id}`,
        conversationId: `oracle-b2c-service-${id}`,
        dimensionlabs_timestamp: ts,
        platformJson: {
          reference: incident.lookupName,
          status: incident.statusWithType?.status?.lookupName,
          incident_channel: incident.channel?.lookupName,
          source: incident.source?.lookupName,
          channel: t.channel?.lookupName,
          entry_type: t.entryType?.lookupName,
        },
      }, { historical: historical || ts < Date.now() - HOUR });
      newest = Math.max(newest, written);
    }
  }
  // A record updated mid-run can shift the paging; stopping the cursor a little before the run started means the next run reads it. Re-sends are safe.
  return Math.max(since, Math.min(newest, started - 5 * 60 * 1000));
}
```

## 5. Run it

Save the code as `oracle-b2c-service.mjs` in a project set up as in [Set up your project](/api/tracker#set-up-your-project), and run it with the [scheduled sync](/api/scheduled-sync) runner: `import { syncOracleB2cService as sync } from './oracle-b2c-service.mjs';`.

- **History first:** `node run.mjs 2025-01-01` sends every entry and response since that date to `/trackhistorical`. Then start a [backfill](/dimensions/backfill) from the **Backfill** tab.
- **Then every 15 minutes:** the scheduled job sends new entries to `/track`, and they are enriched automatically every day. An entry 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 incident costs one call plus one per thread, and the code waits as long as `osvc-crest-next-request-after` asks, so a history load takes a while. Ask your administrator to map the application context to a resource pool if the job competes with other integrations. You can also [upload a file](/data/upload-a-file) of exported incidents with one row per thread (check it with the [file checker](/data/check-a-file)), then run a backfill.

## Oracle B2C Service reference

- [Quick start](https://docs.oracle.com/en/cloud/saas/b2c-service/cxsvc/c_osvc_quick_start.html)
- [Authenticate and authorize](https://docs.oracle.com/en/cloud/saas/b2c-service/cxsvc/c_osvc_authentication_and_authorization.html)
- [Custom headers](https://docs.oracle.com/en/cloud/saas/b2c-service/cxsvc/c_osvc_custom_headers.html)
- [API throttling](https://docs.oracle.com/en/cloud/saas/b2c-service/cxsvc/c_osvc_api_throttling.html)
- [Supported query parameters](https://docs.oracle.com/en/cloud/saas/b2c-service/cxsvc/c_osvc_query_parameters.html)
- [Supported HTTP methods](https://docs.oracle.com/en/cloud/saas/b2c-service/cxsvc/c_osvc_http_methods.html)
- [Incidents REST endpoints](https://docs.oracle.com/en/cloud/saas/b2c-service/cxsvc/api-incidents.html)
- [Get multiple incidents](https://docs.oracle.com/en/cloud/saas/b2c-service/cxsvc/op-services-rest-connect-v1.4-incidents-get.html)
- [Get an incident](https://docs.oracle.com/en/cloud/saas/b2c-service/cxsvc/op-services-rest-connect-v1.4-incidents-id-get.html)
- [Incident thread](https://docs.oracle.com/en/cloud/saas/b2c-service/famug/c-Responding-to-incidents-aa1180848.html)

Checked against Oracle's documentation on 2026-10-26.
