curl --request POST \
--url https://app.vumasign.com/api/v1/envelopes/{envelopeId}/recipients/{recipientId}/signing-url \
--header 'Authorization: Bearer <token>'import requests
url = "https://app.vumasign.com/api/v1/envelopes/{envelopeId}/recipients/{recipientId}/signing-url"
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
fetch('https://app.vumasign.com/api/v1/envelopes/{envelopeId}/recipients/{recipientId}/signing-url', 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}/recipients/{recipientId}/signing-url"
req, _ := http.NewRequest("POST", 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))
}{
"url": "https://app.vumasign.com/e/specimen-payload.specimen-signature",
"expires_at": "2026-09-02T10:15:00.000Z"
}{
"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>"
}
}Mint a short-lived, single-use URL for an embedded signer.
THE ONE ENDPOINT IN THIS API THAT RETURNS A BEARER CREDENTIAL FOR A LEGAL ACT. Anyone holding the url can open the document as that signer. Do not log it, cache it, store it or put it in a support ticket — put it in an iframe src and nowhere else.
⚠️ 10 MINUTES, AND SINGLE USE. The first GET spends it. A second GET within the 10 minutes renders a page that posts signing_url_invalid to your host page and signs nothing. ⚠️ AN EXPIRED URL POSTS NOTHING: it is refused a frame, so treat a ready that never arrives as “mint a new URL, once”. Mint it when the signer arrives, not when the envelope is created — Dropbox Sign’s own advice, and the reason a recipient id is durable while a URL is not.
⚠️ THE URL AND THE SESSION HAVE DIFFERENT LIVES. expires_at is when the URL stops being redeemable; a signer who redeems it just before then gets 1 hour from that moment to read and sign (or less, if the envelope’s deadline is sooner). Reload the iframe on this timer only if nobody ever opened it.
FOUR THINGS MUST BE TRUE, and each has its own code: the envelope is open for signing, the recipient was created with "embedded": true, they have not already acted, and it is their turn (a sequential envelope holds later positions back — recipient_not_yet_turn is a 409 and the identical call succeeds once the person in front finishes).
⚠️ A vsk_test_ key may mint only on a sandbox envelope — one a test key created — and is refused test_key_cannot_mint on any other.
Framing
The page is served with Content-Security-Policy: frame-ancestors naming the origins registered against the brand this envelope was sent under, and with no X-Frame-Options. Register those origins in Settings first; they must be bare — https://example.com, no trailing slash, no path, no wildcard — and an unregistered parent cannot frame the page at all. A URL we cannot verify — unreadable, altered or expired — gets X-Frame-Options: DENY instead, and the ordinary emailed signing page keeps X-Frame-Options: DENY unconditionally.
Talking to your page
The iframe posts { action, ...payload } to each registered origin, never to *. Actions: ready, signed, declined, delegated, signing_url_invalid. signing_url_invalid carries can_remint — branch on that, not on reason, or a signer who finishes in another tab turns your retry into an infinite loop.
⚠️ delegated MEANS THEY HANDED IT TO SOMEBODY ELSE: this recipient is retired and a replacement holds their position, so take the iframe down — but the envelope is not finished and there may be no webhook behind this one for a long time, because a sequential replacement is not invited until its turn and an embedded one is never emailed at all. Re-read the roster with GET /api/v1/envelopes/{envelopeId} and mint a URL for the new recipient id when you want them to sign in your page.
⚠️ NEITHER A postMessage NOR A RETURN URL IS A COMPLETION SIGNAL, AND THERE IS NO return_url ON THIS ENDPOINT FOR EXACTLY THAT REASON. signed is a browser event: the signer can close the tab, lose connectivity, or have any script on your own page forge it. The webhook is authoritative. Use these messages to move your interface — close the modal, navigate onwards — and use recipient.completed and envelope.completed to decide that a document was signed, to release goods, or to bill anybody. This is the mistake every first integration makes, and it is the one that is expensive.
NO Idempotency-Key, unlike POST /api/v1/envelopes. A retry mints a second URL, which emails nobody and bills nothing — and the response IS the credential, so “I do not know which state I am in” is not reachable. ⚠️ Redeeming a second URL does revoke the session a first one began.
curl --request POST \
--url https://app.vumasign.com/api/v1/envelopes/{envelopeId}/recipients/{recipientId}/signing-url \
--header 'Authorization: Bearer <token>'import requests
url = "https://app.vumasign.com/api/v1/envelopes/{envelopeId}/recipients/{recipientId}/signing-url"
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
fetch('https://app.vumasign.com/api/v1/envelopes/{envelopeId}/recipients/{recipientId}/signing-url', 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}/recipients/{recipientId}/signing-url"
req, _ := http.NewRequest("POST", 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))
}{
"url": "https://app.vumasign.com/e/specimen-payload.specimen-signature",
"expires_at": "2026-09-02T10:15:00.000Z"
}{
"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, as returned by POST /api/v1/envelopes or POST /api/v1/envelopes/one-off.
An envelope id.
"01960000-0000-4000-8000-0000000000e7"
The recipient’s id from that same response. ⚠️ Not the role name — a recipient has no natural key, because one person may hold two roles and two people may hold one.
A recipient id.
"01960000-0000-4000-8000-000000000060"
Response
The URL and when it stops being redeemable. Not readable again from anywhere: only a digest is stored.
The minted URL.
Single use. The first GET spends it; a second, within the 10 minutes, renders a page that posts { action: "signing_url_invalid", reason: "used", can_remint: true } to your host page, and the remedy is to call this endpoint again. There is no endpoint that reads this value back — a credential readable twice is one stored somewhere readable, and we store only a digest.
ISO 8601, UTC. 10 minutes from minting, or the envelope’s own deadline if that is sooner. ⚠️ THIS IS THE LIFE OF THE URL, NOT OF THE SIGNING SESSION: a signer who opens it just before it expires gets a full session of 1 hour (or less, if the envelope’s deadline is sooner) from that moment. A host that reloads the iframe on this timer rather than only when the URL was never opened will interrupt somebody mid-signature.