WP Webhooks / Blog / Integrations

Connecting WordPress to Salesforce Without a Middleman

A WordPress Salesforce integration built directly on the REST API: the client credentials flow, upserting by external ID, and the org-wide API limits.

9 min 2026-09-03
#salesforce#integrations#api

TL;DR: Connecting WordPress to Salesforce is three decisions — how you get a token, which endpoint you call, and who absorbs the failure when Salesforce is slow.

  • Use the OAuth 2.0 client credentials flow. It is the server-to-server flow, needs no logged-in user, and the token response tells you which host to call.
  • Call the token response's instance_url, never the login host. This is the single most common first-integration bug.
  • Upsert by external ID rather than create. Salesforce answers 201 when it inserted and 204 when it updated, so a repeated delivery is harmless.
  • The daily API allowance is org-wide and shared across REST, SOAP and Bulk. Your form competes with every other integration in the org.
  • Never call Salesforce inline on the request that created the record. Queue it.

/ Auth

How do you authenticate a WordPress site to Salesforce?

Use the OAuth 2.0 client credentials flow. It exists for exactly this shape of integration: a server acting on its own behalf, with no human at a browser to approve anything. You configure an External Client App (or a classic Connected App) in Salesforce Setup, enable the client credentials flow, nominate a run-as user, and you get a consumer key and secret.

Two older approaches still show up in tutorials and both are worth skipping. The username-password flow embeds a password and a security token in your source and is reserved by Salesforce for special scenarios. The web server flow is designed for apps acting on behalf of a signed-in user, which is not what a form submission is.

The token exchange is a single POST. What matters is the response, not the request:

PHP — exchanging client credentials for a token

$res = wp_remote_post( 'https://MyDomain.my.salesforce.com/services/oauth2/token', [
    'timeout' => 15,
    'body'    => [
        'grant_type'    => 'client_credentials',
        'client_id'     => $consumer_key,
        'client_secret' => $consumer_secret,
    ],
] );

if ( is_wp_error( $res ) ) {
    return $res; // transport failure — worth retrying
}

$body = json_decode( wp_remote_retrieve_body( $res ), true );

// Both of these matter. The second one is the part people miss.
$token = $body['access_token'] ?? '';
$host  = $body['instance_url'] ?? '';

The instance_url in that response is the host every subsequent API call must use. It is not always the domain you authenticated against, it can differ per org, and it can change after a Salesforce migration. Hard-coding your My Domain and ignoring instance_url produces an integration that works in your sandbox and fails in someone else's production org.

Tokens from this flow are short-lived and carry no refresh token — you request a new one when the old one stops working. Treat a 401 as "fetch a fresh token and replay once", and anything still failing after that as a configuration problem rather than something to keep retrying.

FIG 01 — Token, version discovery, and the upsert that decides create or update

/ Endpoint

Which Salesforce endpoint should a WordPress site call?

For one record at a time, the sObject Rows by External ID resource, addressed with PATCH. The path carries the object, the external ID field, and the value:

PATCH /services/data/v64.0/sobjects/Contact/External_Id__c/{value}

Pin an API version deliberately. Salesforce ships three releases a year and each gets its own version number, and a version stays supported for years before retirement — as of Summer '25 the oldest supported version is v31.0. Rather than guessing, ask the org what it supports: a GET to /services/data/ returns the list of available versions, which is the honest way to choose one and the only way to notice an org is older than you assumed.

For volume, the sObject Collections resources take up to 200 records in a single call. That matters more than it sounds, because of the limit arithmetic in the next section.

Cyberpunk illustration: a heavy armoured spar driven straight across a dark gap and locked into a glowing coupling ring on the structure opposite, while the stripped, unlit relay platform that used to sit between the two hangs by slack cables beneath it.

/ Identity

How do you avoid creating duplicate Salesforce records?

Upsert on a field you control, rather than querying for a match and then deciding. An external ID is a custom field marked External ID and Unique in Salesforce — a WordPress user ID, an order number, an email address — and it turns the whole create-or-update question into one idempotent call.

The response tells you what happened, and the two codes are worth handling separately:

ResponseMeaningWhat to do
201 CreatedNo record matched the external ID, so Salesforce inserted oneStore the returned record id if you need it later
204 No ContentA record matched and was updatedNothing — success with an empty body is not an error
400 duplicate valueThe external ID matched more than one recordFix the field uniqueness in Salesforce; the payload is not the problem
401 UnauthorizedToken expired or invalidFetch a fresh token, replay once, then stop
403 REQUEST_LIMIT_EXCEEDEDThe org burned its daily API allowanceBack off hard — this is not your integration alone

The 204 catches people out. A successful update returns an empty body, so code that parses JSON unconditionally throws on the happy path and the delivery gets logged as a failure while Salesforce did exactly what was asked. Check the status before you decode.

The duplicate-value error is the other one worth naming. Salesforce documents a "Duplicate value found: <field> duplicates value on record with id" failure on PATCH upserts, and it appears most often when two deliveries for the same record land within milliseconds of each other. Serialising deliveries per record — which a queue does for free — removes it.

An upsert keyed on a field you own can be delivered twice with no consequence. A create cannot. Everything else about retry design follows from that one choice. — the rule that makes retries safe

/ Limits

What are the Salesforce API limits, and who else spends them?

Salesforce meters API calls per org on a 24-hour rolling basis, not per integration and not per calendar day. Enterprise Edition allows 100,000 calls plus 1,000 per licensed user; Unlimited allows 100,000 plus 5,000 per user. The pool is shared across REST, SOAP, Bulk and Connect — so your WordPress integration is drawing from the same budget as the org's data warehouse sync, its marketing tooling and every admin running a report through an API client.

Work out what your site actually spends before shipping. A form doing one upsert per submission at 200 submissions a day costs 200 calls — trivial. The same form doing a lookup, then an upsert, then a second call to attach a related record costs three times that, and a WooCommerce store firing on four order-status transitions each with a lookup is a different conversation entirely:

  • 200 submissions × 1 upsert = 200 calls/day
  • 200 submissions × 3 calls (lookup + upsert + related record) = 600 calls/day
  • 2,000 orders × 4 status changes × 3 calls = 24,000 calls/day — a quarter of the base allowance for one plugin

That last line is why sObject Collections matters. Batching those 24,000 calls at up to 200 records each brings the same work down to roughly 120 calls, and the difference between those two numbers is the difference between an integration nobody notices and one the Salesforce admin asks you to turn off.

Salesforce treats the daily cap as a soft limit briefly and then enforces it with REQUEST_LIMIT_EXCEEDED. Concurrency is metered separately: production orgs allow 25 simultaneous long-running requests, where "long-running" means 20 seconds or more.

/ Failure

Should the Salesforce call run inside the WordPress request?

No. Whatever created the record in WordPress — a form submission, a checkout, a user registration — is a request with a person waiting at the end of it, and an outbound API call inside it makes that person wait for Salesforce.

The reasoning is not about typical latency, it is about the tail. Salesforce is normally fast. But when it is slow, or the token endpoint is slow, or DNS hiccups, your checkout inherits that latency directly. With a 15-second timeout, a Salesforce incident becomes a 15-second checkout for every customer, and PHP-FPM workers pile up behind it. The correct shape is to record the intent, return immediately, and let a background worker own the delivery — that way a Salesforce outage delays data rather than breaking the site.

Which failures deserve a retry is a question with a precise answer, and getting it wrong is expensive in both directions. A 5xx, a 429 and a connection timeout are all worth repeating. A 400 caused by a bad field name will fail identically forever — retrying it five times just multiplies the API calls you are already rationing.

ConcernHand-rolled wp_remote_postWebhook Actions
Connector catalogueNone — you write every callAlso none. You point it at an endpoint you name; there is no Salesforce connector to pick from a list
Delivery timingInline — the visitor waits for SalesforceQueued — the request returns before the API call runs
Retry on 5xx / 429One attempt, then the data is goneExponential backoff, 5 attempts by default
Retry on 4xxRetries a doomed payload if you loop naivelyMarked failed immediately — a bad payload is not retried
Evidence of what was sentWhatever you remembered to error_log()Per-attempt log with request, response and replay
Consumer key and secretConstants in wp-config or the databaseNot these — the vault stores a finished header, not the exchange that mints one

