curl --request GET \
--url https://app.vumasign.com/api/v1/envelopes/{envelopeId}/documents/{position}/draft \
--header 'Authorization: Bearer <token>'import requests
url = "https://app.vumasign.com/api/v1/envelopes/{envelopeId}/documents/{position}/draft"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://app.vumasign.com/api/v1/envelopes/{envelopeId}/documents/{position}/draft', 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}/documents/{position}/draft"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}"<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>"
}
}The document as it stands, mid-flight, stamped DRAFT.
THE DOCUMENT WITH EVERY ANSWER SO FAR ON IT, WHILE IT IS STILL OUT FOR SIGNATURE. The operation above serves the document as originally sent and, once sealing has landed, the executed copy — and between those two moments there was nothing to fetch. This is that gap: the pages carry every signature, initial and answer of every party who has already signed or approved, every page is stamped DRAFT, and one page is appended listing who is still outstanding and what each of them is waiting to do.
⚠️ IT ANSWERS application/pdf DIRECTLY — THERE IS NO url, AND THE DIFFERENCE IS FORCED RATHER THAN CHOSEN. The operation above returns a short-lived url because the bytes it serves are at rest and a credential is how you reach them. A draft is rendered while you wait and is never stored anywhere, so there is nothing for a credential to point at. Content-Disposition: attachment carries a filename built from the envelope’s title.
⚠️ IT IS A SNAPSHOT AND IT IS NOT EVIDENCE. Nothing is stored, nothing is recorded and no seal is applied, so the bytes attest to nothing about themselves — the appended page says so in as many words. Two calls a second apart can differ, because a party may have signed in between. The artefact that IS evidence is the sealed copy, and it is served by the operation above once envelope.sealed has fired.
What it costs
⚠️ A DRAFT IS A FULL RENDER AND IS RATE LIMITED SEPARATELY FROM YOUR KEY’S MINUTE. Two ceilings apply, both per minute: one on the ENVELOPE, so a single record page cannot spend your whole allowance, and one on the ORGANISATION, shared by every envelope and every key you hold. rate_limited names which of the two refused and Retry-After says how long to wait. ⚠️ THE RateLimit-* HEADERS DO NOT DESCRIBE THESE BUCKETS — they mean here exactly what they mean on every other operation in this API, which is your key’s own per-minute allowance. Nothing is billed either way: a draft consumes no envelope and no document.
⚠️ A RENDER THAT FAILS HAS STILL SPENT ITS PLACE. The charge is taken before the renderer is asked, so a service_unavailable from this operation has counted against both ceilings, and it is not refunded — the counter only ever goes up, by design, so that nothing can talk the limiter out of what it has seen. An internal_error may or may not have been charged: some arise before the charge is taken (including a failure to take it) and some after, and the answer does not say which — so budget for it as though it counted. Retry a service_unavailable on its published backoff, not in a loop.
When there is no draft to take
draft_not_available (422) covers both of them and the message says which. An envelope still in draft has asked nobody to do anything, so the render would be your original with nothing written on it. A document that has already SEALED has a final executed copy, which is strictly better than a draft of it. Either way the next request is the same one: GET /api/v1/envelopes/{envelopeId}/documents.
⚠️ A voided, declined OR expired ENVELOPE STILL RENDERS. None of those ever completes and none ever seals, so a draft is the only copy of them that will ever exist — and a contract that died part-signed is exactly the thing somebody needs to file.
The rest of the rules
position is 1-based, matching what a sender is shown — “Document 2 of 3” — and ordinarily there is exactly one.
⚠️ documents:read, NOT envelopes:read. A key that may read an envelope’s status is not thereby a key that may fetch what it holds, and a draft is what it holds more than the original is, because a draft carries other parties’ signatures.
⚠️ A vsk_test_ KEY MAY READ THE STATUS OF ANY ENVELOPE IN ITS ORGANISATION BUT MAY ONLY DRAFT A SANDBOX ONE — test_key_cannot_read_documents otherwise. A part-signed real contract is still a real contract.
⚠️ NO Idempotency-Key. Nothing is created, so there is nothing to replay; retrying is a second render and spends the ceilings above.
⚠️ FOUR SITUATIONS ANSWER WITH THE SAME 404, the disclosure rule this namespace uses everywhere: no such envelope anywhere; one belonging to another organisation; an id that is not a uuid; and a position this envelope has no document at. “It exists but is not yours” is itself the secret.
curl --request GET \
--url https://app.vumasign.com/api/v1/envelopes/{envelopeId}/documents/{position}/draft \
--header 'Authorization: Bearer <token>'import requests
url = "https://app.vumasign.com/api/v1/envelopes/{envelopeId}/documents/{position}/draft"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://app.vumasign.com/api/v1/envelopes/{envelopeId}/documents/{position}/draft', 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}/documents/{position}/draft"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}"<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.
Path Parameters
The envelope’s id, as returned by POST /api/v1/envelopes.
An envelope id.
"01960000-0000-4000-8000-0000000000e5"
Which of the envelope’s documents, 1-based, as documents[].position reports it from GET /api/v1/envelopes/{envelopeId}/documents. ⚠️ There is one spelling of each position: 01, 1.0 and 1 are refused rather than read as 1, so that one document does not answer to five URLs.
Which document of the envelope.
x >= 11
Response
The document as it stands. Content-Disposition: attachment; filename="<title> (draft).pdf", and Cache-Control: no-store — a draft stops being true the moment the next party acts, so nothing may keep a copy of this response.
The PDF bytes. ⚠️ NOT JSON — like GET /api/v1/documents/{id}/pages/{n}, this operation’s success body is not a resource. Its refusals still are: every non-2xx is the same Error envelope in application/json.