ERP → Salesforce¶
These are the calls ERP sends. Salesforce is the server. They run inside POST /webhook/onboarding, before ERP returns the codes to Salesforce. The sample company is Sharma Distributors. The body Salesforce sent in is on Salesforce → ERP.
Auth is OAuth client credentials. Every URL below is on the Salesforce instance:
https://{instance}/services/data/v67.0
| Order | Method and path | What it is |
|---|---|---|
| 1 | PATCH /sobjects/Account/001000000000001AAA |
Turn the Temp Account into a Sold To and write DIST001 |
| 2 | POST /sobjects/Account |
Create the Bill To BILL001 under that Sold To |
If the server has no Salesforce URL and no token, these calls are not made. The local Sold To and Bill To still exist, and the onboarding response still returns the codes.
These fields are not driven by ERP_Payload_Mapping. They are the body of WriteBackOnboarding.
PATCH /sobjects/Account/{id}¶
Upgrade the Temp Account to a Sold To. Salesforce is still waiting on POST /webhook/onboarding while this PATCH runs.
PATCH /services/data/v67.0/sobjects/Account/001000000000001AAA
Authorization: Bearer {access token}
Content-Type: application/json
Caller: httpSalesforceClient.WriteBackOnboarding. 001000000000001AAA is onboarding_requests.salesforce_record_id. If that id is blank, the PATCH is skipped and the onboarding HTTP response is still 200.
Pre-check¶
Before the PATCH, ERP runs:
If that query returns an Id whose first 15 characters differ from 001000000000001AAA, the upgrade stops. The error stored on the onboarding row is:
sold to code DIST001 already exists on Salesforce Account {otherId} (cannot upgrade Temp 001000000000001AAA); assign a free code or clear the conflicting Account
An empty query result, or a hit on the Temp Account itself, is allowed. The same 15-character comparison treats a 15-character Id and an 18-character Id as the same record.
What ERP sends¶
RecordTypeId is the org's Sold To record type, looked up at runtime by developer name Sold_To. The value below is a placeholder. Bank fields are omitted when empty, which is the live case, because bank details never arrived on the push. ERP_Record_ID__c on this call is ERP's own row id. Salesforce replaces it with DIST001 when it reads the onboarding response. BillingCountry does not come from the onboarding JSON. The PATCH always sends India.
{
"RecordTypeId": "012000000000001AAA",
"Sold_To_Code__c": "DIST001",
"Name": "Sharma Distributors",
"GST_Registration_Number__c": "29ABCDE1234F1Z5",
"PAN_Number__c": "ABCDE1234F",
"BillingStreet": "21 Residency Road",
"BillingCity": "Bengaluru",
"BillingState": "Karnataka",
"BillingPostalCode": "560025",
"BillingCountry": "India",
"Phone": "9900112233",
"Contact_Name__c": "Meena Rao",
"Contact_Phone__c": "9900112233",
"Email__c": "meena.sharma@example.com",
"Payment_Terms__c": "Net 30",
"Territory__r": {
"Territory_Code__c": "TER-KA-BLR-01"
},
"Account_Status__c": "Active",
"ERP_Record_ID__c": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}
| JSON field | Type | Required | Source in ERP | Salesforce field |
|---|---|---|---|---|
RecordTypeId |
id | yes | record type cache, developer name Sold_To |
RecordTypeId |
Sold_To_Code__c |
string | yes | DIST001 |
Sold_To_Code__c |
Name |
string | no | prospectName |
Name |
GST_Registration_Number__c |
string | no | gstRegistrationNumber |
GST_Registration_Number__c |
PAN_Number__c |
string | no | panNumber |
PAN_Number__c |
Bank_Account_Name__c |
string | no | bank name, omitted when blank | Bank_Account_Name__c |
Bank_Account_Number__c |
string | no | bank number, omitted when blank | Bank_Account_Number__c |
Bank_IFSC_Code__c |
string | no | IFSC, omitted when blank | Bank_IFSC_Code__c |
BillingStreet |
string | no | billingAddress.street |
BillingStreet |
BillingCity |
string | no | billingAddress.city |
BillingCity |
BillingState |
string | no | normalized state name | BillingState |
BillingPostalCode |
string | no | billingAddress.pincode |
BillingPostalCode |
BillingCountry |
string | yes on this call | always the literal India |
BillingCountry |
Phone |
string | no | contactPhone |
Phone |
Contact_Phone__c |
string | no | same phone as Phone |
Contact_Phone__c |
Contact_Name__c |
string | no | contactName |
Contact_Name__c |
Email__c |
string | no | contactEmail |
Email__c |
Payment_Terms__c |
string | no | Net 30 when the Sold To value is blank. It was not in the Salesforce request |
Payment_Terms__c |
Territory__r.Territory_Code__c |
string | no | territoryCode. The whole Territory__r object is omitted when the code is blank |
Territory__c, resolved by external id |
Account_Status__c |
string | yes | Active |
Account_Status__c |
ERP_Record_ID__c |
string | no | Sold To primary key. Falls back to DIST001 only when that id is blank |
ERP_Record_ID__c |
What Salesforce sends back¶
HTTP 204. The body is empty.
That save changes the record type from Temp to Sold To on an existing Account. SoldTo_AfterSave calls Account_AfterSave_Shared. The subflow sees prior record type Temp and current record type Sold To, and queues the record-id callback with accountType = SoldTo and erpCode = DIST001. The callback is skipped when Bypass_Automation__c.Is_Bypassed__c is true.
A 204 clears errorMessage on onboarding row ONB-2026-0001. The Sold To row in ERP still has an empty salesforceRecordId. The callback fills it. The outbound integration log reference is flowB writeback sfId=001000000000001AAA soldTo=DIST001.
| System | Record | State |
|---|---|---|
| Salesforce | 001000000000001AAA |
Record type Sold To. Sold_To_Code__c = DIST001. ERP_Record_ID__c is still the UUID until the onboarding HTTP response is applied |
| ERP | Sold To DIST001 |
Unchanged by this PATCH. salesforceRecordId empty |
| ERP | Onboarding ONB-2026-0001 |
errorMessage empty |
Errors¶
A non-success from the query or the PATCH is returned to writeBackOnboarding, stored in onboarding_requests.errorMessage, and logged. processOnboardingReceive ignores that error. Salesforce still receives HTTP 200 and the codes. The Account can still be record type Temp. Sold_To_Code__c stays blank, because only this PATCH writes it.
Salesforce REST errors use the platform body, for example a duplicate Sold_To_Code__c or a validation rule on GST, PAN, or billing address. ERP does not rewrite that body. It wraps the message:
If the Bill To step has not run yet, a PATCH failure means no Bill To Account is created either. WriteBackOnboarding creates Bill Tos only after the PATCH returns. See Failures and retry.
POST /sobjects/Account¶
Create the Bill To. This runs only after the Sold To PATCH succeeded, and still before the 200 response. Caller: upsertBillToDuringOnboarding.
POST /services/data/v67.0/sobjects/Account
Authorization: Bearer {access token}
Content-Type: application/json
Sforce-Duplicate-Rule-Header: allowSave=true
The duplicate-rule header lets the Bill To save when it shares the Sold To GSTIN and address and the org's duplicate rule is set to allow the save.
One Bill To is created on the automatic path. Its name is {prospect name} - Billing. A manual respond that passes several codes names the later ones {prospect name} - Billing 2, and so on.
Pre-check¶
An empty result means create. A hit means update that Account instead, with the body in Update, if BILL001 already exists.
What ERP sends¶
The parent is the Sold To code, not a Salesforce Id. ERP does not have the Sold To Id yet. The callback has not run. GST is copied from the Sold To, not from a separate field on the original push. RecordTypeId is the org's Bill_To record type. ERP_Record_ID__c is the ERP Bill To primary key.
{
"RecordTypeId": "012000000000002AAA",
"Name": "Sharma Distributors - Billing",
"Bill_To_Code__c": "BILL001",
"Parent": {
"Sold_To_Code__c": "DIST001"
},
"GST_Registration_Number__c": "29ABCDE1234F1Z5",
"BillingStreet": "21 Residency Road",
"BillingCity": "Bengaluru",
"BillingState": "Karnataka",
"BillingPostalCode": "560025",
"BillingCountry": "India",
"Account_Status__c": "Active",
"ERP_Sync_Status__c": "Synced",
"ERP_Last_Sync_Timestamp__c": "2026-11-05T12:00:00Z",
"ERP_Record_ID__c": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
| JSON field | Type | Required | Source in ERP | Salesforce field |
|---|---|---|---|---|
RecordTypeId |
id | yes | record type cache, developer name Bill_To |
RecordTypeId |
Name |
string | yes | Sharma Distributors - Billing |
Name |
Bill_To_Code__c |
string | yes | BILL001, allocated from that Bill To name |
Bill_To_Code__c |
Parent.Sold_To_Code__c |
string | yes, when ParentId is unknown |
parent Sold To code | ParentId, resolved by Sold_To_Code__c |
ParentId |
id | used instead of Parent once the parent Salesforce Id is known |
not set on this first create | ParentId |
GST_Registration_Number__c |
string | yes | Sold To GSTIN | GST_Registration_Number__c |
BillingStreet |
string | yes | same address as the Sold To | BillingStreet |
BillingCity |
string | yes | same | BillingCity |
BillingState |
string | yes | normalized state | BillingState |
BillingPostalCode |
string | yes | pincode | BillingPostalCode |
BillingCountry |
string | yes | literal India |
BillingCountry |
Account_Status__c |
string | yes | Active |
Account_Status__c |
ERP_Sync_Status__c |
string | yes | literal Synced |
ERP_Sync_Status__c |
ERP_Last_Sync_Timestamp__c |
string | no | UTC time of the call, RFC3339 | ERP_Last_Sync_Timestamp__c |
ERP_Record_ID__c |
string | no | Bill To primary key. Falls back to BILL001 when that id is blank |
ERP_Record_ID__c |
What Salesforce sends back¶
HTTP 201 Created
HTTP 200 with the same shape is also accepted. An empty id is a failure.
The insert is a new Bill To. BillTo_AfterSave calls Account_AfterSave_Shared, which queues a record-id callback with accountType = BillTo and erpCode = BILL001, unless automation bypass is on.
ERP does not write 001000000000002AAA onto the local Bill To from this response. The callback does that. The id is checked only to confirm the create returned one. The Salesforce caller of /webhook/onboarding is still waiting, and receives 200 only after this create returns.
| System | Record | State |
|---|---|---|
| Salesforce | 001000000000002AAA |
Record type Bill To, parent is the Account whose Sold_To_Code__c is DIST001, GSTIN copied from the Sold To, ERP_Sync_Status__c = Synced |
| ERP | Bill To BILL001 |
Present. salesforceRecordId still empty |
Update, if BILL001 already exists¶
A retry sends a PATCH instead of a second create.
{
"Name": "Sharma Distributors - Billing",
"GST_Registration_Number__c": "29ABCDE1234F1Z5",
"BillingStreet": "21 Residency Road",
"BillingCity": "Bengaluru",
"BillingState": "Karnataka",
"BillingPostalCode": "560025",
"BillingCountry": "India",
"ERP_Sync_Status__c": "Synced",
"ERP_Last_Sync_Timestamp__c": "2026-11-05T12:00:00Z"
}
Salesforce answers HTTP 204 with an empty body. Bill_To_Code__c and Parent are not sent again. This branch is the retry path, after a first attempt created the Bill To and a later write-back is repeated.
Errors¶
A query, create, or update failure is wrapped as:
or update bill to BILL001 during onboarding write-back: ..., or pre-check bill to code BILL001 in Salesforce: ....
On the automatic receive, that message is stored on the onboarding row and the original Salesforce caller still gets HTTP 200 with soldToCode and billToCodes. The Sold To upgrade has already committed. A Bill To failure does not roll it back. The local Bill To row BILL001 exists either way. It was inserted before write-back started.