Skip to content

Salesforce ​

Salesforce is a cloud-based customer relationship management (CRM) platform that helps businesses manage sales, customer interactions, and business processes. It provides tools for sales automation, customer service, marketing, analytics, and application development.

Ingestr supports Salesforce as a source and as a reverse-ETL destination.

URI format ​

The URI format for Salesforce using an OAuth access token is as follows:

plaintext
salesforce://?access_token=<access_token>&domain=<domain>

URI parameters:

  • access_token is an OAuth access token for your Salesforce org.
  • domain is your Salesforce My Domain, instance host, or full instance URL. For sandboxes, use the sandbox My Domain URL, for example https://MyDomainName--SandboxName.sandbox.my.salesforce.com.

You can also use username, password, and security token authentication:

salesforce://?username=<username>&password=<password>&token=<token>&domain=<domain>

URI parameters:

  • username is your Salesforce account username.
  • password is your Salesforce account password.
  • token is your Salesforce security token. This is not the same as an OAuth access token.
  • domain is your Salesforce instance domain (for example, login, test, or your-domain.my). You can also pass the full Salesforce host or URL.

To use the OAuth 2.0 client credentials flow, use the following URI:

salesforce://?grant_type=client_credentials&client_id=<client_id>&client_secret=<client_secret>&domain=<domain>

URI parameters:

  • grant_type=client_credentials selects the client credentials flow. This is optional when both client_id and client_secret are provided.
  • client_id is the consumer key for your Salesforce connected app.
  • client_secret is the consumer secret for your Salesforce connected app.
  • domain is your Salesforce My Domain or instance domain (for example, your-domain.my). You can also pass the full Salesforce host or URL.

You can obtain your security token by logging into your Salesforce account and navigating to the user settings under "Reset My Security Token."

Setting up a Salesforce Integration ​

Option A: Salesforce CLI access token ​

This is the most direct setup path when you want to authenticate interactively in a browser and then pass the resulting OAuth access token to ingestr.

  1. Create a Salesforce developer org from developer.salesforce.com/signup if you do not already have an org. Salesforce sends the org username by email; for developer orgs it can look like your.original.email.3f6ksj33ew99@agentforce.com.

  2. Install the Salesforce CLI from the Salesforce CLI setup guide.

  3. Log in to your org:

    sh
    sf org login web

    For a sandbox or a specific My Domain URL, pass the instance URL:

    sh
    sf org login web --instance-url https://MyDomainName--SandboxName.sandbox.my.salesforce.com
  4. Display the org details and note the Instance Url and username:

    sh
    sf org display --target-org <salesforce-username>

    Recent Salesforce CLI versions hide secrets from this command. If you see a warning that secrets are hidden, use the auth command in the next step instead of setting SF_TEMP_SHOW_SECRETS=true.

  5. Show the access token:

    sh
    sf org auth show-access-token --target-org <salesforce-username>
  6. Use the access token and instance URL in the ingestr source URI:

    sh
    ingestr ingest \
      --source-uri "salesforce://?access_token=<access_token>&domain=<instance-url>" \
      --source-table "account" \
      --dest-uri "duckdb:///sf.db" \
      --dest-table "public.account"

    URL-encode query parameter values if they contain special characters such as &, +, or %.

For Salesforce's official OAuth quickstart, see Salesforce API Authentication. For developer org setup, see Set Up Your Developer Environment.

Option B: Username, password, and security token ​

Use this option when you have a Salesforce security token from user settings. The token URI parameter is the Salesforce security token, not the OAuth access token printed by sf org auth show-access-token.

Option C: Client credentials ​

Use this option when you have a connected app configured for the OAuth 2.0 client credentials flow. ingestr exchanges client_id and client_secret for an access token automatically.

Example ​

Let's say:

  • Your Salesforce access token is fake_access_token.
  • Your Salesforce instance URL is https://your-domain.my.salesforce.com.
  • You want to ingest account data from your salesforce account
  • You want to save this data in a duckdb database sf.db under the table public.account

You can run the following command to achieve this:

sh
ingestr ingest \
  --source-uri "salesforce://?access_token=fake_access_token&domain=https://your-domain.my.salesforce.com" \
  --source-table "account" \
  --dest-uri "duckdb:///sf.db" \
  --dest-table "public.account"

