Skip to main content

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​

FieldRequiredNotes
agent_emailyesThe rep's single-sign-on email address, as Wink knows it. Case and surrounding whitespace do not matter.
phone_numberyesThe customer's number. E.164 preferred — see §5.
customer_idnoYour 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_idnoYour unique id for this event. Supply it: it is what makes a retry safe (§6).
occurred_atnoISO-8601, ideally with a UTC offset (2026-08-14T09:15:22Z). Defaults to the time Wink received the request.
metadatanoAny 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_number is only required on call-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_id must be sent as a JSON string — quote it. A bare number (12345 instead of "0012345") is rejected outright (see the 422 row 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_at never 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.
  • metadata may be omitted, or sent as null; 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_email must be a non-empty string, and (on call-started) so must phone_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-paused and recording-resumed apply to whatever call that rep is currently on.
  • If a call-started arrives 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 missed call-ended.
  • A call-started naming the number the rep is already on, with a customer_id that 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 an event_id. Same number with a conflicting customer_id is 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 200 with "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:

FieldMeaning
liveBoolean. 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_atISO 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_atISO 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
}
FieldMeaning
acceptedAlways true on a 200. The event is durably recorded.
duplicatetrue 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_resolvedWhether agent_email matched an active Wink user for your brand.
call_activeWhether 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_socketsHow 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 (when agent_resolved is also false) the rep could not be identified at all. Not an error, and not worth retrying — see §3a.
  • delivered_sockets: 0 with agent_resolved: true and call_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, a 0 can also mean the rep's tab is connected elsewhere and did receive the event. Not an error, and not worth retrying.

Error responses:

StatusMeaningWhat to do
401Missing, malformed, revoked, or wrong-brand API key.Do not retry. Check the key and the base URL; ask for a re-issue.
404The host you posted to is not one Wink serves for you.Do not retry. Fix the base URL.
422The 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.
5xxSomething 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 401 and 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 returns 422 even 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 422 on 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, or 422. 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 with duplicate: true. Retrying is therefore safe: a 200 you 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​

  1. Get a test key — an owner mints one under Org Settings → API Keys (see API keys), or ask the Wink team during onboarding.
  2. Post a call-started for a real rep's email and a number you can dial, with that rep signed in to Wink, and confirm agent_resolved: true (and, with a single tab open, delivered_sockets: 1 — see the note above on why that count can read low).
  3. Post the matching call-ended and confirm the call clears from the rep's screen.
  4. Repeat the call-started with the same event_id and confirm you get duplicate: true — that is your retry path proven.
  5. Post recording-paused, reload the page while paused, then post recording-resumed. Confirm the companion pauses and resumes capture. A hold event alone should leave capture running.
  6. 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.