Skip to content
Payments docs are in beta — the OpenAPI contract is the source of truth.API reference
VortexPayments
Esc
↑↓navigate↵open⌘Jpreview
On this page

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

  1. Create a quote — POST /v1/quotes with an orderType, a billing account, and catalog price lines. This creates draft version 1.
  2. Negotiate — PATCH /v1/quote-versions/{quoteVersionId} edits lines, terms, and discounts while the version stays draft. Or POST /v1/quotes/{quoteId}/versions for a fresh draft.
  3. Approve — POST /v1/quote-versions/{quoteVersionId}/approve locks the version and generates the order form.
  4. Send for signature — hand the order form to your signer. Register the envelope with POST /v1/order-forms/{orderFormId}/signature-request so completions can be correlated.
  5. Record the signature — POST /v1/order-forms/{orderFormId}/sign with your signer’s refs. This creates the order in one transaction with the form transition — a signed form always has its order.
  6. Execute — POST /v1/orders/{orderId}/execute converts 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; name and defaultCurrency are required on create.
  • POST /v1/customers/{customerId}/billing-accounts — requires invoiceDeliveryMode (email, manual, api_only), collectionMode (manual or automatic), and autoCollectionEnabled. For document-executed billing, collectionMode: "manual" + invoiceDeliveryMode: "api_only" is the common choice — you render and deliver documents yourself. collectionMode: "automatic" additionally needs a customerProfileId (a stored payment method).
  • POST /v1/products then POST /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 collect
  • subscription_creation → subscriptions; the response’s executionRecord.subscriptionIds lists them (see them under GET /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; a sandbox body against a live key is rejected.
  • signatureRequestRef round-trips: registered on the form, presented at /sign, mismatch rejected. Prove a replay returns the same order.
  • Expiry behaves: set expiresAt, run POST /v1/order-forms/expire-due, confirm expired forms can’t be signed.
  • customFieldData answers 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.

Where next

Last updated on September 28, 2026

Was this page helpful?