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 four 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
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 |
|---|---|---|
call_id | yes | Your platform's id for the call, stable for its whole lifetime (e.g. the contact id). Every later event for the same call must repeat it. |
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. |
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",
"call_id": "4c9f0e21",
"agent_email": "rep@example.com",
"phone_number": "+447700900123",
"occurred_at": "2026-08-14T09:15:22Z",
"metadata": { "skill": "jewellery", "campaign": "spring" }
}
POST /call-events/call-ended
Required: call_id, agent_email. Optional: event_id, phone_number,
occurred_at, metadata (a good place for disposition and wrap-up notes).
POST /call-events/hold-started and POST /call-events/hold-ended
Required: call_id, agent_email. Optional: event_id, occurred_at,
metadata.
Rules that hold for every endpoint
phone_numberis only required oncall-started. The later events in a call are matched bycall_idand inherit the number from the start event, so there is no need to repeat it (you may, and it is stored if you do).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, so you can add to your template without coordinating a release with us.
call_idandagent_emailmust be non-empty strings. That is the only validation strict enough to reject a request.
4. Responses
Every accepted event returns 200 with the same four fields:
{
"accepted": true,
"duplicate": false,
"agent_resolved": true,
"delivered_sockets": 1
}
| Field | Meaning |
|---|---|
accepted | Always true on a 200. The event is durably recorded. |
duplicate | true when this event_id was already recorded — your retry arrived and was correctly ignored. No second event was raised. delivered_sockets is normally 0 on a duplicate; it is non-zero only in the rare case where the original attempt was recorded but never reached the rep, and your retry is what completed it. |
agent_resolved | Whether agent_email matched an active Wink user for your brand. |
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. |
Two 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.delivered_sockets: 0withagent_resolved: 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, or call_id / agent_email / (on call-started) phone_number was missing or empty. | 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.
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 —<call_id>-started,<call_id>-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.
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 or hold 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. - Wire the four events into your dialler's call flow and ask for a production key.
If something looks wrong, send us the call_id, the event_id, and the
timestamp of the request; every event is recorded against them and we can
trace it end to end.