Save service 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.

POSThttps://api.spyne.ai/central-config/v1/vini-config/agent-config
AuthBearer token (console user)Rate limitNot documentedProcessingSynchronous; waits for the database write and each push
📌

Service agents (inbound and outbound share these): send the entityConfig.service section and mark Service Policies done.

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 enterpriseId and teamId.
  • 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_TOKEN and Content-Type: application/json.

How the merge works

  • sales, service, receptionist and chat merge section by section.
  • Inside policies and reachOutAndFollowups, each card you send replaces the stored card whole. Cards you leave out are kept.
  • ignoreAniNumbers, transferConfig and silentHours replace whole.
  • A null value 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:

  1. If you sent ignoreAniNumbers or service.policies: those go to the notification config.
  2. If you sent sales.reachOutAndFollowups: the cards you sent go to the speed-to-lead and follow-up config.
  3. If you sent transferConfig: it goes to the transfer config.
  4. 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

Requiredalways sendOptionalcan be left out
Request checklist
  1. Scope it: send enterpriseId and teamId.
  2. Pick sections: put at least one of ignoreAniNumbers, sales, service, receptionist, chat, transferConfig or silentHours in entityConfig.
  3. Whole cards: send each card you change in full.
ParameterTypeRequiredDescription
enterpriseIdstringRequiredDealer group.
teamIdstringRequiredRooftop.
enterpriseName, teamNamestringOptionalStored with the record.
entityConfig sectionsobjectRequiredSend at least one of the sections below.
entityConfig.sales.policiesobjectOptionalSales policy cards. See Sales policy cards.
entityConfig.sales.reachOutAndFollowupsobjectOptionalSpeed-to-lead and follow-up cards.
entityConfig.service.policiesobjectOptionalService policy string lists. Unknown keys are dropped.
entityConfig.receptionist.policiesobjectOptionalReceptionist rules and call script.
entityConfig.chat.policiesobjectOptionalChat rules and chat persona.
entityConfig.ignoreAniNumbersstring[]OptionalCaller numbers to ignore. Replaces the list.
entityConfig.transferConfigobjectOptionaltype, mode, holdMusicUrl, ringingTimeout (number), onFailure, connectOnIvr, recordConsultRoom, recordPostTransfer.
entityConfig.silentHoursobjectOptionalmonday to sunday, each { enabled, start_time, end_time }.

Sales policy cards

Cards under sales.policies:

CardFields
testDriveminDriverAge, licenseTypesAccepted[], insuranceRequired, salespersonRideAlong, standardDurationMinutes, testDriveRoute, multipleVehiclesInOneVisit, bookingCutoffBeforeCloseMinutes, documentsToBring[], extendedOvernightDrives{overnightOffered, formatsOffered[]}, atHomeTestDrive{offered, maxRadiusMiles, minLeadTimeHours, feeApplies, feeAmount}, notes[]
shippinghomeDelivery{offered, maxRadiusMiles, deliveryFee{offered, feeStructure, feeAmount}}, outOfStateShipping{offered, handledBy, shippingCostArrangement, statesSupported[]}, airportPickup{offered}, outOfStateRegistration{offered, statesSupported[]}, notes[]
cpoProgramoffered, programs[{programName, vehicleMakes[], inspectionPoints, warrantyTerms[], eligibilityCriteria[], customerFacingBenefits[]}], notes[]
financePreQualifyonlinePreQualifyPageOffered, preQualifyPageUrl, creditCheckType (soft_pull or hard_pull), estimatedTimeMinutes, whatHappensAfterSubmit, notes[]
tradeInacceptTradeIns, phoneEstimateOffered, onlineEstimatorUrl, notes[]
vehicleHoldoffered, depositRequired, depositAmount, refundPolicy, maxHoldDurationDays, notes[]
additionalFeesfees[{name, amount}], notes[]
paymentEstimates, leasePolicyoffered, notes[]
miscellaneousRulesstring[]
Any other keyAny 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 informationGap with onMissingInfo (capture_and_followup or deflect_to_dealership) and followUpWindow (up to 120 characters), and miscellaneousRules (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) and nearbyPlaces (up to 10).
  • Chat also accepts agentName (up to 80 characters) and openingLine (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)

ParameterTypeDescription
enterpriseId, teamId, userIdstringRecord scope and last writer.
domain, entity, typestringrooftop, AGENT_CONFIG, single.
entityconfigobjectThe full merged record as stored.
createdAt, updatedAtstringRecord timestamps.

Errors

StatusReasonWhen it happens
400Validation errorenterpriseId or teamId is missing, entityConfig has none of the seven sections, or a field breaks its type or length rule.
401Invalid Auth keyMissing or rejected token.
401User ID not found in tokenThe token check returned no user id.
Downstream statusMessage from the agent backendThe 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 statusGeneric request-failed messageThe notification-config push failed, or another push failed without a message.
500Failed to save agent configThe 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.


Did this page help you?