Skip to main content
A single user action produces two results: they sign in, and your product gains a live channel — Telegram, WhatsApp, another messaging app or email, whichever they confirmed in. From then on you can send notifications, approvals and service messages without asking for a contact separately.

What you send

An ordinary authorize request with the channel scope:

What you get

After exchanging the code for tokens, two extra claims: Without the channel scope there are no typed claims. They reach neither the access_token nor the id_token, and therefore never appear in the /connect/userinfo response — it mirrors the access token.
The gate closes the individual claims but not the value. With the default identity resolver sub is composed as {channel_type}:{channel_user_id}, so an application that splits the string on the colon gets both parts without requesting the scope. Treat the gate as a way not to handle channel identifiers where you do not need them, rather than as a way to hide them.
The gate applies in every delivery mode, the embedded one included. Even when Veriqa is linked as a library into your own process, the application receives the identity through the normal authorize → code → token exchange rather than as a ready-made principal. There is no “internal” path that hands out channel claims bypassing the scope.
sub is the stable subject identifier; the channel_type + channel_user_id pair is the user’s address in a specific messaging app. The link lives on your side: Veriqa does not keep your application’s accounts and does not map them to channels. The practical consequence: sub is your account key. The off-the-shelf CMSes do the same — Drupal and TYPO3 store it as the external identity name.

When the account already exists

The scenario doubles as “add a channel to an existing account”: a user who signed in the usual way goes through authorize with the channel scope, and you store the returned pair next to your account. There is no separate API for this — it is the same request.

Linking from your server, without a sign-in

Sometimes there is no browser redirect to hang the link on: your server wants a channel for an account on its own — for example, a signed-in user asks your support bot to connect a messaging app. That goes through the server-to-server confirmation entry followed by a token exchange. What the integrator enables for the client: AllowClientCredentials, AllowConfirmationTokenGrant and openid in AllowedScopes (plus channel if you want the typed claims). The permission is the integrator’s: no parameter of your request widens what is disclosed, and the claims never go beyond AllowedScopes.
  1. Create a confirmation transaction with POST /api/transaction/confirmation, without expected_identities — you do not know yet which channel account will confirm; that is exactly what you want to learn. Show the user the channel_entry from the response.
  2. Wait for the outcome with GET /api/transaction/{id}/result until it is confirmed. With no expected identities matched_type is null — that is not a refusal, and the token is issued all the same.
  3. Exchange the transaction for a token:
    You get an ordinary token response: access_token, token_type, expires_in and id_token, with no refresh_token. scope follows RFC 6749 §5.1: it is returned only when the granted scopes differ from the requested ones. The exchange grants exactly what you requested, so the member is absent — do not require it.
  4. Store sub from the id_token next to your account.
The rules of the exchange:
  • scope must contain openid; channel adds channel_type and channel_user_id; anything else stays within AllowedScopes. offline_access is refused with invalid_scope, as is a scope without openid. A missing transaction_id is invalid_request.
  • A transaction is redeemed once. A second exchange gets invalid_grant — the same error with the same description as an unknown identifier, another client’s transaction, a transaction not yet completed or one already removed after CompletedRetentionSeconds. The responses do not tell these reasons apart on purpose, so exchange right after confirmed. Of two simultaneous exchanges, exactly one gets the token.
  • Once applies to the redemption, not to reading the claims: the access_token of the same response carries the same claims and returns them from /connect/userinfo until it expires.
  • The link key is sub as a whole. With the default identity resolver it is composed as {channel_type}:{channel_user_id}, but a host with its own identity resolver may compose it differently — do not parse the string; request the channel scope when you need the pair.
What the application learns, and who warns the user. With the grant enabled, your application receives the confirming person’s channel identity in every confirmation of that client, not only where the task needs it. Veriqa asks the user no separate “share my data” question: the warning that data is being handed over, and the context of the request, live in the wording of the integrator’s action_type and in the slots your backend fills. Veriqa does not check that every action type of such a client carries that warning — it is the integrator’s responsibility. The entry is a bearer one: whoever opens it first confirms, and sub belongs to them — which is why the context of the request belongs in the wording.
The context of the request is assembled from string slots of the action type’s message contract; each such slot must declare its max_length, and the ceiling is 128 characters. The integrator writes the wording, and your backend fills the slots (slot_values) at creation with what it sees of the device — someone who forwards the QR cannot forge those values, because they do not own your backend. For example, a template:
Here service, device and location are slots your backend fills. The server fills {app} of a confirmation itself, with the transaction’s application identifier (client_id) rather than the client’s DisplayName — so a human-readable application name reaches the wording only through a slot of your own. The example is synthetic: these slot names and this wording are not in the shipped configuration. How to declare the contract and the template, and the full request and response, are in Server-to-server confirmation API.

Two bots: handing the user over to your bot

The sign-in is handled by the bot Veriqa runs, while your own bot runs the business scenarios. After the sign-in the first bot shows a link button into the second, and your bot has to learn who tapped it. The address of that button can carry a link handle — a single-use value your server exchanges for the identity of the user who has just signed in. Two bots is one of several ways to use a bot you already run together with Veriqa — the comparison is in You already have a bot.
  1. Put {handle} into the address of the button — RedirectUrl or the Action of a link item of the menu (Menu and Redirect):
    The example is synthetic: shop_bot is not a real bot. The handle is 22 characters [0-9A-Za-z]; the rest of the platform’s budget for the start parameter (64 characters in Telegram) is yours.
  2. Your bot receives the handle when the user taps the button — in Telegram, as /start <handle>.
  3. Your server exchanges it, with the client credentials token of the application the user signed in to (how to get the token; the client needs AllowClientCredentials):
    channel_type and channel_user_id are returned only to a client whose AllowedScopes contain channel; otherwise the response carries sub alone. sub is the one this application receives in its tokens.
  4. Store the link on your side — “the user of my bot ↔ sub”. Veriqa keeps the handle only until it is exchanged or expires, and keeps no link.
Check who tapped the button. A button can be forwarded, and whoever taps it brings the handle to your bot. Where the platform gives a person one identifier for every bot — Telegram, for example — compare the sender of the message (from.id) with channel_user_id from the response and refuse a mismatch. Veriqa does not make this comparison for you: the endpoint takes the handle only. The comparison needs the channel scope on the client. Where each bot sees its own identifier for the same person, or the tenant has pseudonymization on (channel_user_id is then a pseudonym), there is nothing to compare with — the handle is a bearer link: the link goes to whoever taps the button first.
The rules of the exchange:
  • A handle is exchanged once, and only by the application the sign-in belongs to. Another application gets 404, and the handle stays valid for its own.
  • It is short-lived: Veriqa:TransactionEngine:CompletedRetentionSeconds (600 seconds by default) from the moment the button is sent. The button stays in the chat: tapped later, it brings your bot a handle for which the endpoint returns 404 — ask the user to sign in again.
  • 404 is one response for a missing or empty handle, an unknown, expired or already exchanged one, and another application’s — the reasons are not told apart on purpose. A body that is not JSON is 400, a Content-Type other than JSON is 415; no token is 401; a token issued to a user is 403. A 500 means the identity could not be read — that handle is spent too.
  • The handle works before the browser finishes the sign-in — and when the browser tab was closed and never does. The identity is confirmed by the user in the channel by then.
  • Several replicas need a shared store. Without the Redis channel stores a handle is found only by the replica that issued it.
  • A button without a handle is fine where you do not need it — in Telegram your bot sees the same user identifier the sign-in returned in channel_user_id, so an account already linked is recognized without an exchange.
  • The endpoint exists for sign-ins only; a confirmation your server creates is followed by no button.

When the sign-in is pointless without your bot

The sign-in completes when the user confirms it in the channel: the browser receives the code and returns to your application. The button into your bot arrives afterwards and does not affect the sign-in — a user who never taps it is signed in all the same, while your bot does not know them yet and cannot message them. If users must not go any further without moving into your bot, make the move mandatory in your application:
  1. After the sign-in, check whether a link exists for the sub you received. It appears when your server has exchanged the handle (step 3 above); where your bot sees the same identifier the sign-in returned, it appears when the bot has received the first message from that user.
  2. No link — show a “one step left” screen with a link into your bot, and do not let the user go any further. The user is already signed in with you, so you can issue the link on that screen yourself: a single-use token of your own in the start parameter, which your bot matches to the session. It does not need the handle from Veriqa — that one exists only in the button in the chat and is short-lived.
  3. Grant access once the link has appeared — on an event from your bot or by polling from the page.
If there is no website and your product lives entirely in the bot, no separate gate is needed: the exchange of the handle is the sign-in to the bot, and a user who did not tap the button simply did not sign in.

One sign-in, two channels

A sensitive operation can require confirmation in two different trusted channels.