> ## Documentation Index
> Fetch the complete documentation index at: https://docs.deploybaseline.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Salesforce

> Records, SOQL, describe, sObject collections, composite requests, and OAuth.

Stateful Salesforce emulation of the REST API, versions 31.0 to 68.0: the version list and resources, describe global and per object describes, record create, read, update, and delete by ID, upsert and reads by external ID, relationship traversal, SOQL through `query` and `queryAll` with relationship queries, aggregates, date literals, and paging by query locator, recently viewed records, the deleted and updated replication feeds, sObject Collections for up to 200 writes or 2,000 reads in one call, composite, batch, tree, and graph requests, limits and record counts, the identity URL, and OAuth 2.0 for connected apps: the web server flow with PKCE, the user-agent flow, refresh tokens, the JWT bearer, client credentials, and username-password flows, revoke, introspect, userinfo, ID tokens, and OpenID discovery. Account, Contact, Lead, Opportunity, Case, Task, User, and Organization are built in, and seed config adds custom fields and custom objects. The tests drive it with [`jsforce`](https://github.com/jsforce/jsforce) (3.12.0).

## At a glance

| | |
| - | - |
| Package | `@deploybaseline/salesforce` |
| Port | `4000` when started alone; named services take consecutive ports |
| Config key | `salesforce` |
| HTTP operations | 47, browsable at `http://localhost:3999/docs/salesforce` while it runs ([API reference](/api-reference.md)) |

```bash CLI theme={null}
npx @deploybaseline/cli start --service salesforce
```

```ts Programmatic theme={null}
import { createEmulator } from "@deploybaseline/cli";

const salesforce = await createEmulator({ service: "salesforce" });
// salesforce.url is http://localhost:4000
```

```json baseline.config.json theme={null}
{
  "salesforce": {
    "org": {
      "name": "Acme"
    },
    "access_tokens": [
      {
        "token": "00D-baseline-local-token"
      }
    ],
    "fields": {
      "Account": [
        {
          "name": "External_Id__c",
          "type": "text",
          "length": 40,
          "external_id": true,
          "unique": true
        }
      ]
    },
    "records": {
      "Account": [
        {
          "Name": "Acme",
          "Industry": "Technology",
          "External_Id__c": "ACME-1"
        }
      ],
      "Contact": [
        {
          "FirstName": "Ada",
          "LastName": "Lovelace",
          "Email": "ada@example.com",
          "Account": {
            "External_Id__c": "ACME-1"
          }
        }
      ],
      "Opportunity": [
        {
          "Name": "Acme renewal",
          "StageName": "Prospecting",
          "CloseDate": "2026-12-31",
          "Amount": 12000,
          "Account": {
            "Name": "Acme"
          }
        }
      ]
    }
  }
}
```

## Start

```bash theme={null}
npx @deploybaseline/cli start --service salesforce
```

Salesforce starts only when named in config or with `--service`. When it is the only enabled service, it listens on `http://localhost:4000`.

Without seed config, the org has the admin user `admin@example.com`, and any token the CLI accepts acts as that user.

The emulator is both the instance URL and the login URL. Point jsforce at it with `instanceUrl` and a seeded token:

```ts theme={null}
import { Connection } from "jsforce";

const conn = new Connection({
  instanceUrl: process.env.SALESFORCE_EMULATOR_URL,
  accessToken: "00D-baseline-local-token",
  version: "68.0",
});

const account = await conn.sobject("Account").create({ Name: "Acme" });
const contact = await conn.sobject("Contact").create({
  LastName: "Lovelace",
  Account: { External_Id__c: "ACME-1" },
});
```

## URL mapping

| Real Salesforce URL | Emulator URL |
| - | - |
| `https://{instance}.my.salesforce.com/services/data/...` | `$SALESFORCE_EMULATOR_URL/services/data/...` |
| `https://login.salesforce.com/id/{orgId}/{userId}` | `$SALESFORCE_EMULATOR_URL/id/{orgId}/{userId}` |
| `https://login.salesforce.com/services/oauth2/...` | `$SALESFORCE_EMULATOR_URL/services/oauth2/...` |
| `https://login.salesforce.com/.well-known/openid-configuration` | `$SALESFORCE_EMULATOR_URL/.well-known/openid-configuration` |

## Auth

Send `Authorization: Bearer <token>` (or `OAuth <token>`), as Salesforce takes session IDs and OAuth access tokens.

* **OAuth access tokens** from a connected app's flows act as the user who approved the app (see [OAuth](#oauth)). A token reaches the REST API only with the `api` or `full` scope; others get HTTP 401 `INVALID_SESSION_ID` (`This session is not valid for use with the REST API`) but still open the identity URL and userinfo.
* **Seeded tokens** from `access_tokens` open a session as the user they name, or as the admin user when they name none. Records the session creates are owned, created, and last modified by that user.
* **Any other token** the CLI accepts acts as the admin user (`admin@example.com`), so a quick start needs no setup.
* A missing or unknown token gets HTTP 401 with `INVALID_SESSION_ID` (`Session expired or invalid`) and `WWW-Authenticate: Token`. A token of an inactive user is refused the same way.

`GET /services/data` (the version list) needs no session. Every other call counts against a daily limit of 15,000 API requests, reported in the `Sforce-Limit-Info: api-usage=N/15000` header and in `DailyApiRequests` from `/limits`.

## OAuth

Seed connected apps under `connected_apps` (see [Seed config](#seed-config)), then use the same endpoints and clients as against `login.salesforce.com`. With jsforce, set `loginUrl` to the emulator:

```ts theme={null}
import { Connection, OAuth2 } from "jsforce";

const oauth2 = new OAuth2({
  loginUrl: process.env.SALESFORCE_EMULATOR_URL,
  clientId: "3MVG9-baseline-web-app",
  clientSecret: "BASELINE-WEB-SECRET",
  redirectUri: "http://localhost:3000/oauth/callback",
});

// Send the browser here; the approval page lists the org's active users.
const url = oauth2.getAuthorizationUrl({ scope: "api refresh_token openid", state: "xyz" });

// In the callback:
const conn = new Connection({ oauth2 });
const userInfo = await conn.authorize(code); // access, refresh, and identity
```

The emulator serves:

* `GET /services/oauth2/authorize` with `response_type` of `code`, `token`, or `token id_token`, plus `client_id`, `redirect_uri`, `scope`, `state`, `nonce`, `login_hint`, and `code_challenge` with `code_challenge_method=S256`. It checks the response type, then the client, then that the callback is one of the app's `callback_urls` exactly; those errors answer on the page with HTTP 400 and a form encoded body such as `error=invalid_client_id&error_description=client%20identifier%20invalid`, as the service does. Once the callback is known, `invalid_scope` and PKCE errors go back to it. Otherwise an **Allow Access?** page lists the scopes and a button per active user (the `login_hint` user first) and a Deny link, which sends `access_denied`. The code flow returns `code` and `state` in the query; a code lasts 15 minutes and is spent by any exchange attempt. The user-agent flow returns the token in the fragment.
* `POST /services/oauth2/token` with the client secret in the form or by HTTP Basic, for these grants:
  * `authorization_code`, which needs `redirect_uri` and, when the authorize request sent a challenge, `code_verifier`. The secret is required unless the app turns off `require_secret`.
  * `refresh_token`, which returns a new access token and no new refresh token. The secret is required unless the app turns off `require_secret_for_refresh`. A user holds at most five refresh tokens per app; a sixth revokes the oldest.
  * `password`, with the password followed by the user's security token, when the app sets `password_flow`. The response has no `scope` and no refresh token.
  * `client_credentials`, running as the app's `client_credentials_user`.
  * `urn:ietf:params:oauth:grant-type:jwt-bearer`, with an RS256 `assertion` signed by the key of the app's `certificate`. `iss` is the consumer key, `sub` the username, and `aud` the emulator URL, `https://login.salesforce.com`, or `https://test.salesforce.com`. The user must have approved the app once on the approval page unless the app is `pre_authorized`. The response has no refresh token, `signature`, or `issued_at`.
* `POST /services/oauth2/revoke` (and `GET` with `?token=`) revokes an access or refresh token; revoking a refresh token revokes the access tokens it made. An unknown token answers HTTP 400.
* `POST /services/oauth2/introspect` with client authentication describes a live token of the same app (or of any app, with `introspect_all_tokens`): `active`, `scope`, `client_id`, `username`, `sub`, `token_type`, `exp`, `iat`, and `nbf`. Anything else is `{ "active": false }`.
* `GET` or `POST /services/oauth2/userinfo` returns the OpenID Connect claims of the token's user, taking a bearer token or `?access_token=`.
* `GET /.well-known/openid-configuration` and `GET /id/keys` serve discovery and the key that signs ID tokens.

Token responses have the service's fields in its order: `access_token`, `refresh_token` (with the `refresh_token` or `offline_access` scope), `signature` (base64 HMAC-SHA256 of `id` and `issued_at`, keyed by the consumer secret), `scope`, `id_token` (with the `openid` scope), `instance_url`, `id` (the identity URL), `token_type`, and `issued_at`. ID tokens are RS256, last five minutes, carry `at_hash` and the request's `nonce`, and hold the claims the `profile`, `email`, `phone`, and `address` scopes release.

An access token lapses after the app's `session_timeout_minutes` (120 by default) without use, and a use in the second half of that time starts it again. Leaving `scope` out of a request grants every scope the app lists. `full` covers every scope but `refresh_token`, and `offline_access` is another name for `refresh_token`.

Errors are JSON `{ "error", "error_description" }` with HTTP 400, as the service sends them: `unsupported_grant_type` (also for a flow the app has turned off), `redirect_uri_missing`, `invalid_client_id`, `invalid_client` (a wrong or missing secret), `redirect_uri_mismatch`, and `invalid_grant` for a bad code, verifier, refresh token, assertion, or password. The codes and the `unsupported_grant_type`, `redirect_uri_missing`, `invalid_client_id`, `invalid assertion`, and `expired authorization code` messages match `login.salesforce.com`; the wording of the other descriptions is Baseline's.

## Versions

The emulator serves versions 31.0 (Summer '14) to 68.0 (Winter '27). `GET /services/data` lists each with its release label, and `/services/data/latest/...` is 68.0. Versions 7.0 to 30.0 answer HTTP 410 with `GONE`, as Salesforce retired them; anything else under `/services/data` answers HTTP 404 `NOT_FOUND`. Responses carry URLs for the version the request used. Features that arrived later are missing from earlier versions, as on the service: batch and tree requests need 34.0, composite 38.0, sObject Collections 42.0, collection upserts 46.0, graphs 50.0, and creating a record with `POST .../sobjects/{type}/Id` 37.0.

## Records

Rows live at `/services/data/vXX.X/sobjects/{type}/{id}`:

* `POST /sobjects/{type}` creates a record and answers HTTP 201 with `{ id, success, errors }`.
* `GET .../{id}` returns the record with `attributes` and every field in describe order, or the fields named in `?fields=` (plus `Id`).
* `PATCH .../{id}` updates and answers HTTP 204. `DELETE .../{id}` moves the record to the Recycle Bin and answers HTTP 204.
* `POST .../{id}?_HttpMethod=PATCH` and `?_HttpMethod=DELETE` work for clients that cannot send those methods.
* IDs are read in their 15 and 18 character forms, and an 18 character ID in any letter case reaches its record. Responses always use 18 characters.

Saves are checked in the service's order, with its error codes and wording:

| Problem | Error |
| - | - |
| A field the object does not have | `INVALID_FIELD` `No such column 'X' on sobject of type T` |
| A value of the wrong JSON type | `JSON_PARSER_ERROR` with the line and column of the value |
| A read only or system field | `INVALID_FIELD_FOR_INSERT_UPDATE` |
| A required field left out | `REQUIRED_FIELD_MISSING` `Required fields are missing: [A, B]` |
| Text over the field's length | `STRING_TOO_LONG` |
| A value outside a restricted picklist | `INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST` |
| A number with too many digits | `NUMBER_OUTSIDE_VALID_RANGE` |
| A bad email | `INVALID_EMAIL_ADDRESS` |
| A lookup to the wrong object or to no record | `MALFORMED_ID` `Label: id value of incorrect type: X` |
| A lookup to a deleted record | `ENTITY_IS_DELETED` |
| A taken unique or external ID value | `DUPLICATE_VALUE` |
| An owner who is inactive | `INACTIVE_OWNER_OR_USER` |

Values are read leniently where Salesforce is lenient: numeric strings for numbers, `"true"` and `"false"` for checkboxes, a date for a date and time, and empty text as null. Currency, percent, and number values round half up to the field's scale. Date and time values come back as `2026-07-12T17:49:01.000+0000` in UTC.

The service keeps some fields itself: `Name` on Contact, Lead, and User from the first and last names; `Probability`, `ForecastCategoryName`, `IsClosed`, `IsWon`, and `ExpectedRevenue` on Opportunity from the stage; `IsClosed` and `ClosedDate` on Case from the status; `IsClosed`, `CompletedDateTime`, `IsHighPriority`, and `AccountId` on Task. Case numbers start at `00001001`, and auto number fields follow their display format.

Deleting an Account deletes its contacts, opportunities, and cases; deleting a record deletes its tasks and its master-detail children. Other lookups to a deleted record are cleared.

### Conditional requests

Account rows carry an `ETag`. `If-Match` and `If-None-Match` on Account rows answer HTTP 412 `PRECONDITION_FAILED` on writes that do not match, and HTTP 304 on reads that do. `If-Modified-Since` (HTTP 304 on reads) and `If-Unmodified-Since` (HTTP 412 on writes) work on every object.

## External IDs and relationships

Fields marked `external_id` in the seed (and `Id`) name records in the path:

* `GET /sobjects/{type}/{field}/{value}` reads the record. Several matches answer HTTP 300 with their URLs.
* `PATCH /sobjects/{type}/{field}/{value}` upserts: HTTP 201 with `created: true` when nothing matched, and HTTP 200 with `created: false` when one record matched (HTTP 204 before version 46.0). `?updateOnly=true` answers HTTP 404 instead of creating.
* `DELETE /sobjects/{type}/{field}/{value}` deletes the matching record.
* A field that is not an external ID answers HTTP 404 `Provided external ID field does not exist or is not accessible: X`.

In a write body, a relationship name with an external ID sets a lookup by that value: `"Account": { "External_Id__c": "ACME-1" }`. A polymorphic lookup such as Task `What` needs `attributes.type`. A value that matches nothing answers `Foreign key external ID: X not found for field F in entity T`.

`GET /sobjects/{type}/{id}/{relationship}` follows a relationship: a lookup returns the parent record (and `PATCH` and `DELETE` act on it), and a child relationship such as `Contacts` returns the children as a query result, paged like a [query](#paging).

## SOQL

`GET /services/data/vXX.X/query?q=<SOQL>` runs a query; `GET .../queryAll?q=<SOQL>` also returns deleted records, with `IsDeleted` true. jsforce's `conn.query()`, `find()`, `include()`, `count()`, and `query(soql, { scanAll: true })` work unchanged.

```ts theme={null}
const result = await conn.query(
  "SELECT Name, Account.Name, (SELECT Subject FROM Tasks) FROM Contact WHERE Account.Industry = 'Energy' ORDER BY LastName",
);
```

### What a query may hold

| Clause | Supported |
| - | - |
| `SELECT` | Fields, parent paths up the lookups (`Account.Owner.Username`), child subqueries by relationship name (`(SELECT LastName FROM Contacts)`), `FIELDS(ALL)`, `FIELDS(STANDARD)`, `FIELDS(CUSTOM)` (from 51.0), `TYPEOF` (from 46.0), `toLabel()`, aggregates (`COUNT()`, `COUNT(field)`, `COUNT_DISTINCT()`, `SUM()`, `AVG()`, `MIN()`, `MAX()`) and date functions with aliases |
| `FROM` | Any standard or custom object, with an alias (`FROM Contact c`) |
| `USING SCOPE` | `everything`, `mine`, and `mine_and_my_groups` (the user's own records) |
| `WHERE` | `=`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `LIKE`, `IN`, `NOT IN`, `INCLUDES`, `EXCLUDES`, `AND`, `OR`, `NOT`, parentheses, parent paths, semi-joins and anti-joins (`Id IN (SELECT AccountId FROM Contact)`), date functions, `toLabel()` |
| `WITH` | `SECURITY_ENFORCED`, `USER_MODE`, and `SYSTEM_MODE` are accepted; Baseline has no field level security, so they change nothing |
| `GROUP BY`, `HAVING` | Fields and date functions; `HAVING` on aggregates and grouped values |
| `ORDER BY` | Several keys, `ASC`/`DESC`, `NULLS FIRST`/`NULLS LAST`, parent paths |
| `LIMIT`, `OFFSET` | `OFFSET` up to 2,000 |
| `FOR VIEW`, `FOR REFERENCE` | Record the view for the [recent items](#recent-items) |

Values follow SOQL's rules:

* Text compares without case. Picklists sort in the order of their values, not alphabetically.
* `null` is a value for `=` and `!=`: `Industry != 'Energy'` includes records with no industry, and `NOT IN` keeps them too. Ranges (`<`, `>`, and the rest) and `LIKE` never match an empty field.
* `LIKE` takes `%` and `_` wildcards and `\%` and `\_` for the characters themselves, without case.
* `INCLUDES ('A;B', 'C')` on a multi-select picklist means (A and B) or C.
* Filter values must fit the field: quoted text and IDs, bare numbers and booleans, bare dates (`2026-01-31`) for date fields and bare date/times (`2026-01-31T09:00:00Z`) for date/time fields. IDs are read in 15 or 18 characters.
* Date literals (`TODAY`, `YESTERDAY`, `LAST_WEEK`, `THIS_MONTH`, `LAST_90_DAYS`, `LAST_N_DAYS:30`, `NEXT_N_QUARTERS:2`, `THIS_FISCAL_YEAR`, and the rest) cover days in the running user's time zone (`TimeZoneSidKey`). `LAST_N_DAYS:n` covers today and the n days before it; weeks start on Sunday and the fiscal year is the calendar year.
* Date functions (`CALENDAR_YEAR`, `CALENDAR_QUARTER`, `CALENDAR_MONTH`, `DAY_IN_MONTH`, `DAY_IN_WEEK`, `DAY_IN_YEAR`, `DAY_ONLY`, `HOUR_IN_DAY`, `WEEK_IN_MONTH`, `WEEK_IN_YEAR`, and the `FISCAL_` forms) read date/times in UTC, or in the user's time zone inside `convertTimezone()`.
* Child subqueries nest up to five levels from version 58.0, and one level before.

### Results

Records come back with `attributes` and the fields in the order selected, with the API's spelling of each name. Parent fields nest under the relationship name (`"Account": { "attributes": ..., "Name": "Acme" }`), or `null` when there is no parent. A child subquery is a query result (`{ totalSize, done, records }`), or `null` when there are no children. A polymorphic relationship such as Task `What` reaches the Name object's fields (`Id`, `Name`, `Type`, `Email`, and the like) with the actual object's type in `attributes`.

`SELECT COUNT() FROM ...` answers `{ totalSize, done: true, records: [] }`. Other aggregate queries return `AggregateResult` records keyed by alias, by field name for grouped fields, or `expr0`, `expr1`, and on for unnamed expressions. They never page, and more than 2,000 groups is an error.

### Paging

Results page by the batch size in `Sforce-Query-Options: batchSize=N` (200 to 2,000; 2,000 when not given). Child records count toward a page, which always holds at least one record. When more remain, `nextRecordsUrl` is `/services/data/vXX.X/query/01g...-N`; `GET` it (or the same locator under `queryAll`) for the next page. A locator belongs to the user who ran the query, lasts 15 minutes unused, and closes after its last page; a user keeps ten open at most. Locators live in memory, so they do not survive a restart. A closed, expired, or unknown locator answers `INVALID_QUERY_LOCATOR`.

`GET /query?explain=<SOQL>` returns a one step `TableScan` plan with the rows the query would return and the object's record count.

### Query errors

Errors carry the line of the query, a caret under the spot, and `ERROR at Row:N:Column:N`, as the service writes them:

| Problem | Error |
| - | - |
| A syntax error, or AND and OR mixed without parentheses | `MALFORMED_QUERY` `unexpected token: 'OR'` (or `<EOF>`) |
| An unknown object | `INVALID_TYPE` `sObject type 'X' is not supported. ...` |
| An unknown field | `INVALID_FIELD` `No such column 'X' on entity 'T'. ...` |
| An unknown relationship | `INVALID_FIELD` `Didn't understand relationship 'X' in field path. ...`, or `INVALID_TYPE` `... in FROM part of query call` for a subquery |
| A field selected twice | `INVALID_FIELD` `duplicate field selected: X` |
| A value of the wrong type | `INVALID_FIELD` `value of filter criterion for field 'X' must be of type dateTime and should not be enclosed in quotes` |
| A malformed ID | `INVALID_QUERY_FILTER_OPERATOR` `invalid ID field: X` |
| `LIKE` on an ID | `INVALID_QUERY_FILTER_OPERATOR` `invalid operator on id field` |
| A long text area in `WHERE` | `INVALID_FIELD` `field 'X' can not be filtered in a query call` |
| A field neither grouped nor aggregated | `MALFORMED_QUERY` `Field must be grouped or aggregated: X` (`Ordered field must be ...` in `ORDER BY`) |
| An alias outside an aggregate query | `MALFORMED_QUERY` `Only aggregate expressions use field aliasing` |
| `FIELDS(ALL)` or `FIELDS(CUSTOM)` without `LIMIT` of 200 or less | `MALFORMED_QUERY` `The SOQL FIELDS function must have a LIMIT of at most 200` |
| `OFFSET` over 2,000 | `NUMBER_OUTSIDE_VALID_RANGE` `Maximum SOQL offset allowed is 2000` |

Salesforce does not publish the wording of every query error. Where it does not (operators a field type does not take, semi-join shapes, nesting depth, unsupported scopes), the messages are Baseline's.

## Recent items

`FOR VIEW` sets the running user's `LastViewedDate` and `LastReferencedDate` on the records a query returns, and `FOR REFERENCE` sets `LastReferencedDate`. The dates belong to each user, as on the service, and API reads do not change them. `GET /services/data/vXX.X/recent?limit=N` (up to 200) lists the user's most recently viewed or referenced records with `Id` and name, and `GET /sobjects/{type}` lists them for one object in `recentItems`.

## Describe

* `GET /sobjects` lists every object with its flags and URLs (`maxBatchSize` 200).
* `GET /sobjects/{type}` returns the object's summary and its recently viewed records.
* `GET /sobjects/{type}/describe` returns fields with types, lengths, picklist values, references, relationship names, and the `createable`, `updateable`, `nillable`, `externalId`, and `unique` flags, plus child relationships and the master record type.
* Both describes answer HTTP 304 to an `If-Modified-Since` after the last schema change.

## Replication

* `GET /sobjects/{type}/updated?start=...&end=...` lists IDs changed in the window, for windows that start in the last 30 days.
* `GET /sobjects/{type}/deleted?start=...&end=...` lists deleted IDs with their deletion times, for windows that start in the last 15 days.

Seconds are ignored, as on the service. A missing parameter is `MISSING_ARGUMENT`; an unreadable, reversed, or too early window is `INVALID_REPLICATION_DATE`.

## sObject Collections

`/services/data/vXX.X/composite/sobjects` (version 42.0 and later):

* `POST` creates, `PATCH` updates, and `DELETE ?ids=...` deletes up to 200 records, each with its own result in request order.
* `GET /composite/sobjects/{type}?ids=...&fields=...` and `POST` with `{ ids, fields }` read up to 2,000 records; IDs that match nothing come back as `null`.
* `PATCH /composite/sobjects/{type}/{externalIdField}` upserts up to 200 records (version 46.0 and later).
* Records of different types are processed in chunks of one type; a request may have ten chunks.
* With `allOrNone: true`, any failure rolls back every write, and the records that had succeeded report `ALL_OR_NONE_OPERATION_ROLLED_BACK`. A rolled back create reports no ID.

Each result is `{ id, success, errors: [{ statusCode, message, fields }] }`. Request level problems use the service's codes: `INVALID_TYPE` for a record without `attributes.type`, `EXCEEDED_ID_LIMIT` past the size limits, `INVALID_BATCH_SIZE` past ten chunks, `MISSING_ARGUMENT` for an update without `Id`, and `DUPLICATE_EXTERNAL_ID` for an upsert that names the same value twice.

## Composite requests

Four resources run several requests in one call. Subrequests go through the same routes as direct calls, as the caller, so every result and error matches what the same call made alone would return. `GET /composite` lists the ones the version offers.

### Composite

`POST /composite` (version 38.0 and later) runs up to 25 subrequests in order, at most five of them queries (`query`, `queryAll`) or sObject Collections calls:

```json theme={null}
{
  "allOrNone": true,
  "compositeRequest": [
    { "method": "POST", "url": "/services/data/v68.0/sobjects/Account", "referenceId": "NewAccount", "body": { "Name": "Globex" } },
    { "method": "POST", "url": "/services/data/v68.0/sobjects/Contact", "referenceId": "NewContact", "body": { "LastName": "Smith", "AccountId": "@{NewAccount.id}" } },
    { "method": "GET", "url": "/services/data/v68.0/query?q=SELECT+Id+FROM+Contact+WHERE+AccountId='@{NewAccount.id}'", "referenceId": "Contacts" }
  ]
}
```

* Each subrequest takes `method` (`GET`, `POST`, `PATCH`, `PUT`, `DELETE`), a `url` starting with `/services/data/vXX.X/`, a `referenceId`, and optionally `body` and `httpHeaders` (such as `If-Match` or `Sforce-Query-Options`).
* `@{referenceId.path}` in a later subrequest's URL or body reads a value from an earlier response: `@{NewAccount.id}`, `@{Contacts.records[0].Id}`, or `@{Batch[1].id}` for an sObject Collections result. Names are case sensitive. A body string that is one whole reference takes the value with its JSON type; a reference inside longer text is spliced in as text.
* A reference that names no value (the subrequest failed, comes later, or has nothing at that path) fails its subrequest with HTTP 400 `PROCESSING_HALTED` and `Invalid reference specified. No value for NewAccount.id found in NewAccount.`
* Without `allOrNone`, every subrequest runs, and only those that reference a failed one fail with it.
* With `allOrNone: true`, the first failure stops the run and rolls back every write (and every `FOR VIEW` mark). The failed subrequest keeps its error; every other one reports HTTP 400 `PROCESSING_HALTED` with `The transaction was rolled back since another operation in the same transaction failed.`
* From version 52.0, a reference ID must start with a letter or digit and hold only letters, digits, and underscores.
* `collateSubrequests` is accepted and changes nothing.
* The whole request counts as one API call.

The response is `{ compositeResponse: [{ body, httpHeaders, httpStatusCode, referenceId }] }` in request order, with HTTP 200 even when subrequests fail. A create's `httpHeaders` carry its `Location`; a read of an Account row carries its `ETag`.

### Batch

`POST /composite/batch` (version 34.0 and later) runs up to 25 independent subrequests, each `{ method, url, richInput }` with the URL starting at the version (`v68.0/sobjects/Account`). Subrequests cannot reference each other and nothing rolls back. The response is `{ hasErrors, results: [{ statusCode, result }] }`. With `haltOnError: true`, the subrequests after the first failure are not run and report HTTP 412 `BATCH_PROCESSING_HALTED` with `Batch processing halted per request`. Each subrequest counts as an API call. Binary parts (`binaryPartName`) are not supported.

### sObject Tree

`POST /composite/tree/{type}` (version 34.0 and later) creates records of the URL's type with their children in one transaction:

```json theme={null}
{
  "records": [
    {
      "attributes": { "type": "Account", "referenceId": "ref1" },
      "Name": "SampleAccount1",
      "Contacts": { "records": [{ "attributes": { "type": "Contact", "referenceId": "ref2" }, "LastName": "Smith" }] }
    },
    { "attributes": { "type": "Account", "referenceId": "ref3" }, "Name": "SampleAccount2" }
  ]
}
```

Children sit under their child relationship name and get the parent link filled in. Records are created level by level: every root, then every child, so the result above lists `ref1`, `ref3`, then `ref2`. Success is HTTP 201 with `{ hasErrors: false, results: [{ referenceId, id }] }`. The first failing record undoes the whole request, and the HTTP 400 response names only it: `{ hasErrors: true, results: [{ referenceId, errors: [{ statusCode, message, fields }] }] }`. A repeated reference ID gets one result per instance. A request holds up to 200 records of up to five types, at most five levels deep, and no record may set `Id`.

### Graph

`POST /composite/graph` (version 50.0 and later) takes `{ graphs: [{ graphId, compositeRequest: [...] }] }`. Each graph runs like a composite request with `allOrNone: true`, and graphs succeed or fail apart. The response is `{ graphs: [{ graphId, graphResponse: { compositeResponse }, isSuccessful }] }`. Graph nodes can only create (`POST /sobjects/{type}`) and read, update, upsert, or delete records by ID or external ID. Limits: 75 graphs, 500 nodes per graph and in all, references at most 15 nodes deep, and at most 15 different nodes by API version, method, and object. After more than 14 graphs fail, the rest are not run and report `PROCESSING_HALTED`.

### Composite errors

A malformed body answers HTTP 400 `JSON_PARSER_ERROR` before anything runs. A repeated reference ID or graph ID, an invalid reference ID, a URL outside `/services/data/vXX.X/`, a composite resource inside another, or an unsupported graph node answers `INVALID_INPUT`, and a request over a size limit answers `LIMIT_EXCEEDED`. Those codes come from Salesforce's error code list, but the messages are Baseline's: Salesforce does not publish them.

Composite transactions hold the org to themselves while they run, so a rollback never undoes another client's writes.

## Org limits and identity

* `GET /limits` reports `DailyApiRequests` from the calls made, `DataStorageMB` from the records stored (2 KB each), and the other limits at their Developer Edition values.
* `GET /limits/recordCount` (and `?sObjects=Account,Contact`) counts the records of each object.
* `GET /id/{orgId}/{userId}` returns the session user's identity: username, display name, email, the org's REST and sObjects URLs, `active`, `user_type`, locale, and `utcOffset`. It takes a bearer token or `?oauth_token=`, and answers as the service does with text errors: `Missing_OAuth_Token` (403) without a token, `Bad_OAuth_Token` (403) for an unknown one, `Bad_Id` (404), `Wrong_Org` (403), and `No_Access` (403) for another user.
* `GET /services/data/vXX.X` lists the version's resources, with the identity URL of the session's user.

Errors are a JSON array of `{ message, errorCode, fields? }`, as the REST API returns them. Unknown resources under a version answer HTTP 404 `NOT_FOUND`, and a method a resource does not take answers HTTP 405 naming the allowed ones.

## Seed config

```json theme={null}
{
  "salesforce": {
    "org": {
      "name": "Acme"
    },
    "users": [
      {
        "username": "sam@example.com",
        "first_name": "Sam",
        "last_name": "Seller",
        "title": "Account Executive",
        "password": "s3cret",
        "security_token": "TOKEN42"
      }
    ],
    "connected_apps": [
      {
        "name": "Web App",
        "client_id": "3MVG9-baseline-web-app",
        "client_secret": "BASELINE-WEB-SECRET",
        "callback_urls": ["http://localhost:3000/oauth/callback"],
        "scopes": ["api", "refresh_token", "openid", "profile", "email"]
      },
      {
        "name": "Integration",
        "scopes": ["api"],
        "client_credentials_user": "sam@example.com"
      }
    ],
    "access_tokens": [
      {
        "token": "00D-baseline-local-token"
      },
      {
        "token": "00D-sales-rep-token",
        "user": "sam@example.com"
      }
    ],
    "fields": {
      "Account": [
        {
          "name": "External_Id__c",
          "type": "text",
          "length": 40,
          "external_id": true,
          "unique": true
        },
        {
          "name": "Region__c",
          "type": "picklist",
          "values": ["AMER", "EMEA", "APAC"],
          "restricted": true
        }
      ]
    },
    "objects": [
      {
        "name": "Invoice__c",
        "label": "Invoice",
        "name_field": {
          "label": "Invoice Number",
          "type": "autonumber",
          "format": "INV-{0000}"
        },
        "fields": [
          {
            "name": "Account__c",
            "type": "master_detail",
            "reference_to": "Account"
          },
          {
            "name": "Total__c",
            "type": "currency",
            "precision": 12,
            "scale": 2
          },
          {
            "name": "Paid__c",
            "type": "checkbox"
          }
        ]
      }
    ],
    "records": {
      "Account": [
        {
          "Name": "Acme",
          "Industry": "Technology",
          "External_Id__c": "ACME-1",
          "Region__c": "AMER"
        }
      ],
      "Contact": [
        {
          "FirstName": "Ada",
          "LastName": "Lovelace",
          "Email": "ada@example.com",
          "Account": {
            "External_Id__c": "ACME-1"
          }
        }
      ],
      "Opportunity": [
        {
          "Name": "Acme renewal",
          "StageName": "Prospecting",
          "CloseDate": "2026-12-31",
          "Amount": 12000,
          "Account": {
            "Name": "Acme"
          },
          "Owner": {
            "Username": "sam@example.com"
          }
        }
      ],
      "Invoice__c": [
        {
          "Account__r": {
            "External_Id__c": "ACME-1"
          },
          "Total__c": 1200.5
        }
      ]
    }
  }
}
```

* `users`: [admin@example.com](mailto:admin@example.com) always exists and is the org's admin user.
* `password`: For the username-password flow; send the password followed by the security token.
* `connected_apps`: Connected apps for the OAuth flows.
* `access_tokens`: Bearer tokens that open a session as a user (the admin user when user is left out).
* `fields`: Custom fields on standard objects.
* `objects`: Custom objects with their fields.
* `records`: Records by object, created in order. Name a parent through its relationship by any of its fields.

| Key | Meaning |
| - | - |
| `org` | `id` (an 18 character `00D` ID), `name`, and `instance_name` |
| `users` | `username` (required), `email`, `first_name`, `last_name`, `alias`, `title`, `department`, `phone`, `active`, `time_zone`, `locale`, and for the username-password flow `password` and `security_token` (the admin user may take them too) |
| `connected_apps` | `name` (required), `client_id` and `client_secret` (derived from the name when left out, the same on every start), `callback_urls`, `scopes` (default `api`, `refresh_token`, `openid`), `certificate` (a PEM certificate or public key, for the JWT bearer flow), `pre_authorized`, `require_secret` and `require_secret_for_refresh` (default true), `require_pkce`, `client_credentials_user`, `password_flow`, `session_timeout_minutes` (default 120), `introspect_all_tokens` |
| `access_tokens` | `token` (8 characters or more) and the `user` it acts as, by username |
| `fields` | Custom fields on standard objects, by object name |
| `objects` | Custom objects: `name` (ending in `__c`), `label`, `plural_label`, `key_prefix`, `name_field` (text, or an auto number with a `format`), and `fields` |
| `records` | Records by object name, created in order |

Field types are `text`, `textarea`, `email`, `phone`, `url`, `picklist`, `multipicklist`, `checkbox`, `number`, `currency`, `percent`, `date`, `datetime`, `lookup`, and `master_detail`. Each field takes `name` (ending in `__c`), `label`, `length`, `precision`, `scale`, `required`, `unique`, `external_id`, `case_sensitive`, `values` and `restricted` for picklists, `default`, and for lookups `reference_to`, `relationship_name`, and `child_relationship_name`. A master-detail child is deleted with its parent.

Seeding is idempotent: on a restart with saved state, a record that matches an existing one by an external ID or unique field, or by name, is skipped. Seed errors start with `salesforce seed:` and name the entry.

## Inspector

`GET /` serves a tabbed inspector with a tab per standard and custom object (with each record's fields), a SOQL tab that runs a query as the admin user and shows the rows or the error, users with connected apps, approvals, and sessions, the Recycle Bin, and the custom schema.

## Limits

No SOSL (`/search`), and no SOAP API, so jsforce's `login()` works only through the OAuth username-password flow (with `clientId` and `clientSecret` set). The OAuth server has no device, SAML bearer, asset token, or token exchange flows, no refresh token rotation or expiry policies, no IP restrictions, and no login screen: the approval page picks a user without a password. Batch requests take JSON bodies only, without binary parts. SOQL has no `GROUP BY ROLLUP` or `CUBE`, `FORMAT()`, `convertCurrency()`, `WITH DATA CATEGORY`, `FOR UPDATE`, `UPDATE TRACKING`, or the `team`, `delegated`, and territory scopes, and child subquery results do not page (every child comes back in the parent's row). There are no record types, sharing rules, field level security, validation rules, triggers, or flows, and every session can see and change every record.

## Conformance

| | |
| - | - |
| Routes | 52 supported or partial, 0 planned |
| Route gate | fail |
| Spec gate | no public spec |
| SDKs tested | `jsforce@3.12.0` |
| Dual mode | not yet |

See [CONFORMANCE.md](https://github.com/deploybaseline/cli/blob/main/CONFORMANCE.md) for every provider.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.