Receive DocuSign events

DocuSign calls this endpoint when signing events happen. It moves the contract to Contract-Pending-Spyne-Signature when the dealer signs, then to Contracted when all parties have signed, and creates the rooftop and product records. You do not call it yourself.

POSThttps://api.spyne.ai/credit/v6/contracting/docusign-webhook
AuthNo user token. Requires X-DocuSign-Signature-1 (or -2): HMAC-SHA256, base64, of the raw bodyCallerDocuSign Connect, not a console userRate limitNot documentedProcessingSynchronous; on envelope-completed waits for the signed PDF copy, child records and stage update.

DocuSign Connect calls it with an HMAC signature header. The request is rejected unless the signature matches the configured secret. Every accepted payload is logged.

What it does depends on the event:

EventEffect
envelope-sent, envelope-delivered, recipient-viewed, envelope-viewed, recipient-authentication-failed, envelope-voided, voided, declinedAcknowledged. No change.
recipient-completed (dealer signed)Sets the stage to Contract-Pending-Spyne-Signature. Records the dealer signer and sign date, and copies the sign date to the contract start date. Moves the dealer group stage to match.
envelope-completed (all parties signed)Saves the signed PDF as the contract file. Creates the contract's child records (see below). Sets the stage to Contracted with the dealer's sign date as the contracted date. Records the Spyne signer and sign date.
Any other eventAcknowledged. No change.

When a contract becomes Contracted, this endpoint:

  • creates or updates each rooftop (team), its product lines and its products for the dealer group, and moves the dealer group and its organisation to the matching stage,
  • writes the returned rooftop IDs and names back onto the stored contract payload,
  • creates the contract's plan records for each rooftop, product line and product,
  • logs the ARR change events,
  • creates a billing customer if one does not exist yet,
  • emails the "contracted" notice and the internal stakeholder notice. Both emails are fire and forget.

If creating the child records fails, the stage is not changed and the call returns 500. Repeated events are detected: a second envelope-completed for a Contracted contract returns ALREADY_PROCESSED.

There is no user token. The request must carry X-DocuSign-Signature-1 or X-DocuSign-Signature-2, an HMAC-SHA256 (base64) of the raw body with the shared webhook secret. Without a secret configured, every request is rejected.

Before you call it

  • Only DocuSign should call this endpoint. The envelope must have been created by Send for signature, which stores contractId and enterpriseId as envelope custom fields.
  • If the custom fields are missing, the contract is found by the envelope ID in the contract history.
  • The call is synchronous. On envelope-completed, the response waits for the signed PDF copy, the child record creation and the stage update. The notice emails are sent without waiting. The rate limit is not documented.

Request

Requiredalways sendOnly onesend exactly one from the groupRequired ifrequired only in the case describedOptionalcan be left out
Request checklist
  1. Sign it (DocuSign): send the signed JSON payload with the signature header.
  2. Name the event: include event and envelopeId, either at the top level or under data.
  3. Point at the contract: include data.envelopeSummary.customFields.textCustomFields with contractId and enterpriseId.

Headers

ParameterTypeRequiredDescription
Signature—RequiredSend X-DocuSign-Signature-1, or -2.
X-DocuSign-Signature-1stringOnly oneHMAC-SHA256 of the raw body, base64.
X-DocuSign-Signature-2stringOptionalSecond signature, checked if -1 does not match.
Content-TypestringRequiredapplication/json

Body

These are the fields the handler reads. DocuSign sends more.

ParameterTypeRequiredDescription
eventstringRequiredDocuSign event name. Also read from data.event.
envelopeIdstringRequired ifThe event is processed. Envelope ID. Also read from data.envelopeId.
data.envelopeSummary.customFields.textCustomFieldsobject[]OptionalEntries { "name": "contractId", "value": "..." } and { "name": "enterpriseId", "value": "..." } used to find the contract.
signerEmail / data.signerEmailstringOptionalDealer signer email for recipient-completed. Falls back to the stored signer email.
spyneSignerEmail / data.spyneSignerEmailstringOptionalSpyne signer email for envelope-completed.

Sample request

These samples show the payload shape. Without a valid signature they return 401.

curl -X POST "https://api.spyne.ai/credit/v6/contracting/docusign-webhook" \
  -H "Content-Type: application/json" \
  -H "X-DocuSign-Signature-1: HMAC_SIGNATURE" \
  -d '{
    "event": "envelope-completed",
    "data": {
      "envelopeId": "ENVELOPE_ID",
      "envelopeSummary": {
        "customFields": {
          "textCustomFields": [
            { "name": "contractId", "value": "CONTRACT_ID" },
            { "name": "enterpriseId", "value": "ENTERPRISE_ID" }
          ]
        }
      }
    }
  }'
import requests

payload = {
    "event": "envelope-completed",
    "data": {
        "envelopeId": "ENVELOPE_ID",
        "envelopeSummary": {"customFields": {"textCustomFields": [
            {"name": "contractId", "value": "CONTRACT_ID"},
            {"name": "enterpriseId", "value": "ENTERPRISE_ID"},
        ]}},
    },
}
res = requests.post(
    "https://api.spyne.ai/credit/v6/contracting/docusign-webhook",
    json=payload,
    headers={"X-DocuSign-Signature-1": "HMAC_SIGNATURE"},
)
print(res.status_code, res.json())
const res = await fetch("https://api.spyne.ai/credit/v6/contracting/docusign-webhook", {
  method: "POST",
  headers: { "Content-Type": "application/json", "X-DocuSign-Signature-1": "HMAC_SIGNATURE" },
  body: JSON.stringify({
    event: "envelope-completed",
    data: {
      envelopeId: "ENVELOPE_ID",
      envelopeSummary: { customFields: { textCustomFields: [
        { name: "contractId", value: "CONTRACT_ID" },
        { name: "enterpriseId", value: "ENTERPRISE_ID" },
      ] } },
    },
  }),
});
console.log(res.status, await res.json());

Sample response

{
  "message": "DocuSign webhook processed successfully. Contract stage: Contracted",
  "error": false,
  "code": "SUCCESS",
  "details": null,
  "data": {
    "success": true,
    "contractId": "CONTRACT_ID",
    "enterpriseId": "ENTERPRISE_ID",
    "event": "envelope-completed",
    "newStage": "Contracted",
    "processedAt": "2026-01-01T00:00:00.000Z",
    "childDocumentsCreated": true
  }
}
{
  "message": "Event 'envelope-delivered' acknowledged - no contract updates needed",
  "error": false,
  "code": "SUCCESS",
  "details": null,
  "data": { "success": true, "action": "NO_ACTION", "event": "envelope-delivered", "message": "Event 'envelope-delivered' acknowledged - no contract updates needed" }
}
{
  "message": "DocuSign webhook processed successfully. Contract stage: unchanged",
  "error": false,
  "code": "SUCCESS",
  "details": null,
  "data": {
    "success": true,
    "action": "ALREADY_PROCESSED",
    "event": "envelope-completed",
    "currentStage": "Contracted",
    "contractId": "CONTRACT_ID",
    "message": "Event 'envelope-completed' already processed - no duplicate updates needed"
  }
}
{ "message": "Missing envelopeId in webhook payload", "error": true, "code": "MISSING_ENVELOPE_ID", "details": null, "data": null }
{ "message": "Invalid webhook signature. Request not authenticated.", "error": true, "code": "UNAUTHORIZED", "details": null, "data": null }
{ "message": "Contract not found: CONTRACT_ID", "error": true, "code": "CONTRACT_NOT_FOUND", "details": null, "data": null }
{
  "message": "Cannot move to Contracted stage: Failed to create child documents - <error message>",
  "error": true,
  "code": "SERVER_ERROR",
  "details": null,
  "data": null
}

Response parameters

ParameterTypeDescription
data.successbooleantrue when the event was handled.
data.actionstringNO_ACTION or ALREADY_PROCESSED when nothing changed. Absent when the stage changed.
data.eventstringEvent received.
data.contractIdstringContract ID.
data.enterpriseIdstringDealer group ID (on a stage change).
data.newStagestringNew contract stage (on a stage change).
data.currentStagestringCurrent stage (on ALREADY_PROCESSED).
data.processedAtstringProcessing time (on a stage change).
data.childDocumentsCreatedbooleantrue when the stage moved to Contracted.
data.messagestringExplanation for NO_ACTION and ALREADY_PROCESSED.

Errors

StatusCodeWhen it happens
400MISSING_ENVELOPE_IDA processed event has no envelope ID.
401UNAUTHORIZEDThe signature header is missing or does not match, or no webhook secret is configured.
404CONTRACT_NOT_FOUNDThe contractId custom field matches no contract, or (without custom fields) no contract history has the envelope ID.
500SERVER_ERRORChild record creation failed (the stage is not changed), or another error occurred.

What happens next

After Contracted, each contracted product line on each rooftop exists with stage Contracted. The console's contracted-stage dashboard lists them for the sales-to-onboarding handover. The next client call is Save handover details.

Called before: Send for signature. Called after: Save handover details.


Did this page help you?