Building a Controlled Vendor Write-Back API for ServiceNow TPRM Integrations

anand-bhosle
Tera Guru
Spoiler

Note: This is a longer implementation write-up. If you are here for the quick pattern, focus on the sections: Final Design Pattern, Key Guardrails, Script Include Example, and Testing Checklist.

*Introduction*
:

While working on a Third-Party Risk Management integration, I had a requirement where an external platform needed to write vendor-related data back into ServiceNow. At first glance, this sounds simple: “Allow the external system to update vendor records.”  But in a TPRM implementation, this quickly becomes more sensitive. Vendor records are not just plain reference data. They may be connected to due diligence, engagements, risk assessments, business owners, vendor flags, Incidents, Business Applications, lifecycle status, reporting, and downstream processes. So instead of exposing direct Table API access, I ended up building a controlled Scripted REST API pattern with guardrails, logging, system properties, and a Script Include service layer.



This post shares the pattern, the pitfalls, and the implementation approach that worked for me.


Use Case:

The requirement was to allow an external third-party platform to insert or update vendor [core_company] records in ServiceNow.

The primary table in this example is "core_company"  & external platform sends vendor data such ass

{
"external_id": "VND-10001",
"name": "Example Vendor",
"status": "active",
"business_owner": "owner@example.com",
"corporate_description": "Sample vendor description"
}

 

The ServiceNow API should then:

  • Validate whether the integration is enabled
  • Validate whether the request is coming from the expected integration user
  • Check if the vendor already exists
  • Update the existing company record or insert a new one
  • Set required vendor flags
  • Set data source
  • Return a clear success/error response
  • Log what happened for troubleshooting

 

Why I Did Not Use Direct Table API?

Direct Table API can work for simple integrations, but I did not want to give the external platform broad write access to core_company.

Some reasons:

  • core_company is a shared foundational table
  • Vendor data may be used by multiple modules
  • External systems should not control every field
  • ACL behavior can create confusion during testing
  • Business logic should be centralized and controlled
  • Future field ownership may change
  • It is easier to add guardrails in a scripted REST API

With a Scripted REST API, the external platform gets only one controlled entry point.

The API decides what fields can be updated, what validations are required, and what should be rejected.


Final Design Pattern

The final pattern looked like this:

External Platform ---> Scripted REST API Resource ---> Script Include Service Layer ---> core_company

 

The Scripted REST resource handles the request and response.

The Script Include handles the actual business logic.

This separation is important because putting all logic directly inside the REST resource becomes hard to maintain, test, and extend.


Key Guardrails:

I utilized several system properties to enhance the integration's safety.

  • x_company.tprm.integration.enabled
  • x_company.tprm.integration.allowed_user
  • x_company.tprm.integration.data_source

1. Integration Enabled Property

This acts as a kill switch. "x_company.tprm.integration.enabled = true / false"

If something goes wrong, the integration can be stopped without changing code.


2. Allowed User Property

This ensures only the expected integration user can call the API. 

x_company.tprm.integration.allowed_user = tprm.integration.user

This is useful even when OAuth or Basic Auth is already configured, because it adds another explicit check inside the API logic.


3. Data Source Property

Instead of hardcoding a sys_id, the data source can be stored in a property.

x_company.tprm.integration.data_source = <sys_id of data source record>

This makes the implementation easier to move across Dev, Test, and Prod.


Sample Payload

For a single vendor:

 
{
"external_id": "VND-10001",
"name": "Example Vendor",
"status": "active",
"business_owner": "owner@example.com",
"corporate_description": "Sample corporate description"
}
For batch processing:


{
"records": [
{
"external_id": "VND-10001",
"name": "Example Vendor 1",
"status": "active",
"business_owner": "owner1@example.com",
"corporate_description": "Sample description 1"
},
{
"external_id": "VND-10002",
"name": "Example Vendor 2",
"status": "pending",
"business_owner": "owner2@example.com",
"corporate_description": "Sample description 2"
}
]
}

 

Scripted REST API Resource Example

Below is a simplified and sanitized example.

(function process(request, response) {

var LOG_PREFIX = '[TPRM Vendor WriteBack API] ';

try {
var body = request.body.data;

gs.info(LOG_PREFIX + 'Request received by user=' + gs.getUserName());

var service = new TPRMVendorWritebackService();
var result = service.upsertBatch(body);

response.setStatus(result.statusCode || 200);
response.setBody(result);

} catch (ex) {
gs.error(LOG_PREFIX + 'Unhandled error: ' + ex.message);

response.setStatus(500);
response.setBody({
success: false,
message: 'Unexpected error while processing vendor write-back request.',
error: ex.message
});
}

})(request, response);
The REST resource stays clean. It receives the request, calls the service, and returns the result.

 

Script Include Example

Here is a sanitized Script Include pattern [attached]. "It would not let me copy here, so attaching the file"

 

Important Design Notes

Use a Stable Matching Key

The biggest lesson from this type of integration is this: "Do not rely only on vendor name if you can avoid it."

 

Vendor names can change, contain punctuation differences, or exist in duplicate forms.

Better matching keys include:

External vendor ID

ERP ID

Supplier ID

ServiceNow company sys_id

Unique integration reference ID

If the external platform cannot provide a stable ID, then name matching can be used as a fallback, but it should be treated carefully.

 

Keep Field Ownership Clear

Before allowing write-back, clarify which system owns which fields.

For example:

 
Field                     Owner
------------------------------------------------
Vendor name External platform or ServiceNow?
Vendor status External platform?
Business owner ServiceNow?
Risk score External risk platform?
Corporate description External platform?
Vendor flag ServiceNow logic?
 

Without clear ownership, integrations slowly become a mess. One system updates a field, another system overwrites it, users manually change it, and nobody knows which value is correct.


DO-NOT Let the External System Update Everything

Even if the external platform sends 20 fields, the API should update only approved fields.

For example:

companyGR.setValue('name', vendor.name);
companyGR.setValue('status', vendor.status);
companyGR.setValue('u_corporate_description', vendor.corporate_description);
 

Avoid blindly mapping every incoming payload key to a ServiceNow field.

Do not do this:

for (var field in vendor) {
companyGR.setValue(field, vendor[field]);
}



That pattern is risky because the external payload can accidentally or intentionally update fields you did not intend to expose.


Testing Checklist

Here is the checklist I used while validating the integration.

 

 
1. Test when integration property is disabled
2. Test with unauthorized user
3. Test with authorized integration user
4. Test insert with a new vendor
5. Test update with an existing vendor
6. Test missing required fields
7. Test invalid business owner
8. Test valid business owner
9. Test inactive business owner
10. Test batch payload
11. Test duplicate vendor name scenario
12. Test invalid status value
13. Confirm vendor flags are set correctly
14. Confirm data source is populated
15. Confirm logs are clear enough for troubleshooting

Example Success Response

{
"success": true,
"statusCode": 200,
"total": 1,
"results": [
{
"success": true,
"operation": "updated",
"name": "Example Vendor",
"sys_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
]
}

 

Example Error Response

{
"success": false,
"statusCode": 403,
"message": "Integration is currently disabled."
}
 
Or for a record-level failure:

 

 
{
"success": true,
"statusCode": 200,
"total": 1,
"results": [
{
"success": false,
"name": "",
"message": "Vendor name is required."
}
]
}



----------------------------------------------------------------------------------------------------

                                                  Pitfalls I Ran Into                                                              

 

1. Table API and Scripted REST API Behave Differently

Testing with Table API may fail because of ACLs or role constraints, while the Scripted REST API may behave differently depending on how the logic is written and executed. That does not mean security should be ignored.

It means the Scripted REST API should include its own validation, allowed user checks, and controlled field updates.


2. OAuth Success Does Not Mean API Authorization Success

Getting an OAuth token is only the first step.

The integration user still needs the right access, and the Scripted REST API still needs to validate whether that user should be allowed to perform the operation.


3. Hardcoded sys_ids Create Migration Problems

Avoid hardcoding sys_ids in the Script Include.

This is especially important when moving between:

 
Dev ,Test, Stage, Prod
Use system properties where possible.

4. Vendor Matching by Name Can Create Duplicates

If the integration matches only on the vendor name, duplicate or near-duplicate records can happen.

Examples:

 

 
ABC Company
ABC Company LLC
A.B.C. Company
ABC Co.
A stable external identifier is much safer.

5. Logging Matters More Than You Think

When an external platform says “we sent the payload,” ServiceNow logs become your source of truth.

Use a consistent log prefix.

Example:

[TPRM Vendor WriteBack API]
[TPRM Vendor WriteBack Service]
This makes debugging much easier.

What I Would Improve Next

If I were extending this pattern further, I would consider adding:

1. Dedicated integration log table
2. Request/response correlation ID
3. More detailed validation for choice fields
4. Better duplicate detection
5. Retry/error reporting process
6. Notification when failures happen
7. Automated Test Framework coverage
8. Separate configuration per environment
9. Rate limiting or max batch size
10. More robust user lookup logic
For example, business owner lookup could support:
 
email
user_name
sys_id
employee number
But I would still keep that logic controlled inside the Script Include.



-----------------------------------------------------------

                          Key Takeaways                             

For TPRM integrations, my biggest takeaway was: *"Do not expose broad write access when a controlled API can do the job better."*

  • A Scripted REST API gives you a clean integration boundary.
  • A Script Include gives you a maintainable service layer.
  • System properties give you operational control.
  • Clear logging gives you supportability.
  • And field ownership prevents long-term data quality issues.

This pattern helped me build a safer vendor write-back integration without giving the external platform unnecessary control over a foundational ServiceNow table.

 

Hope this post help few users who may have going through similar obstacles. Comment your challenge below.

 

#Vendor Risk, #GRC #3rd Party Integration #TPRM

 

 

0 REPLIES 0