Skip to content
Product

ERP to Salesforce

ERP → Salesforce

ERP is the client. Salesforce is the server. Auth is the OAuth client credentials described under How the systems connect, against /services/data/v67.0. If ERP has no instance URL and no token, the local rows are still saved and these calls are not made.

POST /api/product-categories

{
  "categoryCode": "CAT-TEA",
  "name": "Tea",
  "level": 1
}

A child category:

{
  "categoryCode": "CAT-TEA-LOOSE",
  "name": "Loose Tea",
  "level": 2,
  "parentCategoryCode": "CAT-TEA"
}

ERP saves the row, then upserts Salesforce, then answers HTTP 200. On a successful first create the body includes "syncStatus": "Synced" and "salesforceRecordId": "a0C000000000001AAA". A failed push still returns 200, with "syncStatus": "Failed". The same POST updates a code that already exists and pushes again.

HTTP error message
400 invalid_json invalid request body
400 validation_error category code is required
400 validation_error category name is required
400 validation_error category level must be >= 1
400 validation_error parent category code is required for level > 1
400 validation_error parent category not found
400 validation_error category cannot be its own parent

PATCH Product_Category__c by code

The code is in the URL, not the body.

PATCH /services/data/v67.0/sobjects/Product_Category__c/Category_Code__c/CAT-TEA
Authorization: Bearer {access token}
Content-Type: application/json
{
  "Category_Name__c": "Tea",
  "Category_Level__c": 1
}

A child adds the parent:

{
  "Category_Name__c": "Loose Tea",
  "Category_Level__c": 2,
  "Parent_Category__r": {
    "Category_Code__c": "CAT-TEA"
  }
}
Salesforce field Example Where ERP got it
Category_Code__c CAT-TEA the URL. External id. Not in the body
Category_Name__c Tea name
Category_Level__c 1 level
Parent_Category__r.Category_Code__c CAT-TEA parentCategoryCode. Omitted when blank

A first create is HTTP 201 with an id. ERP stores that id on the category. An update of a code Salesforce already has is HTTP 204 with an empty body. ERP treats the empty body as a failure: category upsert: empty id (status 204). The local row becomes Failed, and the id stored from the first create is left as it was. See Salesforce → ERP.

The log reference is categoryCode=CAT-TEA name="Tea" level=1 parent=. A category failure is written to the ERP integration log only. ERP does not post an Integration_Log__c for a category.

POST /api/product-categories/CAT-TEA/sync-to-salesforce sends the same PATCH again. Success is HTTP 200:

{
  "erpCode": "CAT-TEA",
  "accountType": "ProductCategory",
  "status": "Synced",
  "salesforceRecordId": "a0C000000000001AAA"
}

A Salesforce error on this retry is HTTP 500 internal_error with message internal server error.

POST /api/product-categories/sync-unsynced runs the same upsert for every category, ordered by level then code. The query returns rows already marked Synced, so a second pass hits the empty 204 and counts those as failed. The answer is HTTP 200, for example { "synced": 1, "failed": 0 }.

POST /api/products

CAT-TEA must already exist when categoryCode is sent.

{
  "product": {
    "productCode": "TEA-500",
    "productName": "Assam Leaf 500g",
    "description": "Loose leaf, 500 g pack",
    "categoryCode": "CAT-TEA",
    "uom": "Pack",
    "packSize": 1,
    "cartonSize": 20,
    "status": "Active",
    "cgstRate": 2.5,
    "sgstRate": 2.5,
    "igstRate": 5,
    "utgstRate": 0,
    "isBundle": false
  }
}

ERP answers HTTP 201 with the product and "components": []. A successful push sets syncStatus to Synced and salesforceRecordId to the id Salesforce returned. A failed push still returns 201, with syncStatus Failed.

PATCH /api/products/TEA-500 sends the same product fields at the top level, without a product wrapper, and pushes again. The product code stays the one in the URL. Success is HTTP 200.

A code may be at most 50 characters. It must start with a letter or digit and may contain ., _, and -.

HTTP error message
400 invalid_json invalid request body
400 validation_error product code is required
400 validation_error product code must be at most 50 characters
400 validation_error product code must be alphanumeric and may include '.', '_' or '-'; spaces and other special characters are not allowed
400 validation_error status must be Active, End of Life (EOL), Discontinuing, or Inactive
400 validation_error CGST rate must be between 0 and 28
400 validation_error category CAT-TEA not found
409 conflict product with this code already exists
404 not_found product not found

The 409 is only on create. The 404 is a PATCH or a sync for a code ERP does not have. SGST, IGST, and UTGST use the same 0-to-28 message with their own names.

Leaving categoryCode blank is allowed. categoryName is then sent as Category_Code__c.

PATCH Product_Master__c by code

PATCH /services/data/v67.0/sobjects/Product_Master__c/Product_Code__c/TEA-500
{
  "Product_Name__c": "Assam Leaf 500g",
  "Description__c": "Loose leaf, 500 g pack",
  "Status__c": "Active",
  "UOM__c": "Pack",
  "Pack_Size__c": 1,
  "Carton_Size__c": 20,
  "CGST_Rate__c": 2.5,
  "SGST_Rate__c": 2.5,
  "IGST_Rate__c": 5,
  "UTGST_Rate__c": 0,
  "Is_Taxable__c": true,
  "Is_Bundle__c": false,
  "Category_Code__c": "CAT-TEA"
}
Salesforce field Example Where ERP got it
Product_Code__c TEA-500 the URL. External id. Not in the body
Product_Name__c Assam Leaf 500g productName
Description__c Loose leaf, 500 g pack description. Omitted when blank
Status__c Active status
UOM__c Pack uom, mapped onto the Salesforce picklist
Pack_Size__c 1 packSize
Carton_Size__c 20 cartonSize
CGST_Rate__c 2.5 cgstRate
SGST_Rate__c 2.5 sgstRate
IGST_Rate__c 5 igstRate
UTGST_Rate__c 0 utgstRate
Is_Taxable__c true true when any GST rate is above 0
Is_Bundle__c false isBundle
Category_Code__c CAT-TEA categoryCode, or categoryName when the code is blank

uom is matched in lower case. pack becomes Pack. Also accepted: case → Case, carton → Carton, piece, pcs, ea, each, and bottle → Piece, kg → Kg, litre and liter → Litre, bag → Pack. Anything else, including a blank unit, is sent as OPEN POINT.

A first create is HTTP 201. ERP stores the id. A later update is HTTP 204 with an empty body, and ERP marks the product Failed: product upsert: empty id (status 204).

The log reference is productCode=TEA-500 name="Assam Leaf 500g" status=Active category=CAT-TEA isBundle=false.

POST /api/products/TEA-500/sync-to-salesforce sends the same PATCH. Success is HTTP 200:

{
  "erpCode": "TEA-500",
  "accountType": "Product",
  "status": "Synced",
  "salesforceRecordId": "a0P000000000001AAA"
}

POST /api/products/sync-unsynced upserts every product, including ones already Synced, so a second pass hits the empty 204. The answer is HTTP 200, for example { "synced": 1, "failed": 0 }.

Bundle TEA-GIFT

TEA-500 must already exist, and it must not itself be a bundle.

{
  "product": {
    "productCode": "TEA-GIFT",
    "productName": "Assam Leaf Gift Pack",
    "categoryCode": "CAT-TEA",
    "uom": "Pack",
    "packSize": 1,
    "cartonSize": 10,
    "status": "Active",
    "cgstRate": 2.5,
    "sgstRate": 2.5,
    "igstRate": 5,
    "utgstRate": 0,
    "isBundle": true
  },
  "components": [
    { "componentProductCode": "TEA-500", "componentQuantity": 2 }
  ]
}

The product PATCH goes out first, with "Is_Bundle__c": true. When that upsert returns an id, ERP replaces the bundle lines.

It queries the lines already stored:

GET /services/data/v67.0/query?q=SELECT Id FROM Bundle_Component__c WHERE Bundle_Product__r.Product_Code__c='TEA-GIFT'

Each id from that query is deleted:

DELETE /services/data/v67.0/sobjects/Bundle_Component__c/{id}

Salesforce answers HTTP 204 with an empty body. Then each component is created:

POST /services/data/v67.0/sobjects/Bundle_Component__c
{
  "Bundle_Product__r": { "Product_Code__c": "TEA-GIFT" },
  "Component_Product__r": { "Product_Code__c": "TEA-500" },
  "Component_Quantity__c": 2
}

Salesforce answers HTTP 201 with an id. ERP checks that the id is present and does not store it. A later sync deletes these rows and creates them again.

HTTP error message
400 validation_error bundle products must have at least one component
400 validation_error component product code is required
400 validation_error unknown component product code(s): TEA-500
400 validation_error nested bundles are not permitted; component(s) are themselves bundles: TEA-GIFT
400 validation_error cannot mark as bundle; already used as a component of: TEA-GIFT

Failed Integration Log

A failed product upsert or a failed bundle replace also posts Integration_Log__c. Log_Type__c is Inbound. Operation__c is Create on the first push and Update on a PATCH.

POST /services/data/v67.0/sobjects/Integration_Log__c
{
  "Log_Type__c": "Inbound",
  "Operation__c": "Update",
  "Status__c": "Failed",
  "ERP_Code__c": "TEA-500",
  "Error_Message__c": "product upsert: empty id (status 204)",
  "Timestamp__c": "2026-11-06T05:30:00.000Z",
  "Payload_Reference__c": "productCode=TEA-500 name=\"Assam Leaf 500g\" status=Active category=CAT-TEA isBundle=false"
}