Tables ​

Salesforce source allows ingesting the following objects into separate tables:

TablePKInc KeyInc StrategyDetails
accountidSystemModstampmergeIndividual or organization that interacts with your business.
account_historyidCreatedDatemergeTracks changes made to fields on Account records.
agent_workidSystemModstampmergeRepresents a work item routed to an agent through Omni-Channel.
campaignidSystemModstampmergeMarketing initiative or project designed to achieve specific goals, such as generating leads.
campaign_historyidCreatedDatemergeTracks changes made to fields on Campaign records.
campaign_memberidSystemModstampmergeAssociation between a Contact or Lead and a Campaign.
campaign_member_statusid-replaceRepresents the possible member statuses for a Campaign.
caseidSystemModstampmergeA customer issue or problem, used for support and service tracking.
case_feedidSystemModstampmergeFeed items (posts, comments, updates) associated with a Case.
case_historyidCreatedDatemergeTracks changes made to fields on Case records.
case_milestoneidSystemModstampmergeRepresents a milestone (required step) in an entitlement process on a Case.
contactidSystemModstampmergeAn individual person associated with an account or organization.
contact_historyidCreatedDatemergeTracks changes made to fields on Contact records.
content_documentidSystemModstampmergeA document uploaded to a library in Salesforce Files or CRM Content.
content_versionidSystemModstampmergeA specific version of a document in Salesforce Files or CRM Content.
conversationidLastModifiedDatemergeRepresents a conversation in messaging channels.
conversation_entryidSystemModstampmergeAn individual message or event within a Conversation.
conversation_participantidLastModifiedDatemergeA participant in a Conversation.
dashboardid-replaceRepresents a dashboard, a visual snapshot of source report data.
dashboard_componentid-replaceAn individual component (chart, table, metric) on a Dashboard.
email_messageidSystemModstampmergeAn email in Salesforce, typically associated with a Case or other record.
eventidSystemModstampmergeUsed to track and manage calendar-based events, such as meetings, appointments, or calls.
event_relationid-replaceRepresents people (invitees) or resources related to an Event.
feed_commentid-replaceA comment added to a feed item in Chatter.
folderid-replaceA folder used to organize documents, dashboards, reports, or email templates.
forecasting_quotaid-replaceAn individual user's or territory's forecast quota for a period.
groupid-replaceA set of users, such as a public group or queue.
leadidSystemModstampmergeProspective customer/individual/org. that has shown interest in a company's products/services.
lead_historyidCreatedDatemergeTracks changes made to fields on Lead records.
opportunityidSystemModstampmergeRepresents a sales opportunity for a specific account or contact.
opportunity_contact_roleid-replaceRepresents the association between an Opportunity and a Contact.
opportunity_field_historyidCreatedDatemergeTracks changes made to fields on Opportunity records.
opportunity_historyidCreatedDatemergeTracks stage and status changes on Opportunity records.
opportunity_line_itemidSystemModstampmergeRepresents individual line items or products associated with an Opportunity.
opportunity_splitid-replaceRepresents credit split between team members on an Opportunity.
opportunity_split_typeid-replaceRepresents the type of an Opportunity split, such as revenue or overlay.
permission_setid-replaceA set of permissions and settings that can be assigned to users.
permission_set_assignmentid-replaceThe assignment of a Permission Set to a user.
pricebookidSystemModstampmergeUsed to manage product pricing and create price books.
pricebook_entryidSystemModstampmergeRepresents a specific price for a product in a price book.
productidSystemModstampmergeFor managing and organizing your product-related data within the Salesforce ecosystem.
profileid-replaceDefines a user's permissions and access settings.
record_typeid-replaceRepresents a record type, which offers different business processes and page layouts per object.
reportid-replaceRepresents a report, a set of data returned in rows and columns.
service_presence_statusid-replaceA presence status that can be assigned to agents in Omni-Channel.
survey_invitationid-replaceAn invitation sent to a participant to complete a survey.
survey_question_scoreid-replaceAggregated score data for a survey question.
survey_responseid-replaceA participant's response to a survey.
survey_subjectid-replaceThe association between a survey and another record.
taskidSystemModstampmergeUsed to track and manage various activities and tasks within the Salesforce platform.
task_relationidSystemModstampmergeRepresents people or other records related to a Task.
topicid-replaceA topic used to organize and discover content in Chatter.
topic_assignmentid-replaceThe assignment of a Topic to a record or feed item.
upgrades_historyid-replaceTracks changes made to fields on Upgrades records.
user--replaceRefers to an individual who has access to a Salesforce org or instance.
user_historyidCreatedDatemergeTracks changes made to fields on User records.
user_role--replaceA standard object that represents a role within the organization's hierarchy.
user_service_presenceidSystemModstampmergeRepresents an agent's presence status in Omni-Channel, used for tracking availability.
voice_callidSystemModstampmergeRepresents a phone call made or received through Service Cloud Voice.
voice_call_feedidSystemModstampmergeFeed items associated with a Voice Call.
voice_call_recordingidSystemModstampmergeRepresents the recording of a Voice Call.
custom:<custom_object_name>--replaceTrack and store data that's unique to your organization. For more information about custom objects in Salesforce, read here

