Salesforce API

Documentation

Every Salesforce call this demo makes

The demo talks to Salesforce from three places, and which place matters more than the endpoints do: the Auth0 Action is the only writer of consent for anyone with an identity, the app reads it back, and one bounded module covers guests who have no identity yet. Grouped by caller for that reason.

Run a query against the org, right now

These are read-only and fixed in code — there is no box to type SOQL into, by design. Writes on this page are documented, never executed.

Sign in to probe your own records — your Contact, your consent history and your bookings. Those queries are always filtered to the signed-in account.

Getting a token

Both the Auth0 Action and the Express app

Every call below is authenticated the same way: an OAuth 2.0 Client Credentials grant against a Connected App, with a Run-As user supplying the record-level identity.

POST /services/oauth2/token

grant_type=client_credentials&client_id=…&client_secret=…

Exchanges the Connected App credentials for a bearer token. No user is signed in to Salesforce — the Run-As user provides the identity every subsequent write is attributed to.

Gotcha: The host MUST be the org My Domain URL (…my.salesforce.com), not login.salesforce.com. Client Credentials is rejected at the generic endpoint.

BASE /services/data/v67.0

instance_url from the token response + /services/data/v{API_VERSION}

All REST calls hang off the instance URL returned with the token, never a hard-coded host.

The Auth0 post-login Action

auth0/terraform/actions/postLogin.js — raw REST via axios

This is the integration brain. It runs inside Auth0 on every login and is the ONLY writer of consent for anyone who has an Auth0 identity. It uses axios rather than jsforce because a jsforce bundle is a poor fit for an Action cold start. Calls are listed in the order one login fires them.

GET /query

SELECT Id FROM Individual WHERE Contact_Point_Key__c = '…' AND Auth0_User_Id__c = null

Reconciliation (P10): looks for an Individual created by an earlier anonymous booking. Runs BEFORE the upsert below — after it, a second Individual would already exist.

Gotcha: Gated on email_verified. An unverified claim inheriting another party’s history is account takeover.

PATCH /sobjects/Individual/{id}

{ "Auth0_User_Id__c": "auth0|…" }

Adopts that anonymous Individual by stamping the Auth0 id onto it. One-way: once set, the anonymous path can never match it again. Prior consent rows keep their original ConsentGiverId.

PATCH /sobjects/Service_Booking__c/{id}

{ "Auth0_User_Id__c": "auth0|…" }

Carries any bookings made as a guest across to the now-known customer, so /bookings can show them.

GET /query

SELECT Id, Name FROM AuthorizationFormText … ORDER BY CreatedDate DESC LIMIT 1

Reads the current policy version. Salesforce is the source of truth, so adding a text row is the whole policy bump — no Action redeploy and no CURRENT_CONSENT_VERSION secret.

Gotcha: Wrapped in try/catch and fails OPEN — a Salesforce outage must not block an existing user’s login.

PATCH /sobjects/Individual/Auth0_User_Id__c/{auth0 user id}

{ "LastName": "…" }

Upsert by external ID. This is what makes re-login idempotent — it resolves to the existing record instead of creating a duplicate Individual on every login.

PATCH /sobjects/Contact/Auth0_User_Id__c/{auth0 user id}

{ "LastName": "…", "Email": "…", "IndividualId": "…" }

The Contact "created on signup", linked to its Individual. Same external-ID upsert, same reason.

GET → POST /query then /sobjects/ContactPointEmail

{ "ParentId": "…", "EmailAddress": "…", "IsPrimary": true }

ContactPointConsent cannot be written without a contact point, so the email is looked up and created if missing. This is the record per-purpose consent actually binds to.

GET /query

SELECT Id, Name FROM DataUsePurpose WHERE Name IN ('Marketing','Law 25','Data Use')

Resolves the three seeded purposes to their ids so each decision can be written against one.

GET → POST /query then /sobjects/AuthorizationFormConsent

