Skip to content

Set this tenant's purchased receptionist connections

PUT
/v1/subscription/receptionist-connections
curl --request PUT \
--url https://api.vocapable.com/v1/subscription/receptionist-connections \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "receptionist_connections": 1 }'

Converge the receptionist connection count at Stripe and locally (ADR-0025).

An added connection is paid for before it is reserved, and it is paid for a full month (the owner’s decision of 2026-08-18). A receptionist connection is not a line on a bill - it is a GPU slot withheld from every other claim on this platform for as long as the tenant holds it, so granting one on the strength of an invoice that renders up to a month later hands out physical capacity on credit. So an INCREASE collects 250·Δbase + 150·Δadditional immediately (_receptionist_month_due_minor), and only a collected charge reaches set_receptionist_connections and the capacity refresh below. The recurring items are moved with proration_behavior="none" because Stripe would otherwise ALSO invoice a prorated stub for the same days at the next renewal - the customer would pay the remainder of this month twice - and the recurring price then simply starts at the next renewal.

A DECREASE is gated on no payment and on no billing state a customer can be trapped in: giving capacity back costs this platform nothing to accept, and refusing it would be refusing to stop charging someone. That last clause used to be a claim the code did not keep - the subscription_inactive guard ran before the direction was known and refused BOTH directions, so a past_due tenant could not lower the bill it was already behind on, which is the one tenant most likely to be asking. The guard is now split by direction: only an INCREASE is held to _RECEPTIONIST_PURCHASE_STATUSES, while a decrease is accepted wherever the Stripe subscription still has items to change (_RECEPTIONIST_RELEASE_STATUSES). It refunds nothing either - the month was prepaid, which is the honest consequence of the decision above and which the portal states before the customer confirms rather than this endpoint discovering for them.

The extra-sessions endpoint’s discipline over TWO items instead of one. Absolute semantics are what make it safely retryable without a durable provisioning operation: an ambiguous outcome cannot double a fee, because setting N twice is setting N. A definite Stripe success updates the local row; an ambiguous outcome leaves it untouched and answers 502 receptionist_connections_update_unconfirmed, with the signed customer.subscription.updated webhook as the reconciler.

The charge is under that same definite-versus-ambiguous split and carries codes of its own, because “your card said no” and “the item change failed” send the customer to different places: 402 receptionist_connections_charge_declined is a clean refusal that changed nothing at all, and 502 receptionist_connections_charge_unconfirmed says the money may have moved while no subscription item did - the local count is untouched, no webhook is coming for it, and the resolution is re-sending the same absolute target, which replays the same charge instead of collecting a second month (_receptionist_charge_idempotency_key).

A third, 402 receptionist_connections_charge_authentication_required, makes the same statement about the money as the decline and the opposite statement about the cure: the card works and the issuer wants the customer present to authenticate, so the invoice the attempt left open is where the payment is completed and the card must NOT be replaced. It is a separate code because the code is the only thing a client can branch on, and while it shared the decline’s every portal pinned to that code told the customer to change a payment method that was never refused.

Both 402s name the invoice their attempt left behind rather than pretending it is not there. A failed pay leaves a FINALIZED, OPEN invoice that the tenant’s own GET /v1/invoices mirror shows, voiding it would break the “send the same number again” cure that replays onto it, and a refusal that told a customer to ignore an invoice they can see is how a support ticket becomes a chargeback.

That promise needs one thing absolute semantics cannot supply on their own, and it is the same thing in every part of the request: what Stripe actually holds. Every ambiguous outcome here leaves the local row behind on purpose, so the row lags a change that landed - in its item ids, which would make a retried 0 -> N first purchase ADD a second item and invoice $250 or $150 twice, AND in its count, which would make the next genuinely different target collect a month for connections Stripe already bills. So the request reads the live item set once (_held_licensed_items) and that one read decides all four of whether there is anything to do, the mutation, the amount, and the charge’s idempotency key.

Whether there is anything to do is the one that is not obvious, and it is not a question the local row may answer. A target equal to the count the row shows looks like a request with nothing to do - and that is precisely the shape of the customer’s natural “give the connection back” after an ambiguous 1 -> 2 landed at Stripe and not here: it answered 200, sent Stripe nothing, and left the $150 item billing for as long as the subscription lived. The undocumented cure was to set a HIGHER number first and then the lower one, which no customer discovers. So a request that would change Stripe’s items now makes that change whatever the local row says, and only a subscription Stripe already holds at the target quantities is answered without a mutation. The money rules are unmoved by it: a decrease still collects nothing, and an increase is still priced from what Stripe holds, so converging a row onto items the customer already paid for is free.

Two items rather than one because the first connection is $250 and the rest are $150 each, and which of them a subscription holds depends on its plan: a standalone receptionist plan already includes the first, an outbound plan attaching a receptionist buys it. rate_card.receptionist_line_items owns that split; this route only converges whatever it returns, and it moves both items in ONE Stripe request so no invoice can show the additional connections billed while the first one is not.

A comped or internal tenant skips the prepay entirely (_PREPAY_EXEMPT_COMMERCIAL_STATUSES): the ADR-0026 d3 hardening removed their default payment method on purpose, so collecting would 402 on every increase forever. The items move, the local count is written, no invoice is raised and no card exists to charge - the recurring price simply lands on the next send_invoice invoice at full value, where the credit note offsets it.

Media typeapplication/json
ReceptionistConnectionsRequest

The absolute purchased quantity - never a delta, so retries converge on N.

Counted the way the checkout body counts it: connections bought ON TOP of whatever the plan already includes. A standalone receptionist tenant at 0 still answers one call at a time, because its plan’s $250 base fee carries the first connection.

object
receptionist_connections
required
Receptionist Connections
integer
Examplegenerated
{
"receptionist_connections": 1
}

Successful Response

Media typeapplication/json
Subscription
object
id
required
Id
string
status
required
Status
string
Allowed values: trialing active past_due unpaid canceled
plan
required
PlanSummary
object
id
required
Id
string
name
required
Name
string
base_price_minor
required
Base Price Minor
integer
currency
required
Currency
string
included_quantities
Included Quantities
object
key
additional properties
string
/^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$/
overage_rates
Overage Rates
object
key
additional properties
string
/^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$/
concurrency_limit
required
Concurrency Limit
integer
receptionist_connections_included
Receptionist Connections Included
integer
0
term
Any of:
string
Allowed values: mtm three_year
feature_flags
Feature Flags
object
key
additional properties
any
workload_class
Any of:
string
Allowed values: engage workforce sales receptionist
extra_sessions
Extra Sessions
integer
0
total_sessions
required
Total Sessions
integer
receptionist_connections
Receptionist Connections
integer
0
total_receptionist_connections
required
Total Receptionist Connections
integer
minute_rate_minor
required
Minute Rate Minor
string
/^(?!^[-+.]*$)[+-]?0*\d*\.?\d*$/
current_period_start
required
Current Period Start
string format: date-time
current_period_end
required
Current Period End
string format: date-time
overrides
Overrides
object
key
additional properties
any
spend_cap
Any of:
SpendCap

The cap pacer governor #4 enforces, wherever it is currently stored.

wallet is the canonical P1 store; subscription_override is the P0 store that remains readable until every cap has migrated (docs/11-billing.md §Wallet).

object
amount_minor
required
Amount Minor
integer
period
Any of:
string
source
required
Source
string
Allowed values: wallet subscription_override
created_at
required
Created At
string format: date-time
updated_at
required
Updated At
string format: date-time
Example
{
"status": "trialing",
"plan": {
"receptionist_connections_included": 0,
"term": "mtm"
},
"workload_class": "engage",
"extra_sessions": 0,
"receptionist_connections": 0,
"spend_cap": {
"source": "wallet"
}
}

Validation Error

Media typeapplication/json
HTTPValidationError
object
detail
Detail
Array<object>
ValidationError
object
loc
required
Location
Array
msg
required
Message
string
type
required
Error Type
string
input
Input
ctx
Context
object
Examplegenerated
{
"detail": [
{
"loc": [
"example"
],
"msg": "example",
"type": "example",
"input": "example",
"ctx": {}
}
]
}