Call-events webhook
Wink shows a rep the customer they are talking to — the customer's order history, their SMS thread, live transcription — the moment the call connects. It learns that a call connected from your telephony platform, which posts a small JSON body to one of six URLs as each call event happens.
This document is the whole contract. It is vendor-neutral: any dialler that can make an HTTP POST can drive it.
1. Endpoints
Base URL: https://api-tjc.trywink.io
POST /call-events/call-started
POST /call-events/call-ended
POST /call-events/hold-started
POST /call-events/hold-ended
POST /call-events/recording-paused
POST /call-events/recording-resumed
One URL per event, so a script that configures one HTTP action per event
never has to build a body that names its own type. Content-Type is
application/json.
The base URL identifies your brand. Post to the host you were given; a
request to any other Wink host is rejected (see 404 below), and a key
issued for one brand does not authenticate on another's host.
2. Authentication
Every request carries an API key in a header:
X-API-Key: wk_live_<40 hex characters>
Wink issues the key. It is shown exactly once at issue time and is stored only as a hash on our side, so we cannot recover it for you — if it is lost, ask for a new one and we revoke the old.
Handling guidance:
- Store it in your platform's secret store or credential/secure-variable facility. Never in a script literal, a source repository, a ticket, or an email thread.
- It is a bearer credential: anyone holding it can post events as your
brand. Treat a leak as an incident and ask us to revoke — revocation takes
effect immediately, and rejected calls are visible to you as
401. - Keys are scoped to one brand and to the call-events feature only. A key cannot read anything back out of Wink.
- Send it over HTTPS only. Do not put it in a URL query string; it belongs in the header, where it stays out of proxy and access logs.
- Ask for separate keys for separate senders (production dialler, test script) so one can be revoked without taking the other down.
3. Request bodies
POST /call-events/call-started
| Field | Required | Notes |
|---|---|---|
agent_email | yes | The rep's single-sign-on email address, as Wink knows it. Case and surrounding whitespace do not matter. |
phone_number | yes | The customer's number. E.164 preferred — see §5. |
customer_id | no | Your own CRM code for the caller, if you have one (e.g. a ShopLC/TJC CustomerCode). Free text, stored as sent — zero-padding and all. |
event_id | no | Your unique id for this event. Supply it: it is what makes a retry safe (§6). |
occurred_at | no | ISO-8601, ideally with a UTC offset (2026-08-14T09:15:22Z). Defaults to the time Wink received the request. |
metadata | no | Any JSON object — skill, campaign, queue, disposition, notes. Stored as sent. |
{
"event_id": "1a2b3c-start",
"agent_email": "rep@example.com",
"phone_number": "+447700900123",
"customer_id": "0012345",
"occurred_at": "2026-08-14T09:15:22Z",
"metadata": { "skill": "jewellery", "campaign": "spring" }
}
POST /call-events/call-ended
Required: agent_email. Optional: customer_id, event_id, phone_number,
occurred_at, metadata (a good place for disposition and wrap-up notes).
{
"event_id": "1a2b3c-end",
"agent_email": "rep@example.com",
"customer_id": "0012345",
"metadata": { "disposition": "sale" }
}
POST /call-events/hold-started and POST /call-events/hold-ended
Required: agent_email. Optional: customer_id, event_id, occurred_at,
metadata.
{
"event_id": "1a2b3c-hold-1",
"agent_email": "rep@example.com",
"customer_id": "0012345"
}
POST /call-events/recording-paused and POST /call-events/recording-resumed
Required: agent_email. Optional: customer_id, event_id, occurred_at,
metadata.
Send recording-paused when your dialler pauses recording (for example,
while taking payment details), and recording-resumed when recording may
continue. These apply to the rep's current call, just like hold events.
{
"event_id": "1a2b3c-recording-pause-1",
"agent_email": "rep@example.com",
"customer_id": "0012345"
}
To resume, post the same shape to /call-events/recording-resumed with a
new event_id, such as 1a2b3c-recording-resume-1. Reuse each event's own id
when retrying it.
Wink relays the change to the rep's open page and, for brands using these webhooks, directly to their connected desktop companion as a backup. A missing or disconnected companion does not change the response. Reloading the page restores the current recording state. Hold does not pause audio capture: send a recording event whenever capture needs to pause or resume.
A new call begins with an unpaused recording baseline. When a call ends, Wink also clears its recording state before the companion stops, so a pause from that call cannot mute the next call. Reloading restores the latest baseline or recording control. A duplicate start for the same live call keeps that call's current recording state.
Rules that hold for every endpoint
phone_numberis only required oncall-started. Wink matches every later event in a call to the rep who is on it (agent_email) and inherits the number from the start event, so there is no need to repeat it (you may, and it is stored if you do).customer_idmust be sent as a JSON string — quote it. A bare number (12345instead of"0012345") is rejected outright (see the422row below), and even where it happens to parse, sending a zero-padded code as a number loses the leading zeros before it ever reaches us. Beyond "it must be a string", it is not validated against any format — it is a value in someone else's system, and a value Wink cannot parse is still a value worth recording. Send it on any event; only the value on the event that opens the call becomes part of the live call, and later events record theirs on the row regardless.occurred_atnever causes a rejection. A missing, empty, or unparseable timestamp falls back to Wink's receive time rather than failing the request — a clock we cannot read is not worth a failed call event.metadatamay be omitted, or sent asnull; both are treated as{}.- Extra fields you send that are not listed here are ignored — including
call_id, if your integration still sends one (§4, "Changes from the first revision") — so you can add to your template without coordinating a release with us. agent_emailmust be a non-empty string, and (oncall-started) so mustphone_number. That is the only validation strict enough to reject a request.
3a. One call at a time
Wink keys a call to the rep, not to an identifier you supply. A rep can
only be on one call, so agent_email names the call unambiguously and there
is no call id for you to mint, store, or repeat.
call-ended,hold-started,hold-ended,recording-pausedandrecording-resumedapply to whatever call that rep is currently on.- If a
call-startedarrives while the rep is already on a different call, Wink ends that call itself and starts the new one. You never have to clean up after a missedcall-ended. - A
call-startednaming the number the rep is already on, with acustomer_idthat does not conflict (equal, or either side absent), is treated as a resend and changes nothing. This is what makes a retried request safe even without anevent_id. Same number with a conflictingcustomer_idis not a resend — it is a genuinely different call (a household line, a new account on the same handset) and displaces the old one exactly as a different number would. - An end, hold or recording event for a rep with no call open is recorded and answered
200with"call_active": false. It is not an error; there is simply nothing to end.
Linked transcription session lifecycle
Chat detail (GET /shoplc/chats/{chat_id}) and call detail/export responses
include these fields on every non-null transcription_session object:
| Field | Meaning |
|---|---|
live | Boolean. true while the shared desktop producer lease is present, including the 120-second orphan reconnect window, and the session has not finalized. Use this field to recover a live companion attachment. |
finalized_at | ISO 8601 timestamp or null. The actual server finalization instant, persisted once and cleared if the session resumes. null also covers legacy sessions whose real end is unknown; it alone does not mean live. |
ended_at | ISO 8601 timestamp or null. When the dialer reported the call's end (Five9, or the call-events webhook), the moment that signal reached Wink, fixed from then on. Otherwise the last persisted transcript activity, or the time the session was torn down; while capture is live and no end has been reported, chunk appends can advance it. It is not a liveness or finalization marker. |
A finalized session has live: false. A missing or expired producer lease also
has live: false, even when finalized_at is null. No linked session remains
transcription_session: null. A Redis read failure returns an API error rather
than inventing live: false.
Producer leases are shared across backend instances. They refresh while the desktop sends frames and expire after 150 seconds without refresh, bounding stale liveness after a backend crash. During orphan grace, recording controls remain eligible for relay; delivery still requires a usable desktop socket.
4. Responses
Every accepted event returns 200 with the same five fields:
{
"accepted": true,
"duplicate": false,
"agent_resolved": true,
"call_active": true,
"delivered_sockets": 1
}
| Field | Meaning |
|---|---|
accepted | Always true on a 200. The event is durably recorded. |
duplicate | true when the event was recognized as a repeat, which happens two ways: this event_id was already recorded, or (call-started only) the event named the call the rep is already on — a resend recognized by number and customer_id alone, with no event_id required (§3a). Either way your retry arrived without adding another log row. delivered_sockets is normally 0 on a duplicate; it can be non-zero when the original attempt never reached the rep, or when a recording retry re-delivers the current recording state to the page and companion. |
agent_resolved | Whether agent_email matched an active Wink user for your brand. |
call_active | Whether this event landed on a live call. false identifies the two cases worth alerting on at your end: an end, hold or recording event for a rep who has no call open, and any event for a rep Wink could not resolve. It is diagnostic only — the response is still 200. |
delivered_sockets | How many of that rep's open Wink browser tabs received the event on the server that handled your request. Wink runs several servers, and a tab connected to a different one still receives the event without being counted here, so treat this as a lower bound — useful for "did anything happen", not as an exact tab count. |
There is no call id in the response. Wink mints and owns one internally, but you never need it — see §3a.
Three 200s worth understanding:
agent_resolved: false— the event was well-formed and is on file, but the email address does not match an active Wink user for your brand. It is deliberately not an error: rejecting it would make your platform retry something that will never succeed. Nothing was shown to any rep. This is the flag to alert on — in practice it means a mis-mapped address, a rep who has not yet completed a Wink login, or a leaver. Send us the email address and we will reconcile it.call_active: false— the event was recorded but there was no live call for it to touch: an end, hold or recording event arrived for a rep who was not on a call, or (whenagent_resolvedis alsofalse) the rep could not be identified at all. Not an error, and not worth retrying — see §3a.delivered_sockets: 0withagent_resolved: trueandcall_active: true— nothing was counted, most often because the rep has no Wink tab open right now. The call state is held for them either way, so if they open Wink mid-call they still see it. Because the count only covers the server that handled your request, a0can also mean the rep's tab is connected elsewhere and did receive the event. Not an error, and not worth retrying.
Error responses:
| Status | Meaning | What to do |
|---|---|---|
401 | Missing, malformed, revoked, or wrong-brand API key. | Do not retry. Check the key and the base URL; ask for a re-issue. |
404 | The host you posted to is not one Wink serves for you. | Do not retry. Fix the base URL. |
422 | The body was not usable: it was not valid JSON; agent_email / (on call-started) phone_number was missing or empty; or a field was sent as the wrong JSON type — most often customer_id sent as a bare number (12345) instead of a quoted string ("0012345"), or metadata sent as something other than an object. | Do not retry unchanged — it will fail identically. Fix the template. |
5xx | Something failed on our side. | Retry — see §6. |
Two notes on 422:
- The key is checked before the body, so a request with a bad key returns
401and tells you nothing about the schema. The one exception is a body that is not syntactically valid JSON: that fails in the HTTP layer before anything else and returns422even when the key is also wrong. The response says only that the JSON could not be parsed and where — never which fields the endpoint expects. - When you get a
422on a request you believe is correct, log the exact bytes you sent. Almost every case is a templating artefact: an unquoted substitution, an empty variable that rendered as"", or a trailing comma.
Changes from the first revision
call_id is no longer required on any endpoint. If you already send it, keep
sending it — the field is ignored and nothing breaks. customer_id is new and
optional: send your own account code for the caller and we will record it
against the call.
5. Phone numbers
Send E.164 — a leading +, country code, no spaces, hyphens, or brackets:
+447700900123 UK mobile
+442071838750 UK landline
If a number arrives in a national format (07700 900123, 020 7183 8750)
Wink converts it using your brand's country, which for a UK brand is the
United Kingdom. That conversion is a convenience, not a contract: a number
we cannot place is stored as you sent it, and a rep may then see no customer
match for a customer we actually know. Numbers that reach you already in
E.164 should be passed through untouched — do not strip the +.
Withheld or unavailable caller ID: send the placeholder your platform
provides rather than omitting phone_number, since call-started requires
the field. The event will be recorded and the rep will simply see no
customer match.
6. Retries and duplicates
Wink processes the event before responding, so a 200 means the event is
recorded, not merely queued.
- Retry on a timeout, a connection failure, or any
5xx. An exponential backoff — say 1s, 4s, 15s, then give up — is plenty; a call event that is minutes late has already missed the call it described. - Do not retry a
401,404, or422. Every one of them will fail the same way, and the retry only delays the alert to whoever can fix it. - Always send
event_id, and reuse the same value on every retry of the same event. It is unique per brand, so a replayed event is recorded once and answered withduplicate: true. Retrying is therefore safe: a200you never saw because the connection dropped will not produce a second event for the rep. - If you cannot supply an
event_id, use something you can regenerate deterministically from your own call reference —<your-call-ref>-started,<your-call-ref>-hold-1. Without one, Wink has no way to tell a retry from a genuinely new event, and a rep can see the same call announced twice.
Recording controls carry a sequence number minted by Wink, based on receive
time and the rep's previous sequence, continuing across call boundaries.
The page and companion use the same number, so hearing a change on
both paths applies it once. A retry re-delivers the current recording
state with its existing sequence; retrying an old pause after a resume
cannot pause that call again. A new event without an event_id is applied
in arrival order, so always supply an id for recording controls.
Ordering is not required in the sense that nothing is rejected for arriving
out of order: events are timestamped and applied as they arrive, and a
hold-ended that overtakes its hold-started self-corrects on the next
event. But an end, hold or recording event for a call Wink never saw start is recorded
without becoming live call state — there is no call on the rep's screen for
it to change. Send call-started first for every call.
7. Getting started
- Get a test key — an owner mints one under Org Settings → API Keys (see API keys), or ask the Wink team during onboarding.
- Post a
call-startedfor a real rep's email and a number you can dial, with that rep signed in to Wink, and confirmagent_resolved: true(and, with a single tab open,delivered_sockets: 1— see the note above on why that count can read low). - Post the matching
call-endedand confirm the call clears from the rep's screen. - Repeat the
call-startedwith the sameevent_idand confirm you getduplicate: true— that is your retry path proven. - Post
recording-paused, reload the page while paused, then postrecording-resumed. Confirm the companion pauses and resumes capture. A hold event alone should leave capture running. - Wire the six events into your dialler's call flow and ask for a production key.
If something looks wrong, send us the agent_email, the event_id, and the
timestamp of the request; every event is recorded against them and we can
trace it end to end.