{ "ConsentGiverId": "…", "AuthorizationFormTextId": "…", "Status": "Signed", "ConsentCapturedDateTime": "…" }

The policy acceptance itself, bound to one specific text version. Queried first so a re-login inside the same version does not double-write. A NEW row per version — prior rows are never updated.

Gotcha: Status is Seen/Signed/Rejected here. Per-purpose opt-in/out lives on ContactPointConsent, not this object.

POST /sobjects/ContactPointConsent

{ "ContactPointId": "…", "DataUsePurposeId": "…", "PrivacyConsentStatus": "OptIn|OptOut", "CaptureDate": "…" }

One row per purpose per capture — the actual marketing / Law 25 / data-use decisions.

Gotcha: PrivacyConsentStatus here, Status on AuthorizationFormConsent. Two different fields, easy to confuse.

The Express app

app/lib/salesforce.js — jsforce

The app reads consent back to prove it landed, and writes bookings. It writes NO consent for anyone with an Auth0 identity — that stays exclusively in the Action, because one writer is what makes the audit trail trustworthy.

SOQL Contact → AuthorizationFormConsent + ContactPointConsent

SELECT Id, IndividualId FROM Contact WHERE Auth0_User_Id__c = :uid

Resolves the signed-in user to their Individual, then fans out to the consent rows. Backs the dashboard and the /consent-data audit ledger.

Gotcha: ContactPointConsent is polymorphic — filter via ContactPointId IN (SELECT Id FROM ContactPointEmail WHERE ParentId = …), because ContactPoint.ParentId is not queryable.

SOQL AuthorizationForm + AuthorizationFormText

newest vN wins

The live policy version shown on /privacy and used to flag stale consent on /demo.

INSERT AuthorizationFormText

{ "AuthorizationFormId": "…", "Name": "v{n+1}" }

The "Bump policy version" button. Adding a row IS the policy change — everyone re-consents at their next login with no deploy anywhere.

UPSERT Service_Booking__c on Booking_Reference__c

{ …13 booking fields, "Consent_Version_At_Booking__c": "v35" }

Writes the booking and stamps the consent version in force at that moment, so two bookings either side of a policy bump carry different versions. Upsert on the reference makes a double-submitted confirm yield one row.

SOQL Service_Booking__c

WHERE Auth0_User_Id__c = :uid ORDER BY CreatedDate DESC

The /bookings read-back, showing each booking beside the consent version it was made under.

The guest consent writer

app/lib/anon-consent.js — jsforce

The one bounded exception to "only the Action writes consent". An anonymous booking has no Auth0 session, so the Action never runs and the app must capture consent itself. Keyed on a normalised email instead of an Auth0 id, so the two writers can never touch the same Individual.

UPSERT Individual on Contact_Point_Key__c

{ "Contact_Point_Key__c": "you@example.com", "LastName": "…" }

Creates the Individual immediately, with NO Auth0_User_Id__c. Deferring it to reconciliation would mean a second Individual later and a rewritten ConsentGiverId — what the audit trail forbids.

Gotcha: The email normalisation here must match the Action’s exactly. Diverge and reconciliation silently stops matching, with no error anywhere.

SOQL → INSERT ContactPointEmail

{ "ParentId": "…", "EmailAddress": "…", "IsPrimary": true }

The contact point the guest’s consent binds to, since there is no identity to bind it to yet.

SOQL → INSERT AuthorizationFormConsent + ContactPointConsent

Status "Signed", then one ContactPointConsent per purpose

Exactly the shape the Action writes, so an adopted guest’s history is indistinguishable from a registered user’s. Idempotent per (contact point, policy version).

Gotcha: If the policy version cannot be read, the guest booking is BLOCKED — the opposite of the Action’s fail-open. Capturing personal data with no recordable consent is the one thing to prevent.

Each probe spends one call against the org's daily API limit. The write calls above are documented only — nothing on this page mutates Salesforce.