Use these as --source-table parameters in the ingestr ingest command.

Examples ​

Copy user_role data from Salesforce into a DuckDB database:

sh
ingestr ingest \
  --source-uri "salesforce://?access_token=<access_token>&domain=<instance-url>" \
  --source-table "user_role" \
  --dest-uri "duckdb:///sf.db" \
  --dest-table "public.user_role"

Copy account data using OAuth 2.0 client credentials:

sh
ingestr ingest \
  --source-uri "salesforce://?grant_type=client_credentials&client_id=<client_id>&client_secret=<client_secret>&domain=<domain>" \
  --source-table "account" \
  --dest-uri "duckdb:///sf.db" \
  --dest-table "public.account"

Copy custom object data from Salesforce into a DuckDB database:

sh
ingestr ingest \
  --source-uri "salesforce://?username=<username>&password=<password>&token=<token>&domain=<domain>" \
  --source-table "custom:My__Community_Group__c" \
  --dest-uri "duckdb:///sf.db" \
  --dest-table "public.my_community"

WARNING

Salesforce API limits may affect the frequency and volume of data ingestion. Incremental loading is supported for objects with a timestamp field such as SystemModstamp, CreatedDate, or LastModifiedDate, but some objects require full-refresh loads. This is indicated by the strategy in the table above: tables with strategy replace don't support incremental loads, while the ones with merge do.

Field-level security ​

ingestr ingests only the fields the authenticating user is permitted to read. Its query is built from what Salesforce returns for that user, so a field the user cannot read is not fetched.

If you want a field to be ingested, make sure the user (or its permission set or profile) has Read access to it, for example through its field-level security (FLS) settings. You can see which fields the user can read by describing the object as that user:

plaintext
GET https://<your-domain>.my.salesforce.com/services/data/v59.0/sobjects/<object>/describe

Only the fields listed in the response are ingested. Grant Read access to the fields you want and run the ingestion again.

Salesforce as a destination ​

ingestr can write rows from any source back into Salesforce (reverse ETL). Each source row creates, updates or deletes one Salesforce record.

URI format ​

The destination URI is the same as the source URI:

plaintext
salesforce://?access_token=<access_token>&domain=<domain>

URI parameters:

  • access_token and domain: same as the source. The username/password and client credentials logins work here too.

The user you connect with needs Create, Edit and, for delete/replace, Delete permission on the object, plus Edit access to every field you write.

Dest-table format ​

plaintext
<object>?external_id=<field>&load_method=<load_method>

Dest-table parameters:

  • <object>: the Salesforce object to write to, e.g. Contact. See Objects and fields.
  • external_id: the field used to find existing records. Needed for merge and replace. See Matching records.
  • load_method (optional): bulk (default) or rest. See Load method.

Quick start ​

sh
ingestr ingest \
  --source-uri "postgres://user:pass@host:5432/db" \
  --source-table "public.customers" \
  --dest-uri "salesforce://?access_token=<token>&domain=<domain>" \
  --dest-table "Contact?external_id=External_Id__c" \
  --primary-key customer_id \
  --incremental-strategy merge

For every row, this finds the Contact whose External_Id__c equals the row's customer_id: if it exists it is updated, otherwise it is created. The other source columns (FirstName, Email, …) are written to the Contact fields of the same name.

