curl --request POST \
--url https://app.vumasign.com/api/v1/envelopes/{envelopeId}/send \
--header 'Authorization: Bearer <token>' \
--header 'Idempotency-Key: <idempotency-key>'import requests
url = "https://app.vumasign.com/api/v1/envelopes/{envelopeId}/send"
headers = {
"Idempotency-Key": "<idempotency-key>",
"Authorization": "Bearer <token>"
}
response = requests.post(url, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Idempotency-Key': '<idempotency-key>', Authorization: 'Bearer <token>'}
};
fetch('https://app.vumasign.com/api/v1/envelopes/{envelopeId}/send', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://app.vumasign.com/api/v1/envelopes/{envelopeId}/send"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("Idempotency-Key", "<idempotency-key>")
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}{
"id": "01960000-0000-4000-8000-0000000000e5",
"status": "sent",
"template_id": "01960000-0000-4000-8000-0000000007e1",
"title": "Employment contract",
"created_at": "2026-09-02T09:00:00.000Z",
"sent_at": "2026-09-02T09:05:00.000Z",
"expires_at": null,
"recipients": [
{
"id": "01960000-0000-4000-8000-00000000005e",
"role": "Employee",
"name": "Thandi Mokoena",
"email": "thandi@example.test",
"routing_type": "sign",
"status": "sent",
"invitation_delivered": true,
"embedded": false,
"external_ref": null
}
]
}{
"error": {
"code": "unauthenticated",
"message": "<string>"
}
}{
"error": {
"code": "unauthenticated",
"message": "<string>"
}
}{
"error": {
"code": "unauthenticated",
"message": "<string>"
}
}{
"error": {
"code": "unauthenticated",
"message": "<string>"
}
}{
"error": {
"code": "unauthenticated",
"message": "<string>"
}
}{
"error": {
"code": "unauthenticated",
"message": "<string>"
}
}{
"error": {
"code": "unauthenticated",
"message": "<string>"
}
}{
"error": {
"code": "unauthenticated",
"message": "<string>"
}
}{
"error": {
"code": "unauthenticated",
"message": "<string>"
}
}Send a draft envelope to its recipients.
THE SECOND HALF OF THE CREATED-VERSUS-SENT SPLIT. POST /api/v1/envelopes with "send": false makes a draft and emails nobody; this is how that draft goes out. Two acts an integrator can separate — build the envelope when a form is submitted, send it when a human approves — which is the arrangement a one-shot create cannot express without either sending too early or holding a request open.
⚠️ A SUB-RESOURCE AND NOT PATCH {"status": "sent"}. Sending emails people, consumes billing allowance and writes an audit event; making that look like editing a property would make an irreversible, billable act look reversible.
200, not 201. Nothing was created — the envelope existed before this call. A client branching on 201 to mean “store this new id” would be wrong here every time.
The idempotency, which is the point
⚠️ Idempotency-Key IS REQUIRED. None of DocuSign, Dropbox Sign or BoldSign offers one on the endpoint that costs real money; this is that endpoint, and an optional guarantee is one nobody sends until they have already been burned.
A REPLAY MEANS “you already sent this, and here is what happened” — the first attempt’s answer, byte for byte, with Idempotency-Replayed: true. Not a second send, not a resend, not a refusal. That is what makes a timeout safe to retry.
A DIFFERENT ENVELOPE UNDER A USED KEY IS idempotency_key_reused, never a replay — including a key you already used to CREATE something. A key identifies one request.
KEYS ARE REMEMBERED FOR 1 DAY, per organisation and per environment. Past that the same string is a new request — and a new request to send an envelope that has already gone out is envelope_not_draft, not a second send. Two mechanisms, and the second one is the database: draft → sent is a locked, one-way transition, so a duplicate send is impossible whatever the key says. The key decides what the second caller is TOLD.
What it refuses
A REFUSAL SENDS NOTHING AND CHANGES NOTHING. The envelope is left exactly as it was — unlike a refused "send": true on the create endpoint, which deletes the draft it had just built — and the idempotency key is freed, so a caller who upgrades their plan or fixes their roster retries with the same key.
⚠️ A vsk_test_ KEY MAY SEND ONLY A SANDBOX ENVELOPE — one a test key created — and only to people it may email. A draft a live key created is refused with test_key_cannot_send, and so is one whose emailed recipients include an address that is neither a member of your organisation nor a confirmed test recipient (Settings → Test recipients). An "embedded": true recipient is never emailed and is never checked. A test send consumes no allowance.
curl --request POST \
--url https://app.vumasign.com/api/v1/envelopes/{envelopeId}/send \
--header 'Authorization: Bearer <token>' \
--header 'Idempotency-Key: <idempotency-key>'import requests
url = "https://app.vumasign.com/api/v1/envelopes/{envelopeId}/send"
headers = {
"Idempotency-Key": "<idempotency-key>",
"Authorization": "Bearer <token>"
}
response = requests.post(url, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Idempotency-Key': '<idempotency-key>', Authorization: 'Bearer <token>'}
};
fetch('https://app.vumasign.com/api/v1/envelopes/{envelopeId}/send', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://app.vumasign.com/api/v1/envelopes/{envelopeId}/send"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("Idempotency-Key", "<idempotency-key>")
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}{
"id": "01960000-0000-4000-8000-0000000000e5",
"status": "sent",
"template_id": "01960000-0000-4000-8000-0000000007e1",
"title": "Employment contract",
"created_at": "2026-09-02T09:00:00.000Z",
"sent_at": "2026-09-02T09:05:00.000Z",
"expires_at": null,
"recipients": [
{
"id": "01960000-0000-4000-8000-00000000005e",
"role": "Employee",
"name": "Thandi Mokoena",
"email": "thandi@example.test",
"routing_type": "sign",
"status": "sent",
"invitation_delivered": true,
"embedded": false,
"external_ref": null
}
]
}{
"error": {
"code": "unauthenticated",
"message": "<string>"
}
}{
"error": {
"code": "unauthenticated",
"message": "<string>"
}
}{
"error": {
"code": "unauthenticated",
"message": "<string>"
}
}{
"error": {
"code": "unauthenticated",
"message": "<string>"
}
}{
"error": {
"code": "unauthenticated",
"message": "<string>"
}
}{
"error": {
"code": "unauthenticated",
"message": "<string>"
}
}{
"error": {
"code": "unauthenticated",
"message": "<string>"
}
}{
"error": {
"code": "unauthenticated",
"message": "<string>"
}
}{
"error": {
"code": "unauthenticated",
"message": "<string>"
}
}Authorizations
Authorization: Bearer vsk_live_…. Chosen over a bespoke X-API-KEY header because every client, proxy and log-redaction rule already knows this one. What a key may DO is its scopes — see x-scopes at the root of this document and x-required-scope on each operation. The scope list is not written here because OpenAPI reserves a requirement’s scope array for oauth2 and openIdConnect and requires it to be empty for an http scheme.
Headers
Any string that identifies this request; a UUID is the usual choice, and at most 255 characters. ⚠️ REQUIRED. Without it a timeout is unresolvable: you cannot learn whether the envelope went out, and retrying either sends it twice or tells you it is not a draft without saying who sent it. The caller’s own request identifier.
"01960000-0000-4000-8000-00000000ffff"
Path Parameters
The draft envelope’s id, as returned when it was created.
An envelope id.
"01960000-0000-4000-8000-0000000000e5"
Response
The envelope, now sent. invitation_delivered on each recipient is what the email provider said about that invitation — null where none was attempted, which on a sequential envelope is every position after the first.
⚠️ HERE, AND ONLY HERE, true MEANS "THE PROVIDER ACCEPTED IT" RATHER THAN "THE PROVIDER DELIVERED IT". This response is built from the answers the provider gave while the send was running, and at that moment nobody — us or them — knows whether the message will arrive. Delivery is reported afterwards, and GET /api/v1/envelopes/{id} is where it shows up. If the distinction matters to you, re-read there rather than trusting this snapshot.
⚠️ A recipient still reading pending after a successful send is NORMAL on a sequential envelope, not a failure.
The envelope, as it now stands.
The envelope’s id. A bare uuid.
draft or sent from this endpoint. Later states arrive as people act.
The template this was made from, or null when there was none — an envelope created by POST /api/v1/envelopes/one-off carries the document itself and never had a template. Branch on null, never on the empty string.
What the signers see naming the document. Defaults to the template’s name.
ISO 8601, UTC.
ISO 8601, UTC. Null while it is a draft.
ISO 8601, UTC. Null when this envelope has no deadline, which is the default. Read back from the stored value rather than echoed, so an offset you sent comes back as the same instant in UTC.
In routing order.
Show child attributes
Show child attributes