← All posts
Changelog

UnifyPort v1 API Is Production-Ready: Everything in the First Stable Release — UnifyPort

The UnifyPort v1 API is now production-ready. This is the first stable release: a single send endpoint, a normalized webhook event layer, and account connection flows for all six channels — WhatsApp, Telegram, LINE, TikTok, Zalo, and X — behind one API surface.

This post is the changelog for that release. If you’ve been waiting for a version you can build on without expecting breaking changes underneath you, this is it. Below is what shipped, endpoint by endpoint, with the actual event names, auth flows, and reliability behavior you’ll work against.

One send API across six channels

The core of v1 is a single endpoint. POST /v1/messages sends a normalized text message through whichever provider account you select. You don’t learn six SDKs or six payload formats — you learn one.

Provider-specific behavior didn’t disappear; it moved into a structured provider_data field. Need to reply to a specific message or set Telegram’s parse mode? Those live in provider_data (for example reply_to and parse_mode) without changing the shape of the top-level request. The common case stays simple; the platform-specific case stays possible.

The six supported channels are WhatsApp, Telegram, LINE, TikTok, Zalo, and X. In the API, provider accounts are created with names like telegram, whatsapp, twitter, line, and zalo.

11 standard webhook events

The other half of v1 is the standard event layer. Every provider message, delivery status update, and account lifecycle event is converted into one stable JSON shape. There are 11 standard events in this release, and your backend consumes all of them through the same handler.

The two you’ll use most:

  • message.received — an inbound message arrived on a connected account
  • message.status.updated — a delivery status changed on a message you sent

The account lifecycle events make connection state observable instead of opaque:

  • account.auth.required — the account needs a login action (a QR scan or a verification code)
  • account.auth.succeeded — login completed; provider_account_ref is now populated, and the event carries standard profile fields (display_name, picture_url, region, status_message)
  • account.started — the account runtime is online
  • account.status.updated — the account’s status changed

A message.received event looks the same regardless of which of the six channels it came from:

{
  "event": "message.received",
  "account_id": "acct_8Q2vK",
  "provider": "whatsapp",
  "from": "+6591234567",
  "text": "Hi, is my order ready?",
  "timestamp": 1749254400,
  "message_id": "wamid.HBgLNTU..."
}

Swap whatsapp for line or zalo and your routing logic doesn’t change. That’s the whole point of the standard layer.

Account connection: QR and code flows

Connecting an account is its own flow in v1, and it’s designed so the auth state is something your webhook can watch rather than something you have to babysit.

You create an account with POST to the accounts endpoint, choosing an auth_mode that matches the connection style:

{
  "provider": "whatsapp",
  "auth_mode": "code",
  "provider_data": { "phone": "+6591234567" }
}

For code-based flows, the E.164 phone number is supplied through provider_data.phone. It’s persisted on the account and automatically replayed for subsequent authentication actions, so you don’t resend it each step. Creating a duplicate provider identity returns a clean 409 duplicate_provider_account instead of a silent second account.

For QR-based flows, the credentials arrive asynchronously through your webhook:

  • WhatsApp — call /auth/qr/start to begin a device session. The QR token lands on your webhook as an account.auth.required event (auth_payload.qr_code is the raw token your UI renders), and is also exposed synchronously via /auth/qr/check for polling. After scanning, account.auth.succeeded arrives with provider_account_ref filled in, followed by account.started.
  • LINE — the QR URL and PIN are delivered asynchronously. Listen for account.auth.required, or poll /auth/session to read auth_payload.url (the QR image) and auth_payload.pin.

The webhook URL is injected automatically when the account is configured — auth state transitions and login events arrive through the same endpoint as your inbound messages. One webhook, every event.

Once account.auth.succeeded lands, POST /v1/accounts/{account_id}/runtime/start brings the account online (it’s a no-op if the runtime is already up).

Webhook control plane

Webhook endpoints in v1 are first-class, configurable objects — not a single URL field buried in settings.

When you create a webhook receiver, you choose exactly which events it gets. subscribed_events accepts the standard event names (such as message.received, account.status.updated), or ["*"] to subscribe to everything. Unknown event names are rejected at write time rather than failing silently later.

Signing is built in. Provide a signing_secret and every delivery is signed with HMAC-SHA256 so your endpoint can verify authenticity. Leave it empty and signing is explicitly disabled — a deliberate choice, not an accident.

Retry behavior is yours to set. retry_policy.max_attempts is a non-negative integer; 0 disables retries entirely, and non-integer values are rejected. The platform handles the redelivery; your endpoint just needs to respond.

Reliability that stays in the platform layer

A production release needs production defaults, and v1 ships with them. Inbound delivery runs through a queue with rate limits, retries, idempotency, and failure isolation, so provider turbulence stays inside the platform instead of cascading into your application.

Account runtime state is normalized too. Instead of each provider’s own vocabulary, runtime_status takes one of eight platform-standard values — unknown, starting, running, stopping, stopped, reconnecting, disconnected, error — with provider-specific labels mapped into that set before you ever see them. You write one state machine, not six.

For accounts that need geographic routing, an optional proxy_config object is persisted with the account and applied automatically.

What this release means

v1 being production-ready is a commitment, not just a version number. The send endpoint, the 11 standard events, the auth flows, and the webhook control plane are the stable surface you can build a product on top of. The point of UnifyPort has always been to stop teams from rebuilding the same integration six times; v1 is the version where that surface stops moving.

If you’ve been reading our other posts on receiving messages without official-API friction — WhatsApp inbound without the official API, LINE without an Official Account, or the WhatsApp vs Telegram cost comparison — this is the API those paths run on.

To start: create an API key, connect your first account with the auth_mode that fits the channel, register a webhook endpoint with a signing_secret, and subscribe to message.received. You’ll have a normalized inbound event hitting your backend within the same session. From there, every additional channel is the same flow — not another integration project.