Save or submit a contract

Save the contract form as a draft, or submit it. A submit checks the whole body and moves the contract to Contract-Initiated, ready to send for signature. Save data.contractId from the response: every later contract call needs it.

POSThttps://api.spyne.ai/credit/v6/contracting/create-contract-v2
AuthBearer token (console user)Rate limitNot documentedProcessingSynchronous; waits for validation and the contract write.

One endpoint covers two calls:

  • Save as draft (isDraft: true). Stores the whole body on the contract record and sets the stage to Draft. The only checks are that contractDetails.enterpriseId is present and that managerName, if sent, is an allowed value.
  • Submit (isDraft: false). Checks the whole body, then:
    • rejects the request if any rooftop already has a Contracted or Contract-Live contract,
    • rejects a second active Individual Dealer contract for the same dealer group,
    • stores the body on the contract and sets the stage to Contract-Initiated,
    • starts the contract signing history,
    • writes a stage log entry,
    • moves the dealer group stage to Contract-Initiated.

No rooftops, product lines or plans are created at this point. They are created only after the contract is fully signed (see Receive DocuSign events).

If you leave out contractId, a new contract record is created from the default contract template. Its file location (contracts/ENTERPRISE_ID/CONTRACT_ID.pdf) is set at the same time. If you send contractId, that contract must belong to the same dealer group and still be in Draft.

Amendments (isAmendment: true) and Reseller contracts (customerType: "Reseller") use this endpoint too, but they follow separate validation paths that this page does not cover.

Send the bearer token of a signed-in Spyne Console user in Authorization: Bearer YOUR_TOKEN. The token is checked against the user validation endpoint on every call. The user ID from the token is saved as the contract's updatedBy.

Before you call it

  • The dealer group (enterpriseId) must already exist. If it cannot be found, a submit returns 400 "Enterprise not found".
  • Call Get contract config first. Every dropdown value must come from it, and pricing must be its data.pricing.
  • contractDetails.currency must have a stored conversion rate from USD. This applies to USD too.
  • If the country is India, the currency must be INR.
  • An Individual Dealer contract has exactly one rooftop. A Group Dealer contract needs customerDetails.contractedRooftops and customerDetails.billingTo.
  • Rooftop names must be unique within the contract (case-insensitive).
  • Every rooftops[].bundle must match a key in line_items.value.
  • Product line names and product names must exist in the product line registry and the product registry. Known Vini AI product names used by the console are salesInbound, salesOutbound, serviceInbound, serviceOutbound, receptionWebChat and acquisitionAiOutbound.
  • The call is synchronous. It waits for validation and the contract write. The history, stage log and dealer group stage updates also run before the response, but if they fail the request still succeeds. The rate limit is not documented.

Request

Requiredalways sendRequired ifrequired only in the case describedOptionalcan be left out
Request checklist
  1. Authenticate: send the Authorization: Bearer YOUR_TOKEN and Content-Type: application/json headers.
  2. Draft or submit: set isDraft to true or false. It is required on submit.
  3. New or existing: to continue a draft, send its contractId. Leave it out to start a new contract.
  4. Fill the details: send contractDetails, customerDetails, signatoryDetails, billingDetails and terms.
  5. Copy the pricing: copy data.pricing from Get contract config into pricing.
  6. Build the rooftops: one rooftops[] entry per rooftop. Each needs address fields, subType, bundle and at least one product line with at least one product.
  7. Price the bundles: build line_items with one bundle per bundle key used by the rooftops.
  8. Send totals: send summary totals, each a number with at most 2 decimal places.
  9. Keep the ID: read data.contractId from the response for Send for signature.

Headers

ParameterTypeRequiredDescription
AuthorizationstringRequiredBearer YOUR_TOKEN
Content-TypestringRequiredapplication/json

Body

"Required if submitting" means required when isDraft is false.

