samples/dotnet/inproc/confirmation in the repository: one application that
is both the Veriqa issuer and the backend that calls it. Two more are built the same way:
samples/dotnet/inproc/step-up confirms a destructive action and checks that the account owner
confirmed it (expected_identities), and samples/dotnet/inproc/agent-approval makes an AI agent’s
tool call wait for a person’s approval.
The flow
- Your backend gets a token with the Client Credentials grant.
- It creates the transaction:
POST /api/transaction/confirmation, naming a declared action type and filling the slots of its message. - It shows the user
channel_entryfrom the answer — the QR image (qr) on a screen, or the link (url) on the same device. - The user opens the channel, reads the wording and confirms or declines.
- Your backend polls
GET /api/transaction/{id}/resultuntil the outcome is no longerpending. - Optionally, it exchanges a confirmed transaction for the
id_tokenof the person who confirmed.
1. Configure the client
Everything lives in the client entry of the application that calls the API:Where the action type is declared
The action type is the message kind, and it is declared in the client entry:Veriqa:OpenIddict:Clients:[n]:MessageTemplates:{action type}:Contract and …:Templates. There is
no ByType or ByAction group to add for it.
A Tenant level is also read, when your host registers a reader for it; none is registered out of
the box.
What the user reads when it ends
Two host-level settings word the end of the transaction, and the runnable samples above state both:DisplayIntent says where the receipt of the outcome appears. The product ships ReplacePrompt —
the receipt takes the place of the message that asked the question — and a confirmation is the case
for the other value: the question carries the wording of the action, so replacing it leaves the
conversation with an outcome and no record of what it answered. NewMessage keeps both. It is an
intent rather than a promise: Telegram and MAX honour it, WhatsApp always sends a new message. Where
the platform lets the buttons of a delivered message be taken away without rewriting its text,
NewMessage takes them away as it delivers the receipt: the question keeps its wording as the record
of what the outcome answered, and nothing under a settled transaction invites another press. Where
the platform has no such operation, a button may survive — pressing again only earns another receipt,
because a terminal transaction cannot be replayed.
The receipt kinds are the product’s own messages, so they live in the host section rather than in
the client entry — the rule about action types above does not reach them. The product ships no
wording under ByType:confirmation, so the broad {kind} declaration is the step that answers and
rewording it works as written. {app} is filled by the server with the ClientId, as above; the
last step of each ladder uses no slot, because no slot of a receipt is guaranteed — not even one
marked Guaranteed.
The contract
Contract:Slots is an array. Each slot:
The server fills
{app} of a confirmation with the client’s ClientId, not its DisplayName. To put a
human-readable name into the wording, use a slot of your own.
The templates
Templates is a ladder, fullest wording first. The shown step is the first one whose slots all have
values, so the optional payee drops out cleanly when it is not sent. The last step may use only
slots that always have a value — a caller slot with Required: true or a server slot with
Guaranteed: true. A ladder whose last step leans on an optional slot is left behind whole, and the
transaction has no text to show. Messengers show the plain text; a step may also be a structure
{ "Plain": "…", "Html": "…" } — see Veriqa:MessageTemplates.
2. Get a token
access_token, token_type and expires_in; cache the token until shortly before
it expires. A token issued to a user is refused on this API with 403.
The endpoint is served over https only — over plain HTTP it answers 400 invalid_request, “This
server only accepts HTTPS requests”. Behind a reverse proxy that terminates TLS, the proxy must be
trusted and forward X-Forwarded-Proto
(OS service, step 7).
3. Create the confirmation
The wording itself is never returned, and your slot values are not logged above
Debug.
The answer — 201 Created
Cache-Control: no-store.
4. Read the outcome
matched_type is the name of the comparable type that matched expected_identities, or null. Only
the client that created the transaction may read it: any other transaction — unknown, removed after
CompletedRetentionSeconds, or another client’s — answers the same 404 transaction_not_found.
5. Learn who confirmed (optional)
WithAllowConfirmationTokenGrant, exchange a confirmed transaction once for the id_token of
the confirming person:
id_token carries sub, and with the channel scope channel_type and channel_user_id. The
rules of the exchange — single redemption, invalid_grant for every kind of “no”, what the
application learns — are in Linking from your server.
Errors of the creation request
Every refusal is Problem Details (application/problem+json): the code is in title, and detail
names no value you sent. Nothing is created on any refusal.
Next steps
Linking from your server
Learn which messenger account confirmed and store it next to your user.
Veriqa:MessageTemplates
Ladders, editions, levels and axes of messages.