The honest read of that table: the right-hand column is not a Salesforce integration you can pick off a shelf. There is no connector catalogue — you still decide the endpoint, the payload and the field mapping. What it removes is the queue, the backoff curve and the attempt log, which is the part that takes a week and is the part people skip.

The credential row deserves precision, because it is the one people misread. The plugin's Credentials Vault stores a finished authorization header — a bearer token, basic auth, or a raw value for a header you name — encrypted at rest, referenced from a webhook by id, injected only at dispatch and redacted out of the delivery log. Write-only means the secret goes in and only a masked hint comes back. What it does not do is perform the client credentials exchange, so the consumer key and secret stay with whatever mints the token. The vault is the right home for an API that authenticates with a static token instead, Pipedrive for instance.

try_it

Seeing it run beats reading about it. The live preview boots a throwaway WordPress with Webhook Actions already installed and demo deliveries sitting in the log — no signup, nothing left on your machine afterwards.

/ Exposure

What does Salesforce not protect you from?

Three things, and none of them show up in a successful test.

The allowance is org-wide, so your bug is everyone's outage. A retry loop with no cap, or a hook that fires on every post save instead of on publish, does not break your integration first. It exhausts the shared daily quota and breaks the org's other integrations, silently, hours later. Cap attempts and know your per-day arithmetic before you ship.

A public form is an unauthenticated write path into your CRM. Anything that turns a form submission into a Salesforce record has handed the internet a way to create records. Without rate limiting and spam filtering in front of it, the cost is not just junk data — it is junk data that consumes the API allowance above. Gate it before the queue, not after.

The run-as user's permissions are the integration's permissions. The client credentials flow acts as a nominated user, and it inherits that user's object and field access in full. Pointing it at an administrator is the fast way to get a working integration and the fast way to give a web-facing code path write access to every object in the org. Create a dedicated integration user with access to the objects you actually touch.

For the specific case of sending form entries rather than platform events, the mechanics differ enough to be worth their own walkthrough — see sending Gravity Forms entries to Salesforce, which covers the licence question and the Lead object. If the destination is HubSpot rather than Salesforce, the HubSpot contacts API has a different rate-limit model worth knowing before you choose.

/Footnotes
¹ OAuth 2.0 client credentials flow, Salesforce Help.
² sObject Rows by External ID, REST API Developer Guide.
³ Authorization through connected apps and OAuth 2.0, REST API Developer Guide.
API limits and monitoring your API usage, Salesforce Developers Blog.
FAQ

Things engineers always ask.

Don't see yours? Open an issue on GitHub or check the full reference in the API docs.

How do I connect WordPress to Salesforce without a plugin marketplace connector? +
Use the Salesforce REST API directly with the OAuth 2.0 client credentials flow. Configure an External Client App in Salesforce Setup, enable the client credentials flow, and exchange the consumer key and secret for an access token. The token response also returns the instance_url, which is the host every subsequent API call must use.
Which OAuth flow should a server-to-server Salesforce integration use? +
The client credentials flow. It is designed for a server acting on its own behalf with no user at a browser, which is what a form submission or an order webhook is. The username-password flow embeds a password in your source and the web server flow assumes a signed-in user, so neither fits.
What is the difference between a 201 and a 204 from a Salesforce upsert? +
A 201 Created means no record matched the external ID so Salesforce inserted a new one. A 204 No Content means a record matched and was updated. The 204 response has an empty body, so code that decodes JSON unconditionally will throw on the path where the update succeeded.
What are the Salesforce API daily limits? +
Enterprise Edition allows 100,000 calls per 24 hours plus 1,000 per licensed user; Unlimited Edition allows 100,000 plus 5,000 per user. The allowance is measured on a rolling 24-hour basis and is shared org-wide across REST, SOAP, Bulk and Connect, so your integration competes with every other one in the org.
Should the Salesforce API call run inside the WordPress request? +
No. An outbound API call inside a checkout or form submission makes the visitor wait for Salesforce, so a Salesforce slowdown becomes a slow checkout and PHP-FPM workers pile up behind it. Queue the delivery and let a background worker own it, so an outage delays data rather than breaking the site.
Ready

Your next automation is
one sentence away.

$ wp plugin install flowsystems-webhook-actions --activate