ParameterTypeRequiredDescription
isDraftbooleanRequiredtrue saves a draft. false submits the contract.
contractIdstringOptionalID of an existing Draft contract to update. Leave it out to create a new contract.
isAmendmentbooleanOptionaltrue routes to the amendment flow (not covered here). The console also sends amendedFromContractId with it.
contractDetailsobjectRequiredDeal-level details.
contractDetails.enterpriseIdstringRequiredID of the dealer group. Required for drafts too.
contractDetails.currencystringRequiredCurrency code. Must exist in the stored conversion rates. Must be INR when customerDetails.country is India.
contractDetails.dealTypestringRequiredOne of data.basicDetails.dealTypes. Currently only New Deal.
contractDetails.customerTypestringRequiredIndividual Dealer, Group Dealer or Reseller. You cannot switch a dealer group from Group Dealer to Individual Dealer while it has an active contract.
contractDetails.managerNamestringOptionalAccount manager. Empty or missing is allowed. If set, it must be one of data.basicDetails.managerNames.
customerDetailsobjectRequired ifSubmitting. Legal entity and address of the dealer group.
customerDetails.legalNamestringRequiredLegal name of the dealer group.
customerDetails.brandNamestringRequiredTrading name.
customerDetails.regionstringRequiredAMER, APAC, EMEA or OTHERS.
customerDetails.streetAddressstringRequiredStreet address.
customerDetails.countrystringRequiredCountry.
customerDetails.pincodestringRequiredPostal (ZIP) code, sent as a string.
customerDetails.statestringRequiredState.
customerDetails.citystringRequiredCity.
customerDetails.stateCodestringOptionalThe console sends it only when the country is India.
customerDetails.contractedRooftopsnumberRequired ifGroup Dealer. Number of contracted rooftops. Must be greater than 0 when sent.
customerDetails.billingTostringRequired ifGroup Dealer. Must be one of data.customerDetails.billingTo. Currently only Group Dealer (Combined).
pricingobjectRequired ifSubmitting. The pricing catalog from data.pricing of Get contract config. It must be a non-empty object and must contain an entry for every product line used in rooftops.
rooftopsobject[]Required ifSubmitting. At least one rooftop.
rooftops[].rooftopNamestringRequiredRooftop name. Unique within the contract. For an Individual Dealer, the console falls back to the legal name.
rooftops[].subTypestringRequiredIndependent Dealer, Franchise Dealer or Mixed.
rooftops[].regionstringRequiredAMER, APAC, EMEA or OTHERS. The console copies it from customerDetails.
rooftops[].streetAddress, country, pincode, state, citystringRequiredRooftop address. The console copies each from customerDetails.
rooftops[].bundlestringRequiredKey of the bundle in line_items.value that prices this rooftop (case-insensitive), for example bundle-1.
rooftops[].teamIdstringOptionalExisting rooftop ID. The console sends it only for amendments. On submit, if it is missing, the rooftop is looked up by name in the dealer group.
rooftops[].existingInventorynumberOptionalMust be a number when sent. The console sends this value inside the Studio AI product line instead.
rooftops[].productsLinesobject[]RequiredAt least one product line.
productsLines[].productLineNamestringRequiredProduct line name from the registry, for example conversationalAi (Vini AI) or studioAi (Studio AI).
productsLines[].planstringRequired ifstudioAi. Studio AI plan, Lite or Pro. Every product must be allowed in that plan. The console sends an empty string for conversationalAi.
productsLines[].productsobject[]RequiredAt least one product. Each has the four fields below.
products[].productstringRequiredProduct name from the product registry, for example salesInbound or serviceInbound.
products[].unitsstringRequiredVolume tier as a positive number in a string, for example "150".
products[].unitTypestringRequiredUnit of units. The console sends vinsPerMonth, leadsPerMonth (sales outbound, service outbound) or roPerMonth (service inbound).
products[].costnumberRequiredMonthly cost for this product. It must be a number. It is not checked against the catalog.
productsLines[].oneTimePurchasesstring[]OptionalOne-time charges the console adds, for example oneTimeSetupFee.
productsLines[].addOnsstring[]OptionalStudio AI add-on names the console adds.
productsLines[].existingInventorynumberOptionalStudio AI only. Existing inventory count the console uses for the setup fee.
signatoryDetailsobjectRequired ifSubmitting. The dealer-side person who signs.
signatoryDetails.namestringRequiredSigner name.
signatoryDetails.emailstringRequiredSigner email. Must be a valid email. DocuSign sends the envelope here.
signatoryDetails.designationstringRequiredSigner job title.
signatoryDetails.contactNumberobjectRequired{ "isdCode": "+1", "number": "5550100000" }. Both parts are required strings.
signatoryDetails.clientPOCsstring[]OptionalExtra dealer emails to copy on the DocuSign envelope. Invalid emails are skipped.
billingDetailsobjectRequired ifSubmitting. Billing frequency and finance contact.
billingDetails.billingFrequencynumberRequired1, 3, 6 or 12.
billingDetails.financePOCNamestringRequiredFinance contact name.
billingDetails.financePOCEmailstringRequiredFinance contact email. Must be a valid email.
billingDetails.financePOCNumberobjectRequired{ "isdCode": "+1", "number": "5550100000" }. Both parts are required strings.
billingDetails.billingTostringOptionalThe console copies customerDetails.billingTo here when it is set.
billingDetails.billingEntitystringOptionalBilling entity from data.billingEntities. To be confirmed: whether the key or the label is expected.
billingDetails.taxIdstringOptionalTax ID. The console trims it and sends it only when it is not empty.
termsobjectRequired ifSubmitting. Contract terms.
terms.t&cTemplatePathstringRequiredTerms template path, normally data.defaultTcTemplatePathPdf from the config call.
terms.editT&CbooleanRequiredtrue if custom terms were uploaded.
terms.exceptionbooleanRequiredtrue if the deal is raised as an exception. The console shows the signing step only when this is false.
terms.contractTermnumberRequiredContract term. The unit is not documented in code.
terms.lockInPeriodnumberRequiredLock-in period. The unit is not documented in code.
terms.noticePeriodForAutoRenewalnumberRequiredNotice period for auto renewal. The unit is not documented in code.
terms.terminationDaysnumberOptionalThe console sends a value from data.terminationDays (30 or 60). Not checked on submit.
terms.freeDaysnumberOptionalMust be a number when sent.
line_itemsobjectRequired ifSubmitting. Priced line items grouped by bundle.
line_items.typestringRequiredMust be bundle.
line_items.valueobjectRequiredOne entry per bundle key, for example bundle-1. It needs at least one bundle.
value.BUNDLE.studioAiobjectOptionalSections studio and addOns, each an array of line items.
value.BUNDLE.conversationalAiobjectOptionalSections named after the product categories in the registry, plus addOns, each an array of line items. The console sends salesAgent, serviceAgent, receptionAgent, acquisitionAgent and addOns. A section that is not a registry category is not checked.
value.BUNDLE.rooftopsCountnumberOptionalThe console sends the number of rooftops in the bundle. Not checked.
value.BUNDLE.existingInventoryPerRooftopnumberOptionalThe console sends this value. Not checked.
Each line item in a section——The fields below.
typestringRequiredLine label, for example Inbound, Outbound, PRO Plan, Setup Fee or oneTimeSetupFee.
frequencystringRequiredMonthly, One-Time, Annually, Quarterly or Half-Yearly.
basePricenumberRequiredList price. Must not be negative.
discountAmountnumberRequiredDiscount amount.
discountPercentnumberRequiredDiscount percent. At most 100.
finalPricenumberRequiredPrice after discount. Must not be negative.
unitsnumber or stringOptionalUnits the price covers.
summaryobjectRequired ifSubmitting. Fee totals.
summary.totalOneTimeFeenumberRequiredTotal one-time fees. At most 2 decimal places.
summary.totalMonthlyRecurringFeenumberRequiredTotal monthly recurring fees. At most 2 decimal places.
summary.totalAnnuallyRecurringFeenumberRequiredTotal annual recurring fees. At most 2 decimal places.