Objects and fields ​

  • --dest-table is the object's API name: Contact, Account, Opportunity, or a custom object such as Invoice__c.
  • Each source column is written to the field with the same API name, e.g. FirstName or Amount__c. Names are not case-sensitive.
  • Find API names in Setup → Object Manager. Labels don't work:
LabelAPI name to use
Custom objectInvoiceInvoice__c
Custom fieldAmountAmount__c
Object from a managed packageInvoice (package acme)acme__Invoice__c
  • ingestr doesn't create objects or fields. A source column that isn't a field, or is read-only (a formula, or a system field like CreatedDate), stops the run before anything is written. Drop such columns, for example with --sql-exclude-columns.
  • A source column named Id is only used to find records, never written. ingestr's own _ingestr_loaded_at and _ingestr_run_id columns are never sent.

Strategies ​

--incremental-strategy is required:

StrategyWhat it doesWhat it needs
mergeUpdates the matching record, or creates it if there is noneexternal_id= and --primary-key
updateUpdates matching records only; a row with no match is rejectednothing when matching by record Id
appendAlways creates new recordsnothing
deleteDeletes matching recordsnothing when matching by record Id
replaceLike merge, then deletes every record that isn't in the sourceexternal_id= and --primary-key
  • append never matches, so re-running it creates duplicates, unless a unique field on the object refuses them (DUPLICATE_VALUE). external_id= isn't allowed with it; --primary-key is accepted and only names rejected rows in the report.
  • delete moves records to the Recycle Bin, where they can be restored for 15 days. They still count toward the org's storage until the bin is emptied.

WARNING

replace deletes every record of the object that isn't in your source, including ones created in Salesforce or by other tools. Use it only when your source is the complete list. As a safety net, a run with 0 source rows deletes nothing, and with the default --reject-mode fail a run with any rejected row deletes nothing either. Under skip, a run where every row was rejected also deletes nothing.

Matching records ​

Two settings decide which record a row updates or deletes:

SettingSet withExample
The Salesforce field to match onexternal_id= in --dest-tableContact?external_id=External_Id__c
The source column that holds the value--primary-key--primary-key customer_id

merge and replace need both. The field must be marked External ID in Salesforce, or be a standard lookup field such as a Contact's or Lead's Email. Standard objects have no External ID field by default, so create one first: Setup → Object Manager → object → Fields & Relationships → New, and tick External ID. If the field isn't unique and two records share a value, Salesforce rejects that row instead of guessing. The record Id can't be used, because Salesforce can't create a record with an id you choose.

update and delete match by the Salesforce record id by default, using a source column named Id, so a source that has the ids needs no extra flags:

sh
# Delete the contacts listed in the source by their Salesforce record id
ingestr ingest \
  --source-uri "csv://churned.csv" \
  --source-table "churned" \
  --dest-uri "salesforce://?access_token=<token>&domain=<domain>" \
  --dest-table "Contact" \
  --incremental-strategy delete

They can also match on any other text, number or id field with external_id=, even one that isn't unique. Then every matching record is updated or deleted. For example, one row keyed on Department updates every contact in that department.

Linking records ​

In Salesforce, a record points to its parent through a lookup field on itself. A Contact's Account, for example, is stored in the Contact's AccountId field. To link records, write that field like any other column, in one of two ways:

You haveColumn nameExample value
The parent's Salesforce record idThe lookup field, e.g. AccountId001WU00002EGrguYAD
Your own key for the parent<Relationship>.<Field>, e.g. Account.Ext_Id__cACC-1

With your own key, Salesforce finds the parent for you:

  • The relationship is usually the lookup field without Id: AccountId → Account, OwnerId → Owner, ParentId → Parent. For a custom lookup, replace __c with __r: Parent__c → Parent__r.
  • The field must identify one parent: a field marked External ID, or a unique field such as a user's email (Owner.Email).
  • The source column can have this name already, or you can map one with --columns, e.g. --columns 'Account.Ext_Id__c::account_code'.
  • Unlinking: an empty (null) value removes the link. Pass --write-nulls=false to leave existing links alone.

Lookups that can point to more than one object. A Task's or Event's Who can be a Contact or a Lead, and its What an Account, Opportunity, Case and more. For these, put the object in the middle, <Relationship>.<Object>.<Field>:

Subject,Who.Contact.Ext_Id__c
Follow-up call,C-1
  • A column without the object, such as Who.Ext_Id__c, stops the run before writing and lists the objects to choose from. This is about the column's final name, whether it comes straight from the source or from --columns.
  • Owner is the exception: Owner.Email means a User, so no object is needed. For a Queue, write Owner.Group.<Field>.
  • If rows point to different objects, use one column per object (Who.Contact.Ext_Id__c and Who.Lead.Ext_Id__c) and fill one per row. A row that fills both is rejected.
  • A record id in the lookup field itself (WhoId) needs no object name.

Many-to-many links. Salesforce stores these as a record of their own, with a lookup to each side. For example, an OpportunityContactRole links a Contact to an Opportunity, and a CampaignMember links a Contact or Lead to a Campaign. Each source row creates one link, and can set the link's own fields:

sh
# opportunity_contacts.csv:
#   Opportunity.Opp_Ext_Id__c,Contact.Ext_Id__c,Role,IsPrimary
#   OPP-7,C-1,Decision Maker,true
#   OPP-7,C-2,Evaluator,false
ingestr ingest \
  --source-uri "csv://opportunity_contacts.csv" \
  --source-table "opportunity_contacts" \
  --dest-uri "salesforce://?access_token=<token>&domain=<domain>" \
  --dest-table "OpportunityContactRole" \
  --incremental-strategy append

To remove links, delete those records. To keep the links exactly in step with your source, give the object an External ID field and use replace. This only works when Salesforce lets the link's lookups change: a CampaignMember's Campaign, for example, is fixed once created, so the run stops before writing. For such links, delete the stale ones and append the new ones.

Load method ​

  • bulk (default): uses Salesforce's Bulk API. It uses far fewer API calls than rest on large tables, and reports rejected rows when the load finishes.
  • rest: writes records in small batches and reports results right away, but uses more API calls on large tables.
sh
--dest-table "Contact?external_id=External_Id__c&load_method=rest"

Use rest if you need --reject-mode fail_fast, upload files (such as ContentVersion.VersionData), or write text that is exactly #N/A, which bulk reads as an empty value. Every strategy works the same with both.

Reverse-ETL options ​

These flags only apply when Salesforce is the destination.

--reject-mode ​

What to do with a row Salesforce refuses, or that matches no record:

ModeBehaviour
fail (default)Write every valid row, then fail the run and list the rejected rows
fail_fastStop at the first rejected row. Needs load_method=rest
skipWrite every valid row, list the rejected rows, and succeed

One bad row never blocks the others. Each rejected row is listed with Salesforce's reason (e.g. REQUIRED_FIELD_MISSING, DUPLICATE_VALUE) and its key. If a record is briefly locked because something else is editing it at the same moment, ingestr retries it a few times first.

Problems that affect the whole run, such as an expired login or a full org storage, always stop it, whatever the mode.

NOTE

Salesforce writes can't be rolled back, so with fail or fail_fast some records may already be written when the run stops.

--write-nulls ​

  • true (default): an empty (null) source value clears the field in Salesforce.
  • false (--write-nulls=false): an empty source value is skipped, and the field keeps its current value.

It has no effect on delete.

Column mapping ​

If a source column's name differs from the Salesforce field, rename it with --columns 'field::source_column'. Separate several with commas:

sh
--columns 'FirstName::first_name,External_Id__c::customer_id'
  • Only renaming is allowed. Field types are set in Salesforce, so an entry with a type is rejected.
  • --primary-key takes the new name. With --columns 'Ext_Id__c::customer_ref', pass --primary-key Ext_Id__c.
  • Link columns can be mapped the same way, including lookups that can point to more than one object: --columns 'Account.Ext_Id__c::account_code,Who.Contact.Ext_Id__c::contact_key'.
  • Names are sent exactly as written. --schema-naming is ignored, since changing FirstName to first_name would no longer match the field.

Values ​

Numbers, booleans, text, dates and timestamps are sent in the form Salesforce expects. Timestamps are sent in UTC; Salesforce keeps them to the whole second. Nested or JSON values are sent as a JSON string.