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
--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
SendAuthorization: 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
apiorfullscope; others get HTTP 401INVALID_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_tokensopen 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) andWWW-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 underconnected_apps (see Seed config), then use the same endpoints and clients as against login.salesforce.com. With jsforce, set loginUrl to the emulator:
GET /services/oauth2/authorizewithresponse_typeofcode,token, ortoken id_token, plusclient_id,redirect_uri,scope,state,nonce,login_hint, andcode_challengewithcode_challenge_method=S256. It checks the response type, then the client, then that the callback is one of the app’scallback_urlsexactly; those errors answer on the page with HTTP 400 and a form encoded body such aserror=invalid_client_id&error_description=client%20identifier%20invalid, as the service does. Once the callback is known,invalid_scopeand PKCE errors go back to it. Otherwise an Allow Access? page lists the scopes and a button per active user (thelogin_hintuser first) and a Deny link, which sendsaccess_denied. The code flow returnscodeandstatein 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/tokenwith the client secret in the form or by HTTP Basic, for these grants:authorization_code, which needsredirect_uriand, when the authorize request sent a challenge,code_verifier. The secret is required unless the app turns offrequire_secret.refresh_token, which returns a new access token and no new refresh token. The secret is required unless the app turns offrequire_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 setspassword_flow. The response has noscopeand no refresh token.client_credentials, running as the app’sclient_credentials_user.urn:ietf:params:oauth:grant-type:jwt-bearer, with an RS256assertionsigned by the key of the app’scertificate.issis the consumer key,subthe username, andaudthe emulator URL,https://login.salesforce.com, orhttps://test.salesforce.com. The user must have approved the app once on the approval page unless the app ispre_authorized. The response has no refresh token,signature, orissued_at.
POST /services/oauth2/revoke(andGETwith?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/introspectwith client authentication describes a live token of the same app (or of any app, withintrospect_all_tokens):active,scope,client_id,username,sub,token_type,exp,iat, andnbf. Anything else is{ "active": false }.GETorPOST /services/oauth2/userinforeturns the OpenID Connect claims of the token’s user, taking a bearer token or?access_token=.GET /.well-known/openid-configurationandGET /id/keysserve discovery and the key that signs ID tokens.
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 withattributesand every field in describe order, or the fields named in?fields=(plusId).PATCH .../{id}updates and answers HTTP 204.DELETE .../{id}moves the record to the Recycle Bin and answers HTTP 204.POST .../{id}?_HttpMethod=PATCHand?_HttpMethod=DELETEwork 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.
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 anETag. 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 markedexternal_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 withcreated: truewhen nothing matched, and HTTP 200 withcreated: falsewhen one record matched (HTTP 204 before version 46.0).?updateOnly=trueanswers 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.
"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.
nullis a value for=and!=:Industry != 'Energy'includes records with no industry, andNOT INkeeps them too. Ranges (<,>, and the rest) andLIKEnever match an empty field.LIKEtakes%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:ncovers 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 theFISCAL_forms) read date/times in UTC, or in the user’s time zone insideconvertTimezone(). - Child subqueries nest up to five levels from version 58.0, and one level before.
Results
Records come back withattributes 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 inSforce-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, andERROR 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 /sobjectslists every object with its flags and URLs (maxBatchSize200).GET /sobjects/{type}returns the object’s summary and its recently viewed records.GET /sobjects/{type}/describereturns fields with types, lengths, picklist values, references, relationship names, and thecreateable,updateable,nillable,externalId, anduniqueflags, plus child relationships and the master record type.- Both describes answer HTTP 304 to an
If-Modified-Sinceafter 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.
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):
POSTcreates,PATCHupdates, andDELETE ?ids=...deletes up to 200 records, each with its own result in request order.GET /composite/sobjects/{type}?ids=...&fields=...andPOSTwith{ ids, fields }read up to 2,000 records; IDs that match nothing come back asnull.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 reportALL_OR_NONE_OPERATION_ROLLED_BACK. A rolled back create reports no ID.
{ 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), aurlstarting with/services/data/vXX.X/, areferenceId, and optionallybodyandhttpHeaders(such asIf-MatchorSforce-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_HALTEDandInvalid 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 everyFOR VIEWmark). The failed subrequest keeps its error; every other one reports HTTP 400PROCESSING_HALTEDwithThe 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.
collateSubrequestsis accepted and changes nothing.- The whole request counts as one API call.
{ 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:
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 400JSON_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 /limitsreportsDailyApiRequestsfrom the calls made,DataStorageMBfrom 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, andutcOffset. 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), andNo_Access(403) for another user.GET /services/data/vXX.Xlists the version’s resources, with the identity URL of the session’s user.
{ 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.