Sample request

The example is an Individual Dealer with one rooftop on Vini AI sales inbound and service inbound. All amounts are set to 0 as placeholders. Real amounts come from the pricing catalog and the deal. pricing is shortened: send the full data.pricing from the config call.

curl -X POST "https://api.spyne.ai/credit/v6/contracting/create-contract-v2" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "isDraft": false,
    "contractId": "CONTRACT_ID",
    "contractDetails": {
      "enterpriseId": "ENTERPRISE_ID",
      "currency": "USD",
      "dealType": "New Deal",
      "customerType": "Individual Dealer",
      "managerName": ""
    },
    "customerDetails": {
      "legalName": "Example Motors LLC",
      "brandName": "Example Motors",
      "region": "AMER",
      "streetAddress": "100 Example Street",
      "country": "United States",
      "pincode": "00000",
      "city": "Example City",
      "state": "Example State"
    },
    "pricing": {
      "conversationalAi": { "type": "<type>", "allowedProducts": {}, "items": {} }
    },
    "rooftops": [
      {
        "rooftopName": "Example Motors",
        "subType": "Franchise Dealer",
        "region": "AMER",
        "streetAddress": "100 Example Street",
        "country": "United States",
        "pincode": "00000",
        "city": "Example City",
        "state": "Example State",
        "bundle": "bundle-1",
        "productsLines": [
          {
            "productLineName": "conversationalAi",
            "plan": "",
            "oneTimePurchases": [],
            "products": [
              { "product": "salesInbound", "units": "100", "unitType": "vinsPerMonth", "cost": 0 },
              { "product": "serviceInbound", "units": "100", "unitType": "roPerMonth", "cost": 0 }
            ]
          }
        ]
      }
    ],
    "signatoryDetails": {
      "name": "Jane Doe",
      "email": "[email protected]",
      "designation": "General Manager",
      "contactNumber": { "isdCode": "+1", "number": "5550100000" },
      "clientPOCs": []
    },
    "billingDetails": {
      "billingFrequency": 12,
      "financePOCName": "John Doe",
      "financePOCEmail": "[email protected]",
      "financePOCNumber": { "isdCode": "+1", "number": "5550100000" }
    },
    "terms": {
      "t&cTemplatePath": "contracts/default-terms-and-conditions-template.pdf",
      "exception": false,
      "contractTerm": 12,
      "lockInPeriod": 12,
      "terminationDays": 30,
      "noticePeriodForAutoRenewal": 30,
      "editT&C": false
    },
    "line_items": {
      "type": "bundle",
      "value": {
        "bundle-1": {
          "existingInventoryPerRooftop": 0,
          "rooftopsCount": 1,
          "studioAi": { "studio": [], "addOns": [] },
          "conversationalAi": {
            "salesAgent": [
              { "type": "Inbound", "frequency": "Monthly", "basePrice": 0, "discountAmount": 0, "discountPercent": 0, "finalPrice": 0, "units": 100 }
            ],
            "serviceAgent": [
              { "type": "Inbound", "frequency": "Monthly", "basePrice": 0, "discountAmount": 0, "discountPercent": 0, "finalPrice": 0, "units": 100 }
            ],
            "receptionAgent": [],
            "acquisitionAgent": [],
            "addOns": []
          }
        }
      }
    },
    "summary": {
      "totalOneTimeFee": 0,
      "totalMonthlyRecurringFee": 0,
      "totalAnnuallyRecurringFee": 0
    }
  }'
import json
import requests

# contract.json holds the same body as the cURL example
with open("contract.json") as f:
    payload = json.load(f)

res = requests.post(
    "https://api.spyne.ai/credit/v6/contracting/create-contract-v2",
    json=payload,
    headers={"Authorization": "Bearer YOUR_TOKEN"},
)
print(res.status_code, res.json())
contract_id = res.json()["data"]["contractId"]
import { readFile } from "node:fs/promises";

// contract.json holds the same body as the cURL example
const payload = JSON.parse(await readFile("contract.json", "utf8"));

const res = await fetch("https://api.spyne.ai/credit/v6/contracting/create-contract-v2", {
  method: "POST",
  headers: { Authorization: "Bearer YOUR_TOKEN", "Content-Type": "application/json" },
  body: JSON.stringify(payload),
});
const body = await res.json();
console.log(res.status, body);
const contractId = body.data.contractId;

To save a draft instead, send the same body with "isDraft": true. For a new draft, leave out contractId.

Sample response

{
  "message": "Contract created successfully",
  "error": false,
  "code": "",
  "details": null,
  "data": {
    "success": true,
    "message": "Contract initiated successfully. Ready to send for signature.",
    "contractId": "CONTRACT_ID",
    "enterpriseId": "ENTERPRISE_ID",
    "contractStage": "Contract-Initiated",
    "note": "Child documents will be created after contract is fully signed"
  }
}
{
  "message": "Contract created successfully",
  "error": false,
  "code": "",
  "details": null,
  "data": {
    "success": true,
    "isDraft": true,
    "message": "Draft contract saved successfully",
    "contractId": "CONTRACT_ID",
    "enterpriseId": "ENTERPRISE_ID",
    "contractStage": "Draft"
  }
}
{
  "message": "Signatory email is required",
  "error": true,
  "code": "",
  "details": null,
  "data": {}
}
{
  "message": "Rooftop #1 \"Example Motors\":\n  - bundle is required\n  - Conversational AI → salesInbound: Unit type is required",
  "error": true,
  "code": "",
  "details": null,
  "data": {}
}
{
  "message": "Enterprise not found",
  "error": true,
  "code": "",
  "details": null,
  "data": { "enterpriseId": "ENTERPRISE_ID" }
}
{ "message": "Invalid Token", "error": true, "code": "ERROR_AUTH_401", "details": null, "data": null }
{
  "message": "Contract not found with ID: CONTRACT_ID",
  "error": true,
  "code": "",
  "details": null,
  "data": {}
}

Rooftop checks are grouped per rooftop in one message.

Response parameters

ParameterTypeDescription
data.successbooleantrue when saved.
data.isDraftbooleanPresent and true for a draft save.
data.messagestringResult message.
data.contractIdstringContract ID. Use it for later drafts, the PDF upload, and Send for signature.
data.enterpriseIdstringDealer group ID.
data.contractStagestringDraft or Contract-Initiated.
data.notestringPresent on submit. It states that child records are created after signing.

Errors

StatusMessageWhen it happens
400enterpriseId is requiredcontractDetails or contractDetails.enterpriseId is missing (draft or submit).
400Field check message, for example Currency is required, Invalid region "X". Please select a valid region., line_items.value.bundle-1.conversationalAi.salesAgent[0].frequency is required and must be one of: ..., summary.totalOneTimeFee must have up to 2 decimal placesA submit fails a body check. The first failing check is returned, except rooftop and product checks, which are collected into one message per rooftop.
400Individual Dealer can only have 1 rooftopIndividual Dealer with more than one rooftop.
400Duplicate rooftop names found: ...Two rooftops share a name.
400Rooftop "NAME" already has an active contract (Contracted or Contract-Live). Please create an amendment instead.A rooftop in the body is already under an active contract.
400An active Individual Dealer contract (...) already exists for this enterprise. ...The dealer group already has an active Individual Dealer contract.
400Cannot change account type from Group dealer to Individual dealer when active contracts existSwitching an active Group Dealer to Individual Dealer.
400Currency must be INR when Country is India (got currency: ...)Country is India and currency is not INR.
400Invalid account manager "...". Must be one of: ...managerName is set and not in the allowed list (draft or submit).
400Contract CONTRACT_ID does not belong to enterprise ENTERPRISE_IDcontractId belongs to another dealer group.
400Cannot modify contract CONTRACT_ID. Contract must be in 'Draft' stage. Current stage: ...contractId is not a draft.
400Enterprise not foundSubmit, and the dealer group could not be read.
401Invalid or missing tokenNo Bearer token, or the token failed validation.
408Request timed outThe token validation call could not connect.
500Contract not found with ID: CONTRACT_IDcontractId does not exist. This returns 500, not 404.
500Other error messageFor example, the default contract template is missing, or a database error occurs.

What happens next

  • The console builds the contract PDF and uploads it to the contract's file location, using an upload link from POST /console/v1/util/gen-presigned-contracting with contractId and enterpriseId.
  • If terms.exception is false, call Send for signature with data.contractId.

Draft saves can be repeated with the same contractId until the contract is submitted.

Called before: Get contract config, Reload a saved contract. Called after: Send for signature.


Did this page help you?