1. Authenticate with a scoped key
Every REST call uses a scoped secret key (flowyte_sk_…), minted by the operator for the
destination and shown once:
escalation_destinations:*: a connector key works conversations but can’t edit
destinations.
Keys are organization- and destination-scoped: a key only ever sees its own tenant’s sessions.
Cross-tenant calls fail closed as
404.
2. Verify the signature first
Flowyte delivers webhooks to yourendpoint_url over its signed pipeline. The body is a stable
envelope — route on the channel field in every notification:
Signature headers
Verification recipe
1
Check the timestamp
Reject if
|now − Flowyte-Timestamp| > 300s (replay window).2
Recompute the signature
Compute
hex(HMAC_SHA256(secret, Flowyte-Delivery + "." + Flowyte-Timestamp + "." + rawBody))
using the raw request bytes — re-serializing the JSON changes whitespace and breaks the match.3
Compare in constant time
Accept only if it equals
Flowyte-Signature, using a constant-time compare (never ==). During a
rotation, also accept if any secret you hold verifies Flowyte-Signature-Prev.401 — do no work.
Webhooks are the doorbell, not the mailbox — they tell you something happened; the REST API is the
source of truth. Delivery is at-least-once, so a webhook may be redelivered — dedupe on
Flowyte-Delivery. Don’t parse conversation content out of webhooks; read it from /messages.3. Claim, reply, resolve
Claim
POST /escalations/{id}/claim takes ownership and starts an activity-based lease (chat ≈ 2 min,
SMS ≈ 4 h). Claiming is idempotent for your key — re-claiming a session you hold just re-extends
the lease — and guarded, so a second connector gets 409 claim_conflict.
The claim response (and GET /escalations/{id} as the owner) returns the context package: the last
N transcript turns (each with its sequence number), the escalation reason, any verified identifiers
(SMS: the caller’s phone), and completed or failed tool actions (names and status only). It’s returned
only to the claiming owner and is rebuilt at claim time, so you start current with any turns that
arrived while the notification was in flight.
Claim can also fail with 409 destination_unavailable (the harness was disconnected or the destination
is inactive) or 403 plan_required (the organization is below Starter — the handshake test is exempt).
Read and sync
Every turn — customer and your own replies — carries the conversation’s sequence number (seq),
the single ordering and dedup key. To catch up, or after a reconnect, poll
GET /escalations/{id}/messages?after_seq=N and process strictly increasing seq values. Never assume
a webhook told you everything; the /messages mailbox is authoritative and ordered.
Reply
client_message_idis your idempotency key. A replay with the same id returns the original receipt ({ messageId, seq, delivered }) instead of double-sending. Use one stable id per logical message and reuse it across retries.- The reply is delivered to the customer in their original thread — chat shows a “specialist has joined” divider; SMS sends a text.
- Media (
mediaUrls) is chat-only; on SMS it returns422 media_unsupported_channel. - SMS compliance: a reply to an opted-out recipient returns
409 sms_suppressed; a quiet-hours send returns200withdelivered: false. These are terminal — don’t retry.
/messages read) extends your lease. Send
POST /escalations/{id}/heartbeat if you go quiet but want to keep the thread;
POST /escalations/{id}/typing shows a typing indicator (chat only). If the lease expires, the thread
returns to the AI and you get 409 lease_expired — re-claim to continue.
Finish
4. Channels: chat vs SMS
The lifecycle is identical across channels — route on thechannel field — but:
- Chat replies appear live in the website widget; typing indicators and media attachments are supported.
- SMS threads are customer-initiated only. You reply inside an SMS conversation the customer already started — you cannot open one. (Outbound initiation is coming soon, tied to Flowyte’s outbound campaign rules.) SMS has no typing indicator and no media in v1, and it requires the operator’s completed A2P 10DLC (TCR) registration — the same registration their inbound number already needs. See SMS.
5. The handshake test
Before a destination can route real conversations, the operator runs a handshake test (POST /escalation-destinations/{id}/test) that proves your connector can talk back and forth:
- Flowyte opens a sandbox escalation on a synthetic conversation and sends you a signed
escalation.testwebhook — a normalescalation.requestedenvelope plus"test": true. - Your connector must, within the window: verify the signature → claim → post one message → resolve.
- Flowyte reports a step ledger (
{ webhook_delivered, claimed, message_posted, resolved }) with an actionable hint on failure. Only a green run flips the destination toactive.
escalation.test exactly like escalation.requested — same code path. Test messages reach
no customer and are never billed, but your connector doesn’t need to know that; just claim, reply,
resolve.