Salesforce-to-Salesforce Integration using REST API: Enriching Account Data with Verified Information

The Context

In many enterprises, multiple Salesforce orgs coexist with different roles. One org may serve as the system of engagement, while another holds verified, authoritative data.

Our goal was simple: ensure Org A accounts always had trusted data (such as Company Name, Headquarters, Website, and Industry) by pulling this information from Org B. Instead of users manually checking and updating records, the process had to be automatic, reliable, and scalable.

The CAPIQ Id was used as the unique identifier to match records across the two orgs.


The Approach

1. REST API in Org B (Source Org)

A REST resource in Org B provided verified account data by CAPIQ Id.

@RestResource(urlMapping='/verifiedAccounts/*')
global with sharing class VerifiedAccountsAPI {
    @HttpPost
    global static AccountResponse getAccounts(List<String> capiqIds) {
        List<Account> accounts = [
            SELECT Id, CAPIQ_Id__c, Name, Website, Industry, Headquarters__c
            FROM Account
            WHERE CAPIQ_Id__c IN :capiqIds
        ];
        
        AccountResponse response = new AccountResponse();
        response.accounts = new List<AccountDTO>();
        
        for (Account acc : accounts) {
            response.accounts.add(new AccountDTO(acc));
        }
        return response;
    }
}

global class AccountDTO {
    public String capiqId;
    public String name;
    public String website;
    public String industry;
    public String headquarters;
    
    public AccountDTO(Account acc) {
        capiqId = acc.CAPIQ_Id__c;
        name = acc.Name;
        website = acc.Website;
        industry = acc.Industry;
        headquarters = acc.Headquarters__c;
    }
}


2. Batch Job in Org A (Target Org)

The batch job in Org A orchestrated the enrichment:

  • New Accounts (created after last run) were picked up.
  • Flagged Accounts (Get_Data_From_External__c) were refreshed on-demand.
  • All Accounts were periodically refreshed every 2 days.
  • Not Found Handling – if no match was found in Org B, the CAPIQ Id was recorded both at the account level (External_Status__c = "Not Found") and centrally in a Custom Setting. These accounts were retried in subsequent scheduled runs.
  • Partial Updates – we used Database.update(records, false) to ensure one failure didn’t stop the whole batch.

global class AccountDataEnrichmentBatch implements Database.Batchable<SObject>, Database.Stateful {

    Set<String> notFoundIds = new Set<String>();
    Integer successCount = 0;
    Integer failureCount = 0;

    global Database.QueryLocator start(Database.BatchableContext bc) {
        DateTime lastRun = AccountSyncConfig__c.getOrgDefaults().Last_Run__c;
        return Database.getQueryLocator([
            SELECT Id, CAPIQ_Id__c, Get_Data_From_External__c
            FROM Account
            WHERE LastModifiedDate > :lastRun
               OR Get_Data_From_External__c = true
        ]);
    }

    global void execute(Database.BatchableContext bc, List<Account> accounts) {
        List<String> capiqIds = new List<String>();
        for (Account acc : accounts) {
            if (acc.CAPIQ_Id__c != null) {
                capiqIds.add(acc.CAPIQ_Id__c);
            }
        }

        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:OrgB/verifiedAccounts');
        req.setMethod('POST');
        req.setBody(JSON.serialize(capiqIds));
        
        Http http = new Http();
        HttpResponse res = http.send(req);

        if (res.getStatusCode() == 200) {
            List<AccountDTO> verifiedAccounts =
                (List<AccountDTO>) JSON.deserialize(res.getBody(), List<AccountDTO>.class);

            Map<String, AccountDTO> verifiedMap = new Map<String, AccountDTO>();
            for (AccountDTO dto : verifiedAccounts) {
                verifiedMap.put(dto.capiqId, dto);
            }

            List<Account> toUpdate = new List<Account>();
            for (Account acc : accounts) {
                if (verifiedMap.containsKey(acc.CAPIQ_Id__c)) {
                   AccountDTO dto = verifiedMap.get(acc.CAPIQ_Id__c);
                    acc.Name = dto.name;
                    acc.Website = dto.website;
                    acc.Industry = dto.industry;
                    acc.Headquarters__c = dto.headquarters;
                    acc.Get_Data_From_External__c = false;
                    acc.External_Status__c = 'Updated';
                    acc.Last_Enriched__c = System.now();
                    toUpdate.add(acc);
                } else {
                    notFoundIds.add(acc.CAPIQ_Id__c);
                    acc.External_Status__c = 'Not Found';
                }
            }

            // Allow partial updates
            Database.SaveResult[] results = Database.update(toUpdate, false);

            for (Database.SaveResult sr : results) {
                if (sr.isSuccess()) successCount++;
                else failureCount++;
            }
        }
    }

    global void finish(Database.BatchableContext bc) {
        AccountSyncConfig__c cfg = AccountSyncConfig__c.getOrgDefaults();
        cfg.Last_Run__c = System.now();
        cfg.Last_Successful_Updates__c = successCount;
        cfg.Last_Failures__c = failureCount;
        cfg.Not_Found_CAPIQ_Ids__c = String.join(new List<String>(notFoundIds), ',');
        upsert cfg;
    }
}

Architectural Flow

┌─────────────┐             REST API            ┌─────────────┐
│   Org A     │  ─────────────────────────────► │   Org B     │
│ (Target)    │                                 │ (Source)    │
│  Batch Job  │  ◄───────────────────────────── │  REST API   │
└─────────────┘          Verified Data          └─────────────┘
       │
       ▼
 Enriched Accounts 
 (Updated, Not Found, Failed statuses + Timestamps)


Key Highlights

  • Partial UpdatesDatabase.update(..., false) prevented batch failures.
  • Not Found Resilience – unmatched CAPIQ Ids were tracked and re-attempted in the next scheduled run.
  • Transparency – each account had a status (Updated, Not Found, Failed) and timestamp for auditability.
  • Configurable & Future-Proof – Custom Settings controlled field mappings, run stats, and retry logic.

Final Thoughts

This integration wasn’t just about moving data — it was about building a trustworthy enrichment pipeline. With verified data pulled automatically, users in Org A no longer had to cross-check or manually update accounts.

The design ensured:

  • High-quality data (verified from Org B)
  • Resilience (partial updates, retries for not found IDs)
  • Auditability (clear statuses and run stats)
  • Flexibility (easily extendable to more fields or objects)

Comments

Leave a comment