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.
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:
| Event | Effect |
|---|---|
envelope-sent, envelope-delivered, recipient-viewed, envelope-viewed, recipient-authentication-failed, envelope-voided, voided, declined | Acknowledged. 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 event | Acknowledged. 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
contractIdandenterpriseIdas 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
- Sign it (DocuSign): send the signed JSON payload with the signature header.
- Name the event: include
eventandenvelopeId, either at the top level or underdata. - Point at the contract: include
data.envelopeSummary.customFields.textCustomFieldswithcontractIdandenterpriseId.
Headers
| Parameter | Type | Required | Description |
|---|---|---|---|
| Signature | — | Required | Send X-DocuSign-Signature-1, or -2. |
X-DocuSign-Signature-1 | string | Only one | HMAC-SHA256 of the raw body, base64. |
X-DocuSign-Signature-2 | string | Optional | Second signature, checked if -1 does not match. |
Content-Type | string | Required | application/json |
Body
These are the fields the handler reads. DocuSign sends more.
| Parameter | Type | Required | Description |
|---|---|---|---|
event | string | Required | DocuSign event name. Also read from data.event. |
envelopeId | string | Required if | The event is processed. Envelope ID. Also read from data.envelopeId. |
data.envelopeSummary.customFields.textCustomFields | object[] | Optional | Entries { "name": "contractId", "value": "..." } and { "name": "enterpriseId", "value": "..." } used to find the contract. |
signerEmail / data.signerEmail | string | Optional | Dealer signer email for recipient-completed. Falls back to the stored signer email. |
spyneSignerEmail / data.spyneSignerEmail | string | Optional | Spyne 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
| Parameter | Type | Description |
|---|---|---|
data.success | boolean | true when the event was handled. |
data.action | string | NO_ACTION or ALREADY_PROCESSED when nothing changed. Absent when the stage changed. |
data.event | string | Event received. |
data.contractId | string | Contract ID. |
data.enterpriseId | string | Dealer group ID (on a stage change). |
data.newStage | string | New contract stage (on a stage change). |
data.currentStage | string | Current stage (on ALREADY_PROCESSED). |
data.processedAt | string | Processing time (on a stage change). |
data.childDocumentsCreated | boolean | true when the stage moved to Contracted. |
data.message | string | Explanation for NO_ACTION and ALREADY_PROCESSED. |
Errors
| Status | Code | When it happens |
|---|---|---|
400 | MISSING_ENVELOPE_ID | A processed event has no envelope ID. |
401 | UNAUTHORIZED | The signature header is missing or does not match, or no webhook secret is configured. |
404 | CONTRACT_NOT_FOUND | The contractId custom field matches no contract, or (without custom fields) no contract history has the envelope ID. |
500 | SERVER_ERROR | Child 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.
Updated about 1 hour ago
