Invoicing and payments
The customer's own quote-to-cash: invoices, public invoice pages and PDFs, Stripe checkout, manual payments, subscriptions and the MRR rollup. Not the workspace's own SalesShift bill — that is Plans and your subscription.
22 endpoints· 5 called live· 4 not exercised here· verified against http://127.0.0.1:8741
22 endpoints
GET/api/v1/salesshift/invoicesCalled liveInvoices, optionally filtered by status.
Query parameters
| Name | Type | Required | Notes |
|---|---|---|---|
| status_filter | string? | optional | |
| limit | integer | optional | Default 100. |
Request
curl 'https://api.vxcloud.io/api/v1/salesshift/invoices' \
-H "Authorization: Bearer $SS_TOKEN"Response · 200
{
"success": "bool",
"data": [
{
"id": "str",
"number": "str",
"status": "str",
"title": "str",
"quote_id": "null",
"deal_id": "null",
"contact_id": "null",
"company_id": "null",
"subscription_id": "null",
"currency": "str",
"subtotal_cents": "int",
"discount_cents": "int",
"tax_cents": "int",
"total_cents": "int",
"amount_paid_cents": "int",
"amount_due_cents": "int",
"total_display": "str",
"amount_due_display": "str",
"reporting_total_cents": "int",
"fx_rate": "float",
"fx_as_of": "null",
"order_discount_kind": "str",
"order_discount_value": "float",
"payment_terms": "str",
"payment_terms_label": "str",
"po_number": "str",
"terms": "null",
"notes": "null",
"footer_note": "str",
"bill_to_name": "str",
"bill_to_email": "str",
"bill_to_address": "str",
"bill_to_tax_id": "null",
"due_date": "str",
"issued_at": "str",
"sent_at": "null",
"paid_at": "null",
"voided_at": "null",
"last_pdf_at": "null",
"pdf_sha256": "null"
},
"...x6"
]
}POST/api/v1/salesshift/invoicesVerified in sessionCreate a draft invoice with no quote behind it.
Create a draft invoice with no quote behind it. The number is allocated now rather than at issue time, matching quotes. It burns a number on an abandoned draft, which is the deliberate trade: the counter is gapless because finance reads a gap as a missing document, and a voided draft is a better answer to "where did INV-00042 go" than silence.
Request body (required) — InvoiceIn
| Field | Type | Required | Notes |
|---|---|---|---|
| title | string? | optional | |
| deal_id | string(uuid)? | optional | |
| contact_id | string(uuid)? | optional | |
| company_id | string(uuid)? | optional | |
| currency | string | optional | Default "USD". |
| payment_terms | string | optional | Default "net_14". |
| due_date | string(date)? | optional | |
| po_number | string? | optional | |
| terms | string? | optional | |
| notes | string? | optional | |
| footer_note | string? | optional | |
| bill_to_name | string? | optional | |
| bill_to_email | string? | optional | |
| bill_to_address | string? | optional | |
| bill_to_tax_id | string? | optional | |
| order_discount_kind | string | optional | Default "none". |
| order_discount_value | number | optional | Default 0. |
| lines | LineIn[] | optional |
Values in the request below are typed placeholders, not sample data.
Request
curl -X POST 'https://api.vxcloud.io/api/v1/salesshift/invoices' \
-H "Authorization: Bearer $SS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "title": "<string>", "deal_id": "<string(uuid)>", "contact_id": "<string(uuid)>", "company_id": "<string(uuid)>", "currency": "USD", "payment_terms": "net_14", "due_date": "<string(date)>", "po_number": "<string>", "terms": "<string>", "notes": "<string>", "footer_note": "<string>", "bill_to_name": "<string>", "bill_to_email": "<string>", "bill_to_address": "<string>", "bill_to_tax_id": "<string>", "order_discount_kind": "none", "order_discount_value": 0, "lines": [ "<LineIn>" ] }'Response · 201
{
"data": "…",
"success": "…"
}Errors
| Status | detail |
|---|---|
| 400 | payment_terms must be one of {...} |
| 400 | Max {...} line items per invoice |
| 400 | computed at runtime: err |
| 400 | Product {...} not found |
GET/api/v1/salesshift/invoices/{invoice_id}Called liveOne invoice with its line items.
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
| invoice_id | string(uuid) | required |
Request
curl 'https://api.vxcloud.io/api/v1/salesshift/invoices/{invoice_id}' \
-H "Authorization: Bearer $SS_TOKEN"Response · 200
{
"success": "bool",
"data": {
"id": "str",
"number": "str",
"status": "str",
"title": "str",
"quote_id": "null",
"deal_id": "null",
"contact_id": "null",
"company_id": "null",
"subscription_id": "null",
"currency": "str",
"subtotal_cents": "int",
"discount_cents": "int",
"tax_cents": "int",
"total_cents": "int",
"amount_paid_cents": "int",
"amount_due_cents": "int",
"total_display": "str",
"amount_due_display": "str",
"reporting_total_cents": "int",
"fx_rate": "float",
"fx_as_of": "null",
"order_discount_kind": "str",
"order_discount_value": "float",
"payment_terms": "str",
"payment_terms_label": "str",
"po_number": "str",
"terms": "null",
"notes": "null",
"footer_note": "str",
"bill_to_name": "str",
"bill_to_email": "str",
"bill_to_address": "str",
"bill_to_tax_id": "null",
"due_date": "str",
"issued_at": "str",
"sent_at": "null",
"paid_at": "null",
"voided_at": "null",
"last_pdf_at": "null",
"pdf_sha256": "null"
}
}Errors
| Status | detail |
|---|---|
| 404 | Invoice not found |
PATCH/api/v1/salesshift/invoices/{invoice_id}Verified in sessionEdit the header.
Edit the header. Money is always recomputed server-side afterwards — a currency or header-discount change re-prices every line.
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
| invoice_id | string(uuid) | required |
Request body (required) — InvoicePatch
| Field | Type | Required | Notes |
|---|---|---|---|
| title | string? | optional | |
| deal_id | string(uuid)? | optional | |
| contact_id | string(uuid)? | optional | |
| company_id | string(uuid)? | optional | |
| currency | string? | optional | |
| payment_terms | string? | optional | |
| due_date | string(date)? | optional | |
| po_number | string? | optional | |
| terms | string? | optional | |
| notes | string? | optional | |
| footer_note | string? | optional | |
| bill_to_name | string? | optional | |
| bill_to_email | string? | optional | |
| bill_to_address | string? | optional | |
| bill_to_tax_id | string? | optional | |
| order_discount_kind | string? | optional | |
| order_discount_value | number? | optional |
Values in the request below are typed placeholders, not sample data.
Request
curl -X PATCH 'https://api.vxcloud.io/api/v1/salesshift/invoices/{invoice_id}' \
-H "Authorization: Bearer $SS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "title": "<string>", "deal_id": "<string(uuid)>", "contact_id": "<string(uuid)>", "company_id": "<string(uuid)>", "currency": "<string>", "payment_terms": "<string>", "due_date": "<string(date)>", "po_number": "<string>", "terms": "<string>", "notes": "<string>", "footer_note": "<string>", "bill_to_name": "<string>", "bill_to_email": "<string>", "bill_to_address": "<string>", "bill_to_tax_id": "<string>", "order_discount_kind": "<string>", "order_discount_value": 0 }'Response · 200
{
"data": "…",
"success": "…"
}Errors
| Status | detail |
|---|---|
| 400 | payment_terms must be one of {...} |
POST/api/v1/salesshift/invoices/{invoice_id}/checkoutVerified in sessionCreate a Stripe Checkout Session for the amount still outstanding.
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
| invoice_id | string(uuid) | required |
Request body (optional)
Untyped Dict[str, Any] body — there is no Pydantic model, so these are the field names the handler actually reads.
return_base
Values in the request below are typed placeholders, not sample data.
Request
curl -X POST 'https://api.vxcloud.io/api/v1/salesshift/invoices/{invoice_id}/checkout' \
-H "Authorization: Bearer $SS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "return_base": "<value>" }'Response · 200
{
"amount_cents": "…",
"checkout_url": "…",
"session_id": "…",
"success": "…"
}Errors
| Status | detail |
|---|---|
| 404 | Invoice not found |
| 400 | Invoice is already {...} |
| 400 | Nothing left to pay |
| 400 | Connect Stripe under Settings → Integrations first |
| 502 | Stripe rejected the request: {...} |
POST/api/v1/salesshift/invoices/{invoice_id}/issueVerified in sessionDraft → open: stamp the issue date, derive the due date from the payment term and freeze the bill-to snapshot.
Draft → open: stamp the issue date, derive the due date from the payment term and freeze the bill-to snapshot.
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
| invoice_id | string(uuid) | required |
Request body (optional) — InvoiceIssueIn
| Field | Type | Required | Notes |
|---|---|---|---|
| due_date | string(date)? | optional |
Values in the request below are typed placeholders, not sample data.
Request
curl -X POST 'https://api.vxcloud.io/api/v1/salesshift/invoices/{invoice_id}/issue' \
-H "Authorization: Bearer $SS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "due_date": "<string(date)>" }'Response · 200
{
"data": "…",
"success": "…"
}Errors
| Status | detail |
|---|---|
| 409 | Invoice {...} is already {...} |
PUT/api/v1/salesshift/invoices/{invoice_id}/linesVerified in sessionBulk replace, same contract as the quote editor: the client sends the whole grid and the server re-prices it, so a failed call can never leave the document half-updated.
Bulk replace, same contract as the quote editor: the client sends the whole grid and the server re-prices it, so a failed call can never leave the document half-updated.
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
| invoice_id | string(uuid) | required |
Request body (required)
Untyped Dict[str, Any] body — there is no Pydantic model, so these are the field names the handler actually reads.
lines
Values in the request below are typed placeholders, not sample data.
Request
curl -X PUT 'https://api.vxcloud.io/api/v1/salesshift/invoices/{invoice_id}/lines' \
-H "Authorization: Bearer $SS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "lines": "<value>" }'Response · 200
{
"data": "…",
"success": "…"
}Errors
| Status | detail |
|---|---|
| 400 | lines must be a list |
| 400 | Max {...} line items per invoice |
| 400 | computed at runtime: err |
GET/api/v1/salesshift/invoices/{invoice_id}/pdfVerified in sessionRender and return the invoice PDF inline.
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
| invoice_id | string(uuid) | required |
Request
curl 'https://api.vxcloud.io/api/v1/salesshift/invoices/{invoice_id}/pdf' \
-H "Authorization: Bearer $SS_TOKEN"Response · 200
Response carrying the invoice PDF inline (application/pdf).
POST/api/v1/salesshift/invoices/{invoice_id}/record-paymentVerified in sessionBank transfer / cheque / cash.
Bank transfer / cheque / cash. Same ledger, provider='manual'.
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
| invoice_id | string(uuid) | required |
Request body (required)
Untyped Dict[str, Any] body — there is no Pydantic model, so these are the field names the handler actually reads.
amount_centsproviderreference
Values in the request below are typed placeholders, not sample data.
Request
curl -X POST 'https://api.vxcloud.io/api/v1/salesshift/invoices/{invoice_id}/record-payment' \
-H "Authorization: Bearer $SS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "amount_cents": "<value>", "provider": "<value>", "reference": "<value>" }'Response · 200
{
"data": "…",
"success": "…"
}Errors
| Status | detail |
|---|---|
| 404 | Invoice not found |
| 400 | amount_cents must be positive |
POST/api/v1/salesshift/invoices/{invoice_id}/sendVerified in sessionEmail the invoice with the PDF attached, from the tenant's own mailbox.
Email the invoice with the PDF attached, from the tenant's own mailbox. `outbound.send_mail` rather than the campaign sender: an invoice must not be blocked by a marketing opt-out, must not carry an unsubscribe footer, and — the reason the others cannot be used at all — must carry an attachment.
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
| invoice_id | string(uuid) | required |
Request body (optional) — InvoiceSendIn
| Field | Type | Required | Notes |
|---|---|---|---|
| to_email | string? | optional | |
| cc | string[] | optional | |
| subject | string? | optional | |
| message | string? | optional | |
| account_id | string(uuid)? | optional |
Values in the request below are typed placeholders, not sample data.
Request
curl -X POST 'https://api.vxcloud.io/api/v1/salesshift/invoices/{invoice_id}/send' \
-H "Authorization: Bearer $SS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "to_email": "<string>", "cc": [ "<string>" ], "subject": "<string>", "message": "<string>", "account_id": "<string(uuid)>" }'Response · 200
{
"attachment": "…",
"cc": "…",
"data": "…",
"from_email": "…",
"message_id": "…",
"sent_to": "…",
"success": "…"
}Errors
| Status | detail |
|---|---|
| 409 | A void invoice cannot be sent |
| 400 | A recipient email is required to send an invoice |
| 502 | computed at runtime: result.error |
POST/api/v1/salesshift/invoices/{invoice_id}/voidVerified in sessionCancel an invoice that was never paid.
Cancel an invoice that was never paid. An invoice with money against it is NOT voidable — voiding would erase the obligation the payment was made against and leave the ledger holding cash for a document that no longer exists. That correction is a refund plus a credit note.
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
| invoice_id | string(uuid) | required |
Request body (optional)
Accepts a JSON object; the source declares no field list.
Request
curl -X POST 'https://api.vxcloud.io/api/v1/salesshift/invoices/{invoice_id}/void' \
-H "Authorization: Bearer $SS_TOKEN"Response · 200
{
"data": "…",
"success": "…"
}Errors
| Status | detail |
|---|---|
| 409 | {...} has been paid against this invoice — refund it and raise a credit note instead of voiding |
POST/api/v1/salesshift/invoices/from-quote/{quote_id}Verified in sessionCreate an invoice from an accepted quote.
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
| quote_id | string(uuid) | required |
Request
curl -X POST 'https://api.vxcloud.io/api/v1/salesshift/invoices/from-quote/{quote_id}' \
-H "Authorization: Bearer $SS_TOKEN"Response · 201
{
"data": "…",
"success": "…"
}Errors
| Status | detail |
|---|---|
| 404 | Quote not found |
| 400 | Only an accepted quote can be invoiced |
GET/api/v1/salesshift/invoices/public/{token}url-secret-onlyVerified in sessionPublic invoice view addressed by its share token.
The invoice's public_token is the credential.
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
| token | string | required |
Request
curl 'https://api.vxcloud.io/api/v1/salesshift/invoices/public/{token}'Response · 200
{
"data": "…",
"success": "…"
}Errors
| Status | detail |
|---|---|
| 404 | Invoice not found |
GET/api/v1/salesshift/invoices/public/{token}/pdfurl-secret-onlyVerified in sessionThe customer's own copy.
The customer's own copy. Unauthenticated on purpose: the token IS the credential, exactly as it is for GET /invoices/public/{token}.
The invoice's public_token is the credential.
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
| token | string | required |
Request
curl 'https://api.vxcloud.io/api/v1/salesshift/invoices/public/{token}/pdf'Response · 200
Response carrying the invoice PDF (application/pdf).
Errors
| Status | detail |
|---|---|
| 404 | Invoice not found |
GET/api/v1/salesshift/subscriptionsCalled liveThe organization's subscriptions, optionally filtered by status.
Query parameters
| Name | Type | Required | Notes |
|---|---|---|---|
| status | string? | optional |
Request
curl 'https://api.vxcloud.io/api/v1/salesshift/subscriptions' \
-H "Authorization: Bearer $SS_TOKEN"Response · 200
{
"success": "bool",
"data": [
{
"id": "str",
"status": "str",
"currency": "str",
"contact_id": "str",
"company_id": "null",
"quote_id": "str",
"mrr_cents": "int",
"arr_cents": "int",
"mrr_display": "str",
"reporting_mrr_cents": "int",
"billing_interval": "str",
"billing_interval_count": "int",
"current_period_start": "str",
"current_period_end": "str",
"started_at": "str",
"trial_end": "null",
"canceled_at": "null",
"cancel_reason": "null",
"provider": "null",
"provider_ref": "null"
},
"...x2"
]
}POST/api/v1/salesshift/subscriptionsNot exercised hereCreate a recurring subscription for a contact or company.
Request body (required)
Untyped Dict[str, Any] body — there is no Pydantic model, so these are the field names the handler actually reads.
billing_intervalbilling_interval_countcompany_idcontact_idcurrencyitemsnotestatus
Values in the request below are typed placeholders, not sample data.
Request
curl -X POST 'https://api.vxcloud.io/api/v1/salesshift/subscriptions' \
-H "Authorization: Bearer $SS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "billing_interval": "<value>", "billing_interval_count": "<value>", "company_id": "<value>", "contact_id": "<value>", "currency": "<value>", "items": "<value>", "note": "<value>", "status": "<value>" }'Response · 201
{
"data": "…",
"success": "…"
}GET/api/v1/salesshift/subscriptions/{subscription_id}Called liveOne subscription by id.
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
| subscription_id | string(uuid) | required |
Request
curl 'https://api.vxcloud.io/api/v1/salesshift/subscriptions/{subscription_id}' \
-H "Authorization: Bearer $SS_TOKEN"Response · 200
{
"success": "bool",
"data": {
"id": "str",
"status": "str",
"currency": "str",
"contact_id": "str",
"company_id": "null",
"quote_id": "str",
"mrr_cents": "int",
"arr_cents": "int",
"mrr_display": "str",
"reporting_mrr_cents": "int",
"billing_interval": "str",
"billing_interval_count": "int",
"current_period_start": "str",
"current_period_end": "str",
"started_at": "str",
"trial_end": "null",
"canceled_at": "null",
"cancel_reason": "null",
"provider": "null",
"provider_ref": "null",
"items": [
{
"id": "...",
"name": "...",
"quantity": "...",
"unit_price_cents": "...",
"product_id": "..."
},
"...x1"
],
"mrr_movements": [
{
"id": "...",
"occurred_on": "...",
"kind": "...",
"delta_cents": "...",
"delta_display": "...",
"note": "..."
},
"...x5"
]
}
}Errors
| Status | detail |
|---|---|
| 404 | Subscription not found |
PATCH/api/v1/salesshift/subscriptions/{subscription_id}Not exercised hereChange the plan value.
Change the plan value. The MRR delta is booked as expansion/contraction automatically from the direction of the change.
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
| subscription_id | string(uuid) | required |
Request body (required)
Untyped Dict[str, Any] body — there is no Pydantic model, so these are the field names the handler actually reads.
mrr_centsnote
Values in the request below are typed placeholders, not sample data.
Request
curl -X PATCH 'https://api.vxcloud.io/api/v1/salesshift/subscriptions/{subscription_id}' \
-H "Authorization: Bearer $SS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "mrr_cents": "<value>", "note": "<value>" }'Response · 200
{
"data": "…",
"success": "…"
}Errors
| Status | detail |
|---|---|
| 404 | Subscription not found |
| 400 | MRR cannot be negative |
POST/api/v1/salesshift/subscriptions/{subscription_id}/cancelNot exercised hereCancel a subscription, immediately or at period end.
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
| subscription_id | string(uuid) | required |
Request body (optional)
Untyped Dict[str, Any] body — there is no Pydantic model, so these are the field names the handler actually reads.
reason
Values in the request below are typed placeholders, not sample data.
Request
curl -X POST 'https://api.vxcloud.io/api/v1/salesshift/subscriptions/{subscription_id}/cancel' \
-H "Authorization: Bearer $SS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "reason": "<value>" }'Response · 200
{
"already_canceled": "…",
"data": "…",
"success": "…"
}Errors
| Status | detail |
|---|---|
| 404 | Subscription not found |
POST/api/v1/salesshift/subscriptions/{subscription_id}/reactivateNot exercised hereReactivate a cancelled subscription.
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
| subscription_id | string(uuid) | required |
Request body (required)
Untyped Dict[str, Any] body — there is no Pydantic model, so these are the field names the handler actually reads.
mrr_centsnote
Values in the request below are typed placeholders, not sample data.
Request
curl -X POST 'https://api.vxcloud.io/api/v1/salesshift/subscriptions/{subscription_id}/reactivate' \
-H "Authorization: Bearer $SS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "mrr_cents": "<value>", "note": "<value>" }'Response · 200
{
"data": "…",
"success": "…"
}Errors
| Status | detail |
|---|---|
| 404 | Subscription not found |
| 400 | Reactivating needs a positive mrr_cents |
GET/api/v1/salesshift/subscriptions/mrrCalled liveThe MRR waterfall — what the ledger exists for.
Query parameters
| Name | Type | Required | Notes |
|---|---|---|---|
| months | integer | optional | Default 12. max 60. |
Request
curl 'https://api.vxcloud.io/api/v1/salesshift/subscriptions/mrr' \
-H "Authorization: Bearer $SS_TOKEN"Response · 200
{
"success": "bool",
"data": {
"current_mrr_cents": "int",
"current_arr_cents": "int",
"ledger_total_cents": "int",
"ledger_agrees_with_cache": "bool",
"waterfall": [
{
"month": "...",
"new": "...",
"contraction": "...",
"expansion": "...",
"reactivation": "...",
"churn": "...",
"closing_mrr_cents": "..."
},
"...x1"
]
}
}POST/api/v1/salesshift/webhooks/stripe/{integration_id}signature-verifiedVerified in sessionStripe webhook receiver for one payment integration.
The Stripe-Signature header is verified against the endpoint secret stored on the integration. With no secret stored the route refuses (500) rather than trusting the body; a bad signature is 400 'Invalid signature'.
Path parameters
| Name | Type | Required | Notes |
|---|---|---|---|
| integration_id | string(uuid) | required |
Request
curl -X POST 'https://api.vxcloud.io/api/v1/salesshift/webhooks/stripe/{integration_id}'Response · 200
{
"duplicate": "…",
"handled": "…",
"received": "…"
}Errors
| Status | detail |
|---|---|
| 404 | Unknown payment integration |
| 503 | Webhook secret not configured for this integration |
| 400 | Invalid signature |