Document-executed billing
Turn a signed agreement into an invoice or subscription — quotes, order forms, external e-sign, and execution.
Document-executed billing
Some merchants don’t click a checkout button — they sign a document. A contractor sends a proposal, the customer signs it, and billing should start from what was signed. Document-executed billing is Vortex’s CPQ-style path for that: negotiated quote → approved order form → externally-signed document → executed order that produces an invoice or subscription.
Use it when the signature happens in your own e-sign stack (Seal, DocuSign, Dropbox Sign, a PDF with a wet signature) and Vortex needs to bill exactly what the signer agreed to.
The flow at a glance
- Create a quote —
POST /v1/quoteswith anorderType, a billing account, and catalog price lines. This creates draft version 1. - Negotiate —
PATCH /v1/quote-versions/{quoteVersionId}edits lines, terms, and discounts while the version staysdraft. OrPOST /v1/quotes/{quoteId}/versionsfor a fresh draft. - Approve —
POST /v1/quote-versions/{quoteVersionId}/approvelocks the version and generates the order form. - Send for signature — hand the order form to your signer. Register the
envelope with
POST /v1/order-forms/{orderFormId}/signature-requestso completions can be correlated. - Record the signature —
POST /v1/order-forms/{orderFormId}/signwith your signer’s refs. This creates the order in one transaction with the form transition — a signed form always has its order. - Execute —
POST /v1/orders/{orderId}/executeconverts the order into a finalized invoice (one_off) or subscriptions (subscription_creation).
Prerequisites
You need a customer, a billing account, and at least one catalog price. If the customer or account doesn’t exist yet, create them first:
PUT /v1/customers/{customerId}— upsert by your own ref;nameanddefaultCurrencyare required on create.POST /v1/customers/{customerId}/billing-accounts— requiresinvoiceDeliveryMode(email,manual,api_only),collectionMode(manualorautomatic), andautoCollectionEnabled. For document-executed billing,collectionMode: "manual"+invoiceDeliveryMode: "api_only"is the common choice — you render and deliver documents yourself.collectionMode: "automatic"additionally needs acustomerProfileId(a stored payment method).POST /v1/productsthenPOST /v1/prices— the catalog lines you quote from.
1. Create the quote
curl -X POST https://api.vortex.nyc/v1/quotes \
-H "Authorization: Bearer $VORTEX_API_KEY" \
-H "Idempotency-Key: quote_$(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"orderType": "subscription_creation",
"customerId": "bcust_...",
"billingAccountId": "bacc_...",
"currency": "USD",
"idempotencyKey": "quote_01J...",
"lines": [
{ "priceId": "price_...", "quantity": 1, "unitAmount": 4900 }
],
"terms": "Net 15. Work begins on execution."
}'
| Field | Notes |
|---|---|
orderType |
one_off → finalized invoice on execute · subscription_creation → subscription(s) · subscription_amendment → change an existing subscription (also pass subscriptionId) |
lines[] |
priceId required; quantity, unitAmount, description override catalog defaults |
idempotencyKey |
Required in the body (domain-level), in addition to the Idempotency-Key header |
The response carries currentVersion.quoteVersionId — the id every next call
uses.
2. Iterate while it’s draft
PATCH /v1/quote-versions/{quoteVersionId} accepts lines, couponIds,
terms, currency, and metadata — anything negotiable stays editable until
approval. To start a clean round instead of patching, create a new version:
POST /v1/quotes/{quoteId}/versions. Void a version that lost the deal with
POST /v1/quote-versions/{quoteVersionId}/void.
3. Approve → order form
curl -X POST https://api.vortex.nyc/v1/quote-versions/qv_.../approve \
-H "Authorization: Bearer $VORTEX_API_KEY" \
-H "Idempotency-Key: approve_$(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "approvedBy": "operator", "approvedByRef": "you@yourapp.example" }'
Approval is irreversible — the version locks and the response contains
orderForm. Optionally pass expiresAt to bound how long the form stays
signable; the POST /v1/order-forms/expire-due sweep expires stale forms.
If you don’t run your own signing UI, POST /v1/order-forms/{orderFormId}/hosted-link
returns a hosted signing page you can send the customer directly.
4. Register the external signature request
Before (or when) you send the order form out, register the envelope/session your signer created:
curl -X POST https://api.vortex.nyc/v1/order-forms/of_.../signature-request \
-H "Authorization: Bearer $VORTEX_API_KEY" \
-H "Idempotency-Key: sigreq_$(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "signatureRequestRef": "env_9f2a..." }'
signatureRequestRef is your pointer — the envelope id, session id, or
delivery id your signing system uses. Vortex stores it on the order form and
uses it to correlate the completion.
5. Record the signature
When the signer completes, tell Vortex. This is the step that creates the order:
curl -X POST https://api.vortex.nyc/v1/order-forms/of_.../sign \
-H "Authorization: Bearer $VORTEX_API_KEY" \
-H "Idempotency-Key: sign_$(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"signatureRequestRef": "env_9f2a...",
"signedDocumentRef": "sdoc_7b11...",
"executionMode": "execute_in_vortex",
"customFieldData": { "company-size": "large" }
}'
| Field | Semantics |
|---|---|
signatureRequestRef |
Your envelope/session id. If one was registered it must be presented and must match — a completion without it is rejected, and a mismatched ref is treated as a foreign completion, not a retry. |
signedDocumentRef |
Pointer to the executed artifact (signed PDF id, certificate id). Stored on the order form for audit; must match any previously recorded value. |
executionMode |
execute_in_vortex — order converts to invoice/subscription on execute · order_only — record the order, produce no billing artifacts. An order signed without it cannot execute (400). |
executeAt |
ISO-8601 future timestamp — schedule execution instead of calling /execute yourself; POST /v1/orders/execute-due picks it up. |
customFieldData |
Answers to product-level custom fields on the quoted lines; required fields are enforced here — a missing answer is a 400. |
signedBy/signedByRef |
Actor metadata. On the public API signedBy accepts system or operator; signedByRef defaults to your API caller. |
/sign is replay-safe: the form transition and the order insert commit in
one transaction. Retrying with the same refs returns the existing order; a
ref the form never saw is rejected — you cannot sign a form with someone
else’s envelope.
6. Execute the order
curl -X POST https://api.vortex.nyc/v1/orders/ordr_.../execute \
-H "Authorization: Bearer $VORTEX_API_KEY" \
-H "Idempotency-Key: exec_$(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "idempotencyKey": "exec_01J..." }'
What execute produces depends on the quote’s orderType:
one_off→ a finalized invoice, ready to collectsubscription_creation→ subscriptions; the response’sexecutionRecord.subscriptionIdslists them (see them underGET /v1/customers/{customerId}/subscriptions)subscription_amendment→ the change applied to the target subscription
Orders created with order_only skip this step — they stay a durable record
of the signature without billing artifacts. Execution problems are terminal
on the order (status: "failed") and visible in executionRecord.errors.
Collecting in person
An executed one_off invoice can settle on a card-present reader instead of
a card-on-file charge. Create the intent with invoiceId on
POST /v1/card-present-payment-intents; the amount must equal the invoice’s
amountRemaining and the invoice must be open, past_due, or
partially_paid. When the collection captures — inline at create or via
POST /v1/card-present-payment-intents/{id}/collection-outcome — the invoice
moves to paid with the attempt, allocation, and payment receipt written, and
invoice.paid is emitted (activating the subscription the invoice opened, for
first-invoice-paid flows). A replayed outcome re-attempts the settle without
double-paying.
Status reference
| Quote version | Order form | Order |
|---|---|---|
draft |
generated |
created |
approved |
signed |
executed |
voided |
expired |
failed |
voided |
Sandbox → production checklist
- Same code path on
vp_test_first — the key’s environment is binding; asandboxbody against a live key is rejected. -
signatureRequestRefround-trips: registered on the form, presented at/sign, mismatch rejected. Prove a replay returns the same order. - Expiry behaves: set
expiresAt, runPOST /v1/order-forms/expire-due, confirmexpiredforms can’t be signed. -
customFieldDataanswers validate against product fields before the order exists. - Executed subscription appears under the customer and carries the quoted prices — not a re-derivation.
- Webhooks: subscribe to
subscription.created,invoice.finalized, and order/subscription lifecycle events so your signer flow can drive off callbacks rather than polling. - In production, switch
vp_test_→vp_live_; nothing else changes.