Measure voice latency without adding overlapping clocks
Separate admission, carrier setup, conversation response and workspace tools while accounting for warm connections and missing data.
Handle asynchronous closure, terminal events and reconciliation without releasing capacity before the provider confirms termination.
The hangup control asks the provider to end a call. Its successful submission is not the same observation as a confirmed terminal call state. Lupin reports ending as requested until a provider callback or status read confirms closure. Present that distinction to the user with a short ending requested message and then refresh the saved call. Avoid immediately telling the caller that the line is disconnected, or freeing the local active-call slot based only on the HTTP response. A transport delay can leave the carrier leg alive after the request has returned.
Use the returned Lupin call ID to inspect GET /v0/calls/:call_id. Provider call-control, leg and session identifiers serve different purposes and should remain private correlation data. A transferred call can have more than one related leg, so a single event does not always describe the whole conversation. Read the event’s role and the recorded lifecycle rather than matching a phone number alone. Preserve the operation key used for the hangup if retrying the same control request, and do not create a fresh call to test whether the original has ended.
curl --fail-with-body "https://api.lupin.sh/v0/calls/$CALL_ID" \
-H "Authorization: Bearer $LUPIN_API_KEY"Telnyx documents unordered, concurrent and duplicate webhook delivery. A delayed answered event must not reopen a call already confirmed as ended. Treat event identity and terminal-state rules as part of the lifecycle design. If a callback never arrives, an authoritative status read can help reconcile the provider outcome. A missing transcript is a different issue: post-call transcription may arrive later and should not be required to stop the phone leg. Keep media cleanup, call closure and transcript persistence independently observable so one delayed branch does not conceal another.
A call_not_active response means the control cannot be applied to the current state. Refresh the call and report what the authoritative record says. Do not respond by redialing or silently switching to another call with the same destination. For reconciliation_required, retain the operation ID and known provider reference and ask the operator to inspect the unfinished effect. Changing the idempotency key to force another control can make incident history harder to understand. The recovery objective is a confirmed end state for the original call, not a successful response from any newly generated request.
After confirmation, check that media resources stopped and the workspace no longer reports an active conversation. Duration accounting and final cost information may settle later; null cost is unavailable data, not proof of a free call. A synthetic regression should deliver hangup confirmation twice and then a stale answered event, asserting that the resource remains terminal. Add a scenario where the control response is lost but the provider closes successfully. These fixtures test correct reconciliation without placing a real call or relying on an arbitrary waiting period to guess that closure probably happened.
A short application polling timeout should become closure unconfirmed, not call ended. Retain the resource and operator reconciliation path while the provider outcome remains unknown. This avoids telling the user the call is over while releasing the single active-call slot for another conversation that the carrier may overlap.
Reviewed against current checked-in Lupin routes, shared contracts and official provider references. No live calls, OAuth authorizations, app actions or purchases were performed.
Prepared with AI assistance by Lupin editorial. How we research and maintain these guides.