Nurture TechnologiesNurture Tech
←Back to Blog
Software Development22 min read·September 27, 2026

Symfony Salesforce IntegrationA Step-by-Step Developer Guide

Connecting Symfony to Salesforce requires setting up a Connected App, implementing OAuth 2.0 authentication, and building a service layer that handles REST API calls reliably. This guide walks through every step from Salesforce configuration to production-ready Symfony code.

Salesforce is one of the most common integration targets for enterprise PHP applications. If your Symfony application needs to read or write contacts, leads, accounts, opportunities, or custom objects in Salesforce, the Salesforce REST API provides a well-documented and flexible way to do it.

This guide covers the complete integration from beginning to production. It starts with Salesforce configuration, works through Symfony service setup, covers the most common operations, and addresses the real challenges that most tutorials skip: rate limits, token refresh, error handling, and bidirectional sync.

The code examples here use Symfony 7 with PHP 8.2, but the same approach works with Symfony 6.x and PHP 8.0+. The Salesforce API version used is v59.0.


Choosing the Right Salesforce API

Salesforce offers several APIs with different capabilities. Before writing any code, choose the API that matches your use case.

APIBest ForLimitations
REST APIReal-time CRUD operations, most common choiceRate limited; not ideal for bulk data
SOAP APILegacy integrations, complex queriesVerbose, harder to work with in PHP
Bulk API 2.0Loading or exporting 50,000+ recordsAsynchronous; not for real-time use
Streaming APIReal-time notifications of record changesRequires persistent connection (Bayeux protocol)
GraphQL APIFlexible queries, reduce over-fetchingBeta in some orgs; check availability

For most Symfony integrations — syncing contacts from a web form, creating leads from an API, reading account data for display — the REST API is the right choice. This guide focuses on the REST API.


Step 1: Create a Salesforce Connected App

A Connected App is how Salesforce represents your external application. It is required for OAuth 2.0 authentication. You need Salesforce admin access to create one.

Creating the Connected App

  • In Salesforce Setup, search for App Manager in the Quick Find box
  • Click New Connected App in the top right
  • Fill in Basic Information: Connected App Name (e.g., Symfony Integration), API Name (auto-populated), Contact Email
  • Under API (Enable OAuth Settings), check Enable OAuth Settings
  • Set Callback URL to your Symfony application's OAuth callback URL (e.g., https://yourapp.com/salesforce/oauth/callback)
  • Under Selected OAuth Scopes, add: Access and manage your data (api), Perform requests on your behalf at any time (refresh_token, offline_access)
  • Save the Connected App

After saving, Salesforce shows you the Consumer Key and Consumer Secret. These are your OAuth client credentials. Store them securely — you will add them to your Symfony environment variables.

OAuth Flow Choice

Salesforce supports several OAuth flows. For server-to-server integrations (where your Symfony backend calls Salesforce without user involvement), use the JWT Bearer Token flow or the Username-Password flow. For integrations where a human user authorises access through their Salesforce account, use the Web Server OAuth flow.

FlowUse Case
Web Server (Authorization Code)User authorises Symfony to access their Salesforce org
JWT Bearer TokenServer-to-server; requires certificate setup
Username-PasswordService account credentials; simpler but less secure
Client CredentialsAvailable in newer API versions; no user interaction needed

This guide uses the Username-Password flow for simplicity in demonstrating the integration. For production systems, the JWT Bearer Token flow or Client Credentials flow is recommended because they don't expose user passwords in configuration.


Step 2: Configure Symfony Environment Variables

Store all Salesforce credentials in environment variables. Never hardcode credentials in your Symfony service classes or commit them to version control.

Add the following to your .env.local file (for local development) and your production environment configuration:

  • SALESFORCE_LOGIN_URL=https://login.salesforce.com (or https://test.salesforce.com for sandbox)
  • SALESFORCE_CLIENT_ID=your_consumer_key
  • SALESFORCE_CLIENT_SECRET=your_consumer_secret
  • SALESFORCE_USERNAME=your_service_account_username
  • SALESFORCE_PASSWORD=your_service_account_password_plus_security_token
  • SALESFORCE_API_VERSION=v59.0

Note on the password: when using the Username-Password flow, the password value is your Salesforce password concatenated with your security token (e.g., MyPassword123ABC456xyz). You can reset your security token in Salesforce under Settings → My Personal Information → Reset My Security Token.


Step 3: Install Dependencies

The Symfony integration uses Symfony's HttpClient component to make API calls. Install it if you haven't already:

  • composer require symfony/http-client
  • composer require symfony/cache (for storing access tokens between requests)

There are also PHP packages specifically for Salesforce integration, including omniphx/forrest (a Laravel-first but Symfony-compatible Salesforce REST library) and developerforce/force.com-toolkit-for-php (older, SOAP-based). For a Symfony application where you want full control over HTTP behaviour, cache strategies, and error handling, building a service class on top of HttpClient is typically the better approach. The examples below use this approach.


Step 4: Build the Salesforce Authentication Service

Create a SalesforceAuthService class responsible for obtaining and caching OAuth access tokens. Every API call to Salesforce requires a valid access token, and tokens expire — so this service needs to handle token refresh transparently.

The authentication service should:

  • POST to the Salesforce token endpoint with grant_type, client_id, client_secret, username, and password
  • Receive an access_token, instance_url, and token_type in the response
  • Cache the access token using Symfony's cache component with a TTL slightly shorter than Salesforce's 2-hour token lifetime
  • Return both the access token and instance URL, since the instance URL changes by org and is required for all subsequent API calls
  • On a 401 Unauthorized response from any API call, invalidate the cached token and re-authenticate

The instance_url returned during authentication is important. All API calls must go to this URL, not the generic login.salesforce.com URL. A typical instance URL looks like https://yourcompany.my.salesforce.com. Store it alongside the access token in your cache.


Step 5: Build the Salesforce API Service

Create a SalesforceApiService class that handles all REST API calls. This class should depend on SalesforceAuthService to get valid tokens and use Symfony's HttpClientInterface for HTTP requests.

The API service needs to construct the correct URL format for each operation. Salesforce REST API URLs follow this pattern:

  • Base URL: {instance_url}/services/data/{api_version}/
  • SObject CRUD: {base}/sobjects/{ObjectType}/{id}
  • Query (SOQL): {base}/query?q={encoded_soql}
  • Search (SOSL): {base}/search?q={encoded_sosl}
  • Composite (batch operations): {base}/composite/

All requests require an Authorization header with Bearer {access_token}. All POST and PATCH requests must include Content-Type: application/json. GET and DELETE requests do not require a Content-Type header.


Step 6: Common CRUD Operations

Creating a Salesforce Contact

To create a Contact record, POST to /services/data/v59.0/sobjects/Contact/ with a JSON body containing the field values. Required fields for a Contact are LastName. Other commonly mapped fields include FirstName, Email, Phone, AccountId, and Title.

A successful creation returns HTTP 201 with a JSON body containing the new record's id and a success boolean. Store this Salesforce ID in your local database to link the local record to its Salesforce counterpart.

Reading a Salesforce Record

To read a specific record by ID, GET /services/data/v59.0/sobjects/Contact/{id}. This returns all fields on the object. To retrieve only specific fields and reduce response payload size, use a SOQL query instead:

GET /services/data/v59.0/query?q=SELECT+Id,FirstName,LastName,Email+FROM+Contact+WHERE+Id='003...'

Updating a Salesforce Record

Updates use PATCH, not PUT. PATCH to /services/data/v59.0/sobjects/Contact/{id} with only the fields you want to change. Salesforce performs a partial update — fields not included in the PATCH body are not affected. A successful update returns HTTP 204 with no response body.

Deleting a Salesforce Record

DELETE to /services/data/v59.0/sobjects/Contact/{id}. A successful deletion returns HTTP 204. In most Salesforce configurations, deleting records moves them to the Recycle Bin rather than permanently removing them.

Upserting with External IDs

If you need to create-or-update a record based on an external identifier (for example, your own application's user ID), Salesforce supports upsert via PATCH to /services/data/v59.0/sobjects/Contact/{ExternalIdField}/{externalIdValue}. This requires that the field is designated as an External ID in Salesforce's field configuration. Upsert returns 201 if a record was created and 204 if an existing record was updated.


Step 7: SOQL Queries from Symfony

SOQL (Salesforce Object Query Language) is a SQL-like language for querying Salesforce data. All queries go through the /services/data/v59.0/query endpoint with the SOQL query URL-encoded as the q parameter.

Important SOQL differences from SQL to be aware of:

  • No SELECT * — you must list every field you want in the SELECT clause
  • Wildcards are not supported in WHERE clauses for equality; use LIKE for partial matches
  • JOINs are not supported; related objects are queried via relationship traversal (SELECT Account.Name FROM Contact)
  • Results are paginated — the response includes a nextRecordsUrl if there are more records than the default page size (2,000)
  • SOQL is case-insensitive for field names but values in string comparisons are case-sensitive by default

Pagination: when a query returns more records than fit in one response, Salesforce includes a nextRecordsUrl in the response JSON. To fetch subsequent pages, GET {instance_url}{nextRecordsUrl}. Continue until the response includes "done": true.


Step 8: Registering the Services in Symfony

Define your SalesforceAuthService and SalesforceApiService in config/services.yaml. Inject environment variables using the %env()% syntax:

  • Bind SALESFORCE_LOGIN_URL, SALESFORCE_CLIENT_ID, SALESFORCE_CLIENT_SECRET, SALESFORCE_USERNAME, SALESFORCE_PASSWORD, and SALESFORCE_API_VERSION as constructor arguments
  • Set autoconfigure: true so Symfony handles service wiring
  • Tag SalesforceApiService with a custom tag if you want to use it in message handlers

For the cache, inject the CacheInterface or use the cache pool tagged as app.cache. Store access tokens with a key like salesforce.access_token and a 110-minute TTL (Salesforce tokens expire in 2 hours; the shorter TTL prevents using a token that's about to expire).


Step 9: Handling Token Expiry and Re-Authentication

Salesforce REST API returns HTTP 401 with error code INVALID_SESSION_ID when an access token has expired. Your SalesforceApiService must handle this gracefully.

The recommended pattern:

  • On any 401 response, delete the cached token
  • Re-authenticate using SalesforceAuthService to get a fresh token
  • Retry the original request once with the new token
  • If the retry also fails with 401, throw an exception — something is wrong with credentials
  • Log all authentication failures for monitoring

Do not retry indefinitely. A single retry after re-authentication is appropriate. Multiple retries in a loop can cause runaway API calls that exhaust your Salesforce API limits.


Step 10: Handling Salesforce API Rate Limits

Salesforce enforces daily API call limits based on your edition and licence count. Exceeding this limit returns HTTP 403 with REQUEST_LIMIT_EXCEEDED. Monitor your API usage proactively.

Every Salesforce API response includes headers showing your current usage:

  • Sforce-Limit-Info: api-usage=12045/15000 — shows calls used and daily limit
  • Log this header in your Symfony application to track consumption
  • If usage exceeds 80% of daily limit, alert your team

Strategies to stay within limits:

  • Use the Composite API to batch up to 25 operations in a single API call
  • Cache Salesforce data locally for read-heavy use cases and sync on a schedule rather than querying on every request
  • Use Bulk API 2.0 for large data loads instead of individual REST calls
  • Avoid querying all fields when you only need a subset — specify fields explicitly in SOQL
  • Implement a queue for write operations during high-traffic periods

Step 11: Receiving Salesforce Data in Symfony (Outbound Messaging)

The integration above covers Symfony calling Salesforce. For bidirectional sync, you also need Salesforce to notify Symfony when records change. Salesforce provides two mechanisms for this: Outbound Messaging and Platform Events.

Outbound Messaging

Outbound Messaging is configured in Salesforce Workflow Rules. When a record matches a rule, Salesforce sends a SOAP XML message to an endpoint URL you configure. Your Symfony application exposes an endpoint that receives and processes these messages.

To receive outbound messages in Symfony:

  • Create a controller action at a URL like POST /salesforce/webhook/outbound
  • Parse the incoming SOAP XML body — the structure depends on the objects you are syncing
  • Respond with an ACK (acknowledgement) XML response — Salesforce retries if it does not receive the ACK within 60 seconds
  • Process the record data asynchronously using a Symfony messenger handler to avoid timeout issues
  • Verify the OrganizationId in the payload matches your Salesforce org to prevent spoofed requests

Platform Events

Platform Events are Salesforce's event-driven integration mechanism. They use the Streaming API with CometD/Bayeux protocol rather than outbound HTTP. For Symfony applications, consuming Platform Events requires a persistent connection to Salesforce's streaming endpoint, which is typically handled by a background worker process rather than an HTTP controller.

A common approach for PHP applications is to use a Salesforce Platform Events consumer library in a Symfony Console command that runs as a long-lived process, or to bridge Platform Events through an external queue (AWS SQS, RabbitMQ) using Salesforce's MuleSoft or a serverless function.


Step 12: Custom Objects and Custom Fields

Working with Salesforce custom objects and custom fields in Symfony requires knowing their API names, which always end in __c (for custom) or __r (for relationship). For example, a custom object named Project has the API name Project__c, and a custom field named Budget has the API name Budget__c.

You can discover all custom object API names and their field definitions using the Salesforce describe endpoint:

  • GET /services/data/v59.0/sobjects/ — lists all available SObjects
  • GET /services/data/v59.0/sobjects/Project__c/describe/ — returns field definitions, required fields, and relationship metadata for a specific object

Use the describe endpoint during development to understand the schema you are working with. For production code, hardcode the field names you need rather than calling describe on every request.


Error Handling and Salesforce API Error Codes

Salesforce REST API error responses are returned as JSON arrays with an errorCode and message field. Understanding the common error codes prevents you from building generic error handling that masks the real problem.

Error CodeHTTP StatusCauseFix
INVALID_SESSION_ID401Access token expired or invalidRe-authenticate and retry
REQUEST_LIMIT_EXCEEDED403Daily API limit reachedReduce call volume or request limit increase
INSUFFICIENT_ACCESS_OR_READONLY400API user lacks permission on the object/fieldUpdate Connected App or user permissions
ENTITY_IS_DELETED404Record was deletedHandle gracefully; remove local reference
FIELD_INTEGRITY_EXCEPTION400Required field missing or invalid valueValidate data before sending to Salesforce
DUPLICATE_VALUE400Record with same unique field already existsUpsert using external ID instead of insert
STRING_TOO_LONG400Field value exceeds Salesforce field lengthTruncate before sending

Build specific exception classes for different error categories: authentication errors, rate limit errors, data validation errors, and not-found errors. This allows calling code to respond appropriately to each case rather than applying a single fallback behaviour to all failures.


Testing the Salesforce Integration

Sandbox vs Production

Always develop and test against a Salesforce sandbox, not production. Change SALESFORCE_LOGIN_URL to https://test.salesforce.com when connecting to a sandbox. Sandbox credentials are separate from production credentials. The Connected App needs to exist in each environment separately or be deployed from production to sandbox via a change set.

Unit Testing

Unit test your SalesforceApiService by mocking Symfony's HttpClientInterface. Inject a mock client that returns predefined responses for authentication and API calls. This lets you test your service logic — pagination handling, error recovery, data mapping — without making real HTTP calls.

Integration Testing

For integration tests, use a dedicated Salesforce developer sandbox with test data. Run integration tests in CI/CD only if you have a reliable way to manage sandbox credentials and test data. Mark integration tests with a Symfony test group (e.g., salesforce) and exclude them from your standard unit test suite so they don't run on every commit.


Securing the Integration

The most common security vulnerabilities in Salesforce integrations:

  • Storing credentials in code or committed to version control — use environment variables and secrets managers
  • Granting the integration user admin-level Salesforce permissions — create a dedicated integration user with only the object and field permissions the integration actually needs
  • Not verifying Salesforce outbound message payloads — validate the OrganizationId to ensure the message is from your Salesforce org
  • Logging access tokens in application logs — never log token values, even at debug level
  • Not using HTTPS for your Symfony webhook endpoint — Salesforce requires HTTPS for outbound message endpoints; self-signed certificates will be rejected

How Nurture Technologies Can Help

Symfony Salesforce integrations look straightforward from the API documentation, but production-grade implementations involve decisions that aren't obvious until something breaks in production: how to handle the 401 token expiry during high-traffic periods without stampeding re-authentication requests, how to implement bidirectional sync without creating infinite loops, how to manage API limits across multiple application instances, and how to structure the service layer for testability.

At Nurture Technologies, we build and maintain custom Salesforce integrations for Symfony and other PHP frameworks. Whether you need a new integration built correctly from the start or an existing integration that is fragile or poorly structured to be refactored into a maintainable service, we can help.

For Founders & Product Leaders

Need a Reliable Symfony Salesforce Integration?

We build production-ready Salesforce integrations for Symfony applications. If you are starting a new integration or dealing with one that has become hard to maintain, we can review your architecture and implement a clean, tested solution.

✓Connected App setup and OAuth flow implementation
✓Service layer architecture for testable, maintainable API code
✓Bidirectional sync with conflict resolution
✓Rate limit management and retry logic
✓Custom object and field mapping
✓Outbound messaging and Platform Events integration
Talk to a Salesforce Integration Developer →No commitment. We'll review your requirements and give you an honest assessment.

Conclusion

A Symfony Salesforce integration built on the REST API is reliable and maintainable when the service layer is designed correctly. The main things to get right are: OAuth token management with proper caching and refresh handling, specific error code handling rather than generic catch-all fallbacks, rate limit awareness through header monitoring and batching, and a test suite that covers the critical paths without requiring a live Salesforce connection for unit tests.

The pattern that fails most often in production is a naive implementation that re-authenticates on every request, ignores rate limit headers, and has no retry logic. With proper token caching, the Composite API for batch operations, and a retry layer that handles the INVALID_SESSION_ID error code, the same integration becomes significantly more robust.

Start with the Username-Password flow for development speed, then migrate to JWT Bearer Token or Client Credentials in production for better security. Test against a sandbox first, validate the schema using the describe endpoint, and deploy to production only after confirming the integration works correctly end-to-end in a staging environment.

FAQ

FREQUENTLY ASKED QUESTIONS

How do I connect Symfony to Salesforce?+

Connect Symfony to Salesforce using the Salesforce REST API. You need to create a Connected App in Salesforce to get OAuth credentials, then implement an authentication service in Symfony that obtains an access token using those credentials. Once authenticated, use Symfony's HttpClient to make REST API calls to create, read, update, and delete Salesforce records. Store the access token in Symfony's cache with a TTL shorter than the 2-hour Salesforce token lifetime.

What is a Salesforce Connected App?+

A Connected App is how Salesforce represents external applications that need API access. It provides the OAuth credentials (Consumer Key and Consumer Secret) that your Symfony application uses to authenticate. You create a Connected App in Salesforce Setup under App Manager. Each Connected App has configurable permissions (OAuth scopes) and callback URLs. For server-to-server integrations, you need the api scope and the refresh_token scope.

Which OAuth flow should I use for a Symfony Salesforce integration?+

For server-to-server integrations where no user interaction is needed, use the JWT Bearer Token flow (most secure, requires certificate setup) or the Username-Password flow (simpler, uses service account credentials). For integrations where a human user authorises Symfony to access their Salesforce data, use the Web Server (Authorization Code) flow. Avoid the Username-Password flow if your security requirements prohibit storing Salesforce passwords in application configuration.

How do I handle token expiry in a Symfony Salesforce integration?+

Store the access token in Symfony's cache with a TTL of 110 minutes (Salesforce tokens expire in 2 hours, so the shorter TTL prevents using a token that's about to expire). When any Salesforce API call returns HTTP 401 with error code INVALID_SESSION_ID, delete the cached token, re-authenticate to get a fresh token, and retry the original request once. If the retry also fails with 401, throw an exception — the credentials are likely invalid.

How do I create a Salesforce Contact from Symfony?+

POST to /services/data/v59.0/sobjects/Contact/ with a JSON body containing the contact fields. The Authorization header must include Bearer {access_token} and the Content-Type must be application/json. Required fields are LastName. A successful creation returns HTTP 201 with a JSON body containing the new record's id. Store this id in your local database to link your application's record to its Salesforce counterpart.

What is SOQL and how do I use it in Symfony?+

SOQL (Salesforce Object Query Language) is a SQL-like language for querying Salesforce data. Unlike SQL, it requires you to specify every field in the SELECT clause — SELECT * is not supported. Execute SOQL queries via GET /services/data/v59.0/query?q={url-encoded-soql}. Results are paginated with a nextRecordsUrl in the response for additional pages. Use SOQL for filtered queries and relationship traversal rather than fetching records by ID one at a time.

How do I update a Salesforce record from Symfony?+

Use PATCH to /services/data/v59.0/sobjects/{ObjectType}/{id} with a JSON body containing only the fields to change. Salesforce performs a partial update — fields not in the request are not affected. A successful update returns HTTP 204 with no response body. If you need to create or update based on an external ID rather than the Salesforce ID, use the upsert endpoint: PATCH /services/data/v59.0/sobjects/{ObjectType}/{ExternalIdField}/{externalIdValue}.

How do I handle Salesforce API rate limits in Symfony?+

Check the Sforce-Limit-Info response header on every Salesforce API call — it shows your daily API usage in the format api-usage=current/limit. Log this value and alert when usage exceeds 80% of the daily limit. Reduce API consumption by caching read results locally, using the Composite API to batch up to 25 operations per request, and using Bulk API 2.0 for large data loads. Avoid querying all fields in SOQL when you only need a subset.

How do I receive Salesforce updates in Symfony?+

Salesforce can push record changes to Symfony using Outbound Messaging (SOAP-based, configured in Workflow Rules) or Platform Events (event-driven, uses Streaming API). For Outbound Messaging, create a Symfony controller that accepts the SOAP XML payload and responds with an ACK XML response. For Platform Events, you need a long-lived consumer process — typically a Symfony Console command or an external queue bridge. Outbound Messaging is simpler to implement for most use cases.

How do I work with Salesforce custom objects in Symfony?+

Custom object API names always end in __c (e.g., Project__c). Custom field names also end in __c (e.g., Budget__c). Use the describe endpoint (GET /services/data/v59.0/sobjects/Project__c/describe/) during development to see all field names, types, and required fields. In production code, hardcode the field names rather than calling describe on each request. CRUD operations for custom objects use the same pattern as standard objects.

Should I use an existing PHP Salesforce library or build my own service?+

For most Symfony applications, building a focused service class using Symfony's HttpClient is better than using a generic Salesforce PHP library. Libraries like omniphx/forrest were designed for Laravel and require adaptation. A custom service class gives you full control over HTTP behaviour, cache integration, error handling, and Symfony service injection. The Salesforce REST API is well-documented and the core operations (auth, CRUD, SOQL) are not complex enough to justify a heavy dependency.

How do I test a Symfony Salesforce integration?+

Unit test your service class by mocking Symfony's HttpClientInterface and returning predefined responses. This lets you test authentication flow, error handling, pagination logic, and data mapping without making real API calls. For integration tests, use a Salesforce developer sandbox (free) with test data. Mark integration tests with a custom PHPUnit group and run them separately from unit tests, since they require live Salesforce credentials and network access.

What permissions does the Salesforce Connected App need?+

For a typical server-to-server REST integration, the Connected App needs the api OAuth scope (access and manage data) and the refresh_token or offline_access scope if you want to refresh tokens without re-authenticating. The Salesforce user associated with your integration credentials also needs Object and Field Level Security permissions for every object and field the integration reads or writes. Follow the principle of least privilege — grant only the specific object access the integration requires.

How do I secure my Symfony Salesforce integration?+

Store Salesforce credentials in environment variables, never in code or version control. Create a dedicated Salesforce integration user with only the permissions the integration actually needs — avoid using an admin account. For outbound message endpoints, verify the OrganizationId in the payload. Never log access tokens or consumer secrets. Use HTTPS for any webhook endpoints receiving Salesforce messages — Salesforce requires HTTPS and rejects self-signed certificates.

Need Answers Specific to Your Project?

Every product has unique requirements. Speak with our engineering team for recommendations tailored to your business.

Free consultation for startups and businesses.

Book Now →