Skip to main content
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 (3.12.0).

At a glance

Start

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 [email protected], 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:

URL mapping

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). 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 ([email protected]), 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), then use the same endpoints and clients as against login.salesforce.com. With jsforce, set loginUrl to the emulator:
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: 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.

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.

What a query may hold

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: 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:
  • 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:
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

  • users: [email protected] 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.
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

See CONFORMANCE.md for every provider.