Set this tenant's purchased receptionist connections
const url = 'https://api.vocapable.com/v1/subscription/receptionist-connections';const options = { method: 'PUT', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"receptionist_connections":1}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}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.
Authorizations
Section titled “Authorizations”Request Bodyrequired
Section titled “Request Bodyrequired”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
Examplegenerated
{ "receptionist_connections": 1}Responses
Section titled “Responses”Successful Response
object
object
object
object
object
object
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).
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
object
object
object
Examplegenerated
{ "detail": [ { "loc": [ "example" ], "msg": "example", "type": "example", "input": "example", "ctx": {} } ]}