Skip to content
Distributor onboarding

Salesforce to ERP

Salesforce → ERP

These are the calls ERP receives. Salesforce is the client. The sample company is Sharma Distributors on every example. Auth, timeouts, and path aliases are on the overview.

Two calls arrive during onboarding.

Order Method and path What it is
1 POST /webhook/onboarding The Temp Account. ERP answers with the Sold To and Bill To codes
2 POST /api/accounts/salesforce-callback The Salesforce Account Id, once for the Sold To and once for the Bill To

Call 2 is sent only after ERP has already called Salesforce. Those outbound calls are on ERP → Salesforce.


POST /webhook/onboarding

Submit a distributor. ERP creates the Sold To and the Bill To, writes them back to Salesforce, then returns the codes in the same response.

POST /api/distributors/onboarding is the same operation. Header: Content-Type: application/json. Named Credential ERP_Named_Credential. Timeout 15 seconds. No retry (Max_Retries__c = 0).

Temp_AfterSave runs on Account when the record type is Temp. On insert it calls ErpOutboundSyncInvocable, which enqueues ErpOutboundSyncQueueable for integration Distributor_Sync. The queueable builds the JSON from the active request mappings and posts it. Account_AfterSave is obsolete.

A mapping with a blank Direction__c is sent on the request. A mapping with Direction__c = Response is not sent. Distributor_Sync_ERP_Code_Writeback is a response row, so soldToCode is not in the request. ErpPayloadBuilder turns a dot path into a nested object, so billingAddress.city is not a flat key. Two active rows both use the key salesforceRecordId and both read Id, so the key appears once.

What Salesforce sends

{
  "salesforceRecordId": "001000000000001AAA",
  "prospectName": "Sharma Distributors",
  "territoryCode": "TER-KA-BLR-01",
  "contactName": "Meena Rao",
  "contactPhone": "9900112233",
  "contactEmail": "meena.sharma@example.com",
  "distributorOnboardingId": "a0X000000000001AAA",
  "gstRegistrationNumber": "29ABCDE1234F1Z5",
  "panNumber": "ABCDE1234F",
  "billingAddress": {
    "street": "21 Residency Road",
    "city": "Bengaluru",
    "state": "Karnataka",
    "pincode": "560025"
  }
}
JSON path Type Required by ERP Salesforce source Stored in ERP Also accepted
salesforceRecordId string no, but write-back is skipped without it Account.Id onboarding_requests.salesforce_record_id
prospectName string yes, under the name of accountName Account.Name Sold To name, onboarding prospectName accountName is preferred when both are sent
gstRegistrationNumber string yes, under the name of gstNumber GST_Registration_Number__c Sold To GSTIN gstNumber is preferred when both are sent
panNumber string yes PAN_Number__c Sold To PAN, uppercased
billingAddress.street string yes BillingStreet Sold To and Bill To street
billingAddress.city string yes BillingCity Sold To and Bill To city
billingAddress.state string yes BillingState Sold To and Bill To state, rewritten to the full Indian state name a code such as KA is rejected
billingAddress.pincode string yes, 6 digits BillingPostalCode Sold To and Bill To pincode
billingAddress.country string no not sent billing country, if a caller sends it
territoryCode string no Territory__r.Territory_Code__c Sold To territoryCode proposedTerritoryCode
contactName string no Contact_Name__c onboarding contact name, sent back on the Sold To patch as Contact_Name__c
contactPhone string no Contact_Phone__c onboarding contact mobile contactMobile
contactEmail string no Email__c onboarding contact email, sent back as Email__c
distributorOnboardingId string no Distributor_Onboarding__c dropped. OnboardingPayload has no field for this key
bankDetails.accountName string no not sent by the current mappings Sold To bank account name flat bankAccountName is not read
bankDetails.accountNumber string no not sent Sold To bank account number flat bankAccountNumber is not read
bankDetails.ifsc string no not sent Sold To IFSC, uppercased flat bankIfscCode is not read

billingAddress.country is not mapped. Bank keys are not in the body Salesforce sends. Their mappings are inactive, and the Temp Account was not given bank fields.

Bank details do not arrive

The inactive mappings use flat keys (bankAccountName, bankAccountNumber, bankIfscCode). ERP only reads the nested bankDetails object. Turning the current rows back on, without changing the keys to bankDetails.accountName, bankDetails.accountNumber, and bankDetails.ifsc, still leaves the bank fields empty.

Payload mappings

Every ERP_Payload_Mapping row on Distributor_Sync for the request. Data_Type__c is String on every row. None of them set Literal_Value__c. A blank Direction__c is sent on the request and is not read from the response. Inactive rows are not loaded.

Sort Mapping Active Direction JSON key Salesforce field Lands in ERP
5 Distributor_Sync_Salesforce_Record_Id yes Request salesforceRecordId Id onboarding salesforceRecordId
10 Distributor_Sync_salesforceId yes blank, so request salesforceRecordId Id same key as the row above. The JSON contains it once
10 Distributor_Sync_Name yes blank, so request prospectName Name Sold To name. accountName wins if both are present
15 Distributor_Sync_Territory_Code yes Request territoryCode Territory__r.Territory_Code__c Sold To territoryCode. proposedTerritoryCode is the alternate
16 Distributor_Sync_Contact_Name yes Request contactName Contact_Name__c onboarding contact name
17 Distributor_Sync_Mobile yes Request contactPhone Contact_Phone__c onboarding contact mobile. contactMobile is the alternate
18 Distributor_Sync_Email yes Request contactEmail Email__c onboarding contact email
19 Distributor_Sync_Onboarding_Id yes Request distributorOnboardingId Distributor_Onboarding__c dropped. OnboardingPayload has no matching field
20 Distributor_Sync_GST yes blank, so request gstRegistrationNumber GST_Registration_Number__c Sold To GSTIN. gstNumber wins if both are present
30 Distributor_Sync_PAN yes blank, so request panNumber PAN_Number__c Sold To PAN
40 Distributor_Sync_Billing_Street yes blank, so request billingAddress.street BillingStreet billing street
50 Distributor_Sync_Billing_City yes blank, so request billingAddress.city BillingCity billing city
60 Distributor_Sync_Billing_State yes blank, so request billingAddress.state BillingState billing state, normalized to the full name
70 Distributor_Sync_Billing_Pincode yes blank, so request billingAddress.pincode BillingPostalCode billing pincode
80 Distributor_Sync_Bank_Account_No no blank bankAccountNumber Bank_Account_Number__c not sent. The flat key would not fill bankDetails.accountNumber anyway
90 Distributor_Sync_Bank_IFSC no blank bankIfscCode Bank_IFSC_Code__c not sent. The nested key ERP reads is bankDetails.ifsc
100 Distributor_Sync_Bank_Account_Name no blank bankAccountName Bank_Account_Name__c not sent. The nested key is bankDetails.accountName

What ERP does

  1. Bind the JSON. A body that is not JSON returns 400 before any of the steps below.
  2. Resolve the name from accountName, then prospectName. Resolve GST from gstNumber, then gstRegistrationNumber. Uppercase the PAN.
  3. Reject a blank name, GST, or PAN.
  4. Check the GSTIN against ^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[A-Z0-9]{1}Z[A-Z0-9]{1}$ and the PAN against ^[A-Z]{5}[0-9]{4}[A-Z]{1}$.
  5. Require street, city, state, and a 6-digit pincode. The state must be a full Indian state or union territory name. Karnataka is kept. KA is rejected. The stored state is the normalized name.
  6. Reject the GSTIN if a Sold To with that GSTIN already exists.
  7. Allocate ONB-2026-0001, DIST001, and BILL001. Each code is checked in Salesforce and skipped if that code is already on an Account. Up to 50 candidates are tried.
  8. Insert the Sold To, the Bill To, and the onboarding row in one transaction. Status is responded. Payment terms on the Sold To are Net 30, which was not in the request. One Bill To is created. Its name is {prospect name} - Billing. A manual respond that passes several codes names the later ones {prospect name} - Billing 2, and so on.
  9. Call the Sold To upgrade and the Bill To create. A failure here is saved on the onboarding row and does not change the HTTP status. The codes are still returned. See Failures and retry.
  10. Write an inbound integration log: flowB receive erpRef=ONB-2026-0001 name="Sharma Distributors" gst=29ABCDE1234F1Z5 soldTo=DIST001.

What ERP sends back

HTTP 200. Every field on OnboardingResponse is present. The callout is a success on any HTTP status from 200 to 299.

{
  "soldToCode": "DIST001",
  "billToCodes": ["BILL001"],
  "recordTypeUpgrade": "Sold To",
  "erpReference": "ONB-2026-0001",
  "status": "responded"
}
Field Meaning What Salesforce does with it
soldToCode The code just assigned Writes it to Account.ERP_Record_ID__c
recordTypeUpgrade Always Sold To Resolves the label to the Sold To record type and writes RecordTypeId
billToCodes One code on this path. The array can hold more than one when the manual respond API is used Nothing. Not mapped
erpReference The onboarding audit key, ONB-{year}-{sequence} Nothing. Not mapped
status responded Nothing. Not mapped

Response mappings. Response is read from the body and is not sent on the request.

Sort Mapping Active Direction Data type JSON key Written to
110 Distrubtor_Sync_RecordType_Writeback yes Response RecordType recordTypeUpgrade RecordTypeId. The value Sold To is resolved by label. The developer name of that row is misspelled (Distrubtor) and the integration still loads it because Integration__c is Distributor_Sync
200 Distributor_Sync_ERP_Code_Writeback yes Response String soldToCode Account.ERP_Record_ID__c

For Sharma Distributors those writes set ERP_Record_ID__c to DIST001 and RecordTypeId to the Sold To record type. Developer name is tried first, then the label, so Sold To matches the label rather than the developer name Sold_To. A missing path does not clear the field. A write-back exception, including an unknown record type name, is written on the integration log and the status stays Synced.

On success the queueable also sets ERP_Sync_Status__c to Synced and ERP_Last_Sync_Timestamp__c to the time the response was handled. Those two are the integration setting, not payload mappings. The queueable writes an Integration_Log__c for the callout. Log_Operation__c on the setting is Update.

Before this 200, ERP has already sent the Sold To upgrade and the Bill To create.

Records after a successful push

System Record State
Salesforce Account 001000000000001AAA Record type Sold To, Sold_To_Code__c = DIST001, ERP_Sync_Status__c = Synced, ERP_Record_ID__c = DIST001
Salesforce Account 001000000000002AAA Bill To created during the same request
ERP Sold To DIST001 Active, payment terms Net 30, GSTIN and address from the request, salesforceRecordId still empty until the callback
ERP Bill To BILL001 Name Sharma Distributors - Billing, parent DIST001
ERP Onboarding ONB-2026-0001 responded, both codes stored, errorMessage empty

Error ERP sends back

These are returned by ReceiveOnboarding itself. Salesforce then sets ERP_Sync_Status__c to Failed and does not apply the response mapping. soldToCode is not written, because the response mapping runs only after a 2xx. Max_Retries__c is 0, so the queueable does not send the push again. The Account stays record type Temp with a blank Sold_To_Code__c. A new attempt means sending Distributor_Sync again for that Account.

No Sold To, Bill To, or onboarding row is inserted for a validation or conflict error. Code allocation also runs before that insert. If the Salesforce code lookup fails, the push returns 500 and nothing is saved.

The body is always { "error", "message" }. The name and GST messages say accountName and gstNumber even when Salesforce sent prospectName and gstRegistrationNumber. Those are the names of the blank resolved values.

{
  "error": "validation_error",
  "message": "accountName is required"
}
HTTP error message
400 invalid_json invalid request body
400 validation_error accountName is required
400 validation_error gstNumber is required
400 validation_error panNumber is required
400 validation_error gst registration number must be a valid 15-character Indian GSTIN
400 validation_error pan number must be a valid PAN in format AAAAA9999A
400 validation_error billing address is required
400 validation_error billing street is required
400 validation_error billing city is required
400 validation_error billing state is required
400 validation_error billing state must be a valid Indian state/UT name (e.g. Maharashtra), not an abbreviation like MH
400 validation_error billing pincode is required
400 validation_error billing pincode must be exactly 6 digits
409 conflict a distributor with this GST number already exists
500 internal_error internal error: ... when code allocation or the database insert fails

A duplicate GSTIN leaves the existing distributor as it is. Write-back failures are a different case. They still produce HTTP 200. They are described in Failures and retry.


POST /api/accounts/salesforce-callback

Return the Account Id ERP must store. Salesforce sends this twice: once for the Sold To, once for the Bill To. This callback is the only write of salesforceRecordId on the local Sold To and Bill To. ERP does not store the Id from the create or update response.

POST /webhook/salesforce-callback is the older path for the same operation. Header: Content-Type: application/json. Timeout 15 seconds. Salesforce retries three times after the first attempt. The endpoint is callout:ERP_Named_Credential/api/accounts/salesforce-callback.

When Salesforce sends it

SoldTo_AfterSave and BillTo_AfterSave call the subflow Account_AfterSave_Shared. Temp accounts do not. The subflow calls ErpCallbackInvocable when Bypass_Automation__c.Is_Bypassed__c is false and one of these is true:

  • The Account is new and the record type is Sold To or Bill To. The Bill To insert hits this case.
  • The record type changed from Temp to Sold To. The upgrade PATCH hits this case, because SoldTo_AfterSave passes the prior record type into the subflow.
  • The Account is not new, the record type is Sold To or Bill To, SF_Record_ID_Sent_to_ERP__c is false, a Sold To or Bill To code is present, and ERP_Sync_Status__c is not Failed.

The callback job sets a flag while it runs so the Pending stamp does not enqueue a second job. ErpCallbackService also refuses to enqueue when ERP_Sync_Status__c is already Failed. A blank code is not sent. Enqueue stamps ERP_Sync_Status__c to Pending, then queues ErpCallbackService.CallbackJob. The job does not run until the Salesforce transaction commits. During onboarding, the original HTTP response is still in flight when the job is queued.

Record type accountType sent erpCode sent
Sold To SoldTo Sold_To_Code__c
Bill To BillTo Bill_To_Code__c

What Salesforce sends for the Sold To

timestamp is UTC, yyyy-MM-dd'T'HH:mm:ss'Z', with no fractional seconds.

{
  "salesforceRecordId": "001000000000001AAA",
  "accountType": "SoldTo",
  "erpCode": "DIST001",
  "timestamp": "2026-11-05T12:00:01Z"
}

ERP stores 001000000000001AAA on Sold To DIST001, and copies that same Id onto every Bill To whose parent code is DIST001, into parentSalesforceId.

What Salesforce sends for the Bill To

{
  "salesforceRecordId": "001000000000002AAA",
  "accountType": "BillTo",
  "erpCode": "BILL001",
  "timestamp": "2026-11-05T12:00:02Z"
}

ERP stores 001000000000002AAA on Bill To BILL001 only. The parent Id is not set by the Bill To callback. accountType may also be Sold To or Bill To with a space. Both forms map to the same handler. Any other value returns 400 with accountType must be Sold To or Bill To.

JSON field Type Required Salesforce source Stored in ERP
salesforceRecordId string yes Account.Id, 18 characters Sold To or Bill To salesforceRecordId
accountType string yes SoldTo or BillTo selects which table is updated
erpCode string yes Sold_To_Code__c or Bill_To_Code__c lookup key
timestamp string, UTC no Datetime.now() in GMT a missing timestamp is stored as the current UTC time for the request only. It is not written onto the account row. A value that is not a time fails JSON binding and returns 400 invalid_json

What ERP does

  1. Require salesforceRecordId and erpCode.
  2. Normalize accountType.
  3. Load the Sold To or Bill To by erpCode.
  4. If that row already stores this same Id, return success and do nothing else. A repeat delivery is accepted.
  5. If that row already stores a different Id, return 409.
  6. For a Sold To: save the Id on DIST001, then copy it onto every Bill To whose parent code is DIST001.
  7. For a Bill To: save the Id on BILL001 only.
  8. Write a callback integration log. The reference is callback soldTo erpCode=DIST001 or callback billTo erpCode=BILL001. An idempotent repeat appends (idempotent).

Order between the two jobs is not fixed. If the Bill To callback arrives first, BILL001.salesforceRecordId is set and parentSalesforceId stays empty until the Sold To callback runs.

What ERP sends back

HTTP 200. The body echoes the code, not the Salesforce Id.

Sold To:

{
  "status": "received",
  "erpCode": "DIST001"
}

Bill To:

{
  "status": "received",
  "erpCode": "BILL001"
}

Any status from 200 to 299 is success. The job sets ERP_Sync_Status__c to Synced, sets SF_Record_ID_Sent_to_ERP__c to true, and stamps ERP_Last_Sync_Timestamp__c. The integration log status is Synced. Operation is Create on the first attempt and Retry when retryCount > 0.

Retries

A non-2xx or a callout exception is a failure. The log stores HTTP {status}: {body}, or the exception message. While retries remain, the Account stays Pending and the job chains another attempt. The waits are 1 minute, then 2, then 4. MAX_RETRIES is 3, so the first attempt plus three retries is the limit. The exhausted attempt sets ERP_Sync_Status__c to Failed and inserts a second Integration_Log__c with Alert_Type__c = Callback Dead Letter, Operation__c = Retry, and Payload_Reference__c starting with deadLetter=true. That log is what fires the admin email and bell. A later save that finds ERP_Sync_Status__c = Failed does not enqueue another callback. A manual retry has to set the status back to Pending first.

Callback errors

{
  "error": "validation_error",
  "message": "salesforceRecordId is required"
}
HTTP error message
400 invalid_json invalid request body
400 validation_error salesforceRecordId is required
400 validation_error erpCode is required
400 validation_error accountType must be Sold To or Bill To
404 not_found sold to account not found
404 not_found bill to account not found
409 conflict sold to account already linked to a different salesforce record id
409 conflict bill to account already linked to a different salesforce record id
500 internal_error internal error: ...

A 404 means the code in the callback is not in this ERP database. A 409 means the row is already linked to a different Account Id, and a retry of the same body will keep failing until that link is cleared.

Records after both callbacks

ERP row Field Value
Sold To DIST001 salesforceRecordId 001000000000001AAA
Bill To BILL001 salesforceRecordId 001000000000002AAA
Bill To BILL001 parentSalesforceId 001000000000001AAA

Later Sold To and Bill To updates from ERP use these Ids in the PATCH path. They do not look the Account up by code again, except for the onboarding pre-checks on ERP → Salesforce.