Save sales policies
Create or partly update the rooftop-wide agent settings, such as sales and service policies. This is the save for the "Sales Policies" and "Service Policies" tasks.
Sales agents (inbound and outbound share these): send the
entityConfig.salessection and markSales Policiesdone.
There is one record per rooftop, shared by all its agents. The call merges the body into the stored record, saves it, then pushes the sections you touched to the agent backend.
Auth: Bearer token of a signed-in user, checked with the identity service. The user id is taken from the token. Rate limit: not documented. Processing: synchronous. The call waits for the database write and each push.
Before you call it
- You need
enterpriseIdandteamId. - Read the current record with Get the agent config. Each card you send replaces the stored card whole.
- In the console UI, the Sales and Service Policies pages are a separate settings app. What that app sends is to be confirmed. The Service Policies link opens the Agents tab, not the Policies tab.
- Send
Authorization: Bearer YOUR_TOKENandContent-Type: application/json.
How the merge works
sales,service,receptionistandchatmerge section by section.- Inside
policiesandreachOutAndFollowups, each card you send replaces the stored card whole. Cards you leave out are kept. ignoreAniNumbers,transferConfigandsilentHoursreplace whole.- A
nullvalue is ignored at every level, so it does not delete anything.
The database is written first. Then, one at a time, the call pushes what you touched to the agent backend:
- If you sent
ignoreAniNumbersorservice.policies: those go to the notification config. - If you sent
sales.reachOutAndFollowups: the cards you sent go to the speed-to-lead and follow-up config. - If you sent
transferConfig: it goes to the transfer config. - If you sent
silentHours: it goes to the silent-hours config.
sales.policies, receptionist and chat are stored only. The agents read them back at call time. If a push fails, the call returns an error but the database already holds the change. Nothing is rolled back.
Request body
- Scope it: send
enterpriseIdandteamId. - Pick sections: put at least one of
ignoreAniNumbers,sales,service,receptionist,chat,transferConfigorsilentHoursinentityConfig. - Whole cards: send each card you change in full.
| Parameter | Type | Required | Description |
|---|---|---|---|
enterpriseId | string | Required | Dealer group. |
teamId | string | Required | Rooftop. |
enterpriseName, teamName | string | Optional | Stored with the record. |
| entityConfig sections | object | Required | Send at least one of the sections below. |
entityConfig.sales.policies | object | Optional | Sales policy cards. See Sales policy cards. |
entityConfig.sales.reachOutAndFollowups | object | Optional | Speed-to-lead and follow-up cards. |
entityConfig.service.policies | object | Optional | Service policy string lists. Unknown keys are dropped. |
entityConfig.receptionist.policies | object | Optional | Receptionist rules and call script. |
entityConfig.chat.policies | object | Optional | Chat rules and chat persona. |
entityConfig.ignoreAniNumbers | string[] | Optional | Caller numbers to ignore. Replaces the list. |
entityConfig.transferConfig | object | Optional | type, mode, holdMusicUrl, ringingTimeout (number), onFailure, connectOnIvr, recordConsultRoom, recordPostTransfer. |
entityConfig.silentHours | object | Optional | monday to sunday, each { enabled, start_time, end_time }. |
Sales policy cards
Cards under sales.policies:
| Card | Fields |
|---|---|
testDrive | minDriverAge, licenseTypesAccepted[], insuranceRequired, salespersonRideAlong, standardDurationMinutes, testDriveRoute, multipleVehiclesInOneVisit, bookingCutoffBeforeCloseMinutes, documentsToBring[], extendedOvernightDrives{overnightOffered, formatsOffered[]}, atHomeTestDrive{offered, maxRadiusMiles, minLeadTimeHours, feeApplies, feeAmount}, notes[] |
shipping | homeDelivery{offered, maxRadiusMiles, deliveryFee{offered, feeStructure, feeAmount}}, outOfStateShipping{offered, handledBy, shippingCostArrangement, statesSupported[]}, airportPickup{offered}, outOfStateRegistration{offered, statesSupported[]}, notes[] |
cpoProgram | offered, programs[{programName, vehicleMakes[], inspectionPoints, warrantyTerms[], eligibilityCriteria[], customerFacingBenefits[]}], notes[] |
financePreQualify | onlinePreQualifyPageOffered, preQualifyPageUrl, creditCheckType (soft_pull or hard_pull), estimatedTimeMinutes, whatHappensAfterSubmit, notes[] |
tradeIn | acceptTradeIns, phoneEstimateOffered, onlineEstimatorUrl, notes[] |
vehicleHold | offered, depositRequired, depositAmount, refundPolicy, maxHoldDurationDays, notes[] |
additionalFees | fees[{name, amount}], notes[] |
paymentEstimates, leasePolicy | offered, notes[] |
miscellaneousRules | string[] |
| Any other key | Any object, or null |
Service policy lists
service.policies takes transportationOptions, supportedVehicles, availableServices, serviceRules, expressServicesRules, outboundServiceRules, diagnosticServicesRules and upsellingRules. Each is a string array.
Receptionist and chat policies
- Both accept
informationGapwithonMissingInfo(capture_and_followupordeflect_to_dealership) andfollowUpWindow(up to 120 characters), andmiscellaneousRules(up to 40 strings, each up to 400 characters). - Receptionist also accepts
incomingCallScript(up to 12,000 characters),incomingCallPointers(up to 40 strings, each up to 400 characters),nearbyTransit(up to 12),shuttles(up to 6) andnearbyPlaces(up to 10). - Chat also accepts
agentName(up to 80 characters) andopeningLine(up to 320 characters).
Sample request
curl -X POST "https://api.spyne.ai/central-config/v1/vini-config/agent-config" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"enterpriseId": "<enterpriseId>",
"teamId": "<teamId>",
"entityConfig": {
"sales": {
"policies": {
"testDrive": { "minDriverAge": 18, "insuranceRequired": true, "standardDurationMinutes": 30 }
}
},
"service": {
"policies": { "transportationOptions": ["shuttle", "loaner"] }
}
}
}'import requests
body = {
"enterpriseId": "<enterpriseId>",
"teamId": "<teamId>",
"entityConfig": {
"sales": {"policies": {"tradeIn": {"acceptTradeIns": True, "phoneEstimateOffered": False}}},
},
}
res = requests.post(
"https://api.spyne.ai/central-config/v1/vini-config/agent-config",
headers={"Authorization": "Bearer YOUR_TOKEN"},
json=body,
)
print(res.status_code, res.json())const res = await fetch('https://api.spyne.ai/central-config/v1/vini-config/agent-config', {
method: 'POST',
headers: { Authorization: 'Bearer YOUR_TOKEN', 'Content-Type': 'application/json' },
body: JSON.stringify({
enterpriseId: '<enterpriseId>',
teamId: '<teamId>',
entityConfig: { service: { policies: { serviceRules: ['Oil changes are first come, first served.'] } } },
}),
});
console.log(res.status, await res.json());Sample response
The 200 body is the saved record as stored. Live sections are not overlaid.
{
"_id": "<id>",
"enterpriseId": "<enterpriseId>",
"teamId": "<teamId>",
"userId": "<userId>",
"domain": "rooftop",
"entity": "AGENT_CONFIG",
"type": "single",
"entityconfig": {
"sales": { "policies": { "testDrive": { "minDriverAge": 18, "insuranceRequired": true, "standardDurationMinutes": 30 } } },
"service": { "policies": { "transportationOptions": ["shuttle", "loaner"] } }
},
"createdAt": "2026-10-01T00:00:00.000Z",
"updatedAt": "2026-10-12T00:00:00.000Z"
}{
"success": false,
"message": "Validation error",
"errors": [
{ "code": "custom", "path": ["entityConfig"], "message": "entityConfig must include at least one of: ignoreAniNumbers, sales, service, receptionist, chat, transferConfig, silentHours" }
]
}{ "status": "401", "message": "Invalid Auth key" }{ "success": false, "message": "<message from the agent backend>", "error": "<message from the agent backend>" }{ "success": false, "message": "Failed to save agent config", "error": "<error message>" }Response parameters (200)
| Parameter | Type | Description |
|---|---|---|
enterpriseId, teamId, userId | string | Record scope and last writer. |
domain, entity, type | string | rooftop, AGENT_CONFIG, single. |
entityconfig | object | The full merged record as stored. |
createdAt, updatedAt | string | Record timestamps. |
Errors
| Status | Reason | When it happens |
|---|---|---|
400 | Validation error | enterpriseId or teamId is missing, entityConfig has none of the seven sections, or a field breaks its type or length rule. |
401 | Invalid Auth key | Missing or rejected token. |
401 | User ID not found in token | The token check returned no user id. |
| Downstream status | Message from the agent backend | The speed-to-lead and follow-up, transfer or silent-hours push was rejected and the agent backend sent a message. The database already holds the change. |
| Downstream status | Generic request-failed message | The notification-config push failed, or another push failed without a message. |
500 | Failed to save agent config | The database read or write failed, or a push got no response at all. |
What happens next
The console UI does not call Mark a task done for "Sales Policies" or "Service Policies". The pages that edit these policies live in a separate settings app, and whether that app marks the tasks done is to be confirmed. The agents read these policies at call time through Get the agent config.
Updated about 1 hour ago
