Skip to main content
Veriqa reads the host’s standard IConfiguration, so every setting can come from appsettings.json, environment variables, user-secrets or any other provider. Everything Veriqa owns lives under a single root key, Veriqa: This reference answers “which setting controls it”. Two neighboring questions have pages of their own: what the specifications give you is in OIDC and OAuth 2.0 support, and what is missing is in Known limitations. In environment variables, : becomes __ — Veriqa:OpenIddict:Database:Provider is Veriqa__OpenIddict__Database__Provider. One shell limitation is worth knowing here. A POSIX shell allows only letters, digits and _ in a variable name, so export rejects the whole name as soon as any segment of the path carries a - or a .: export Veriqa__ChannelDisplay__Hints__my-channel=... fails as an invalid identifier. Where that segment comes from makes no difference. It may be a value you choose — the channel type in Veriqa:AuthPageDesign:QrCode:PixelsPerModuleByChannel:<channel type> and in Veriqa:ChannelDisplay:Hints:<channel type> (the channel type of an SPI adapter is allowed a hyphen), or the record code in the Veriqa:UiConfigurations:Records catalog. It may just as well be a fixed key name of the schema, which you do not choose at all — the message kind in Veriqa:MessageTemplates:confirmation-prompt. The variable name itself is perfectly legal, and the limitation is worked around with the usual tools: Docker (-e), Kubernetes variables (env:), launchSettings.json and a plain env NAME=VALUE command all accept such names, and .NET reads them as usual. A path whose segments hold only letters, digits and _ is set from an environment variable in the usual way. A configuration directory of its own. One variable does not name a setting but a place to read settings from: VERIQA_CONFIG_DIRECTORY points at a directory, and the host layers the appsettings.json and appsettings.<Environment>.json found there over the files shipped with the program and under the environment variables. Both files are optional; a directory that does not exist prevents startup, an empty one is not an error. This is how an installation made by the OS-service installer keeps its configuration — /etc/veriqa on Linux, %ProgramData%\Veriqa on Windows — where replacing the program cannot touch it.
Configuration is validated at startup. An unknown enum value, a relational provider without a connection string or an enabled channel missing its token stops the application immediately rather than failing later on a user’s first sign-in.

Veriqa:OpenIddict

OIDC server, its store and the registered clients.

Database

Provider has no default: an unset value is not “InMemory by default”, it is a choice nobody made. Outside the Development environment such a host fails to start (the startup check names the fix); in Development it starts with a Warning. InMemory remains a valid value, but it is set explicitly and every restart invalidates issued tokens — fine for development and for a single-instance connector. In embedded mode these keys do not select the provider: there your host selects it by calling authServer.UseOpenIddictDatabase(...) or authServer.UseInMemoryOpenIddictStore(); in self-hosted mode the choice is set with these keys.

Server

The behavior depends on the environment, and the difference matters:
  • Development — with no explicit certificates, development keys are generated. Convenient locally, but every restart produces new keys and multiple instances do not share them.
  • Any other environment — there are no development keys: the server refuses to start until both certificates (signing and encryption) are configured. There is no fallback to generation.
The shipped Docker image runs with ASPNETCORE_ENVIRONMENT=Production, so it follows the second rule. If you see generated development keys in a deployed installation, the process is running in Development — worth fixing before rollout.

Clients

Veriqa:OpenIddict:Clients is an array of the applications allowed to sign users in: Next to the clients array sits the deployment gate:

Veriqa:TransactionEngine

The sign-in transaction lifecycle.

Store

See Storage for migrations and retention.

Veriqa:ScopesClaims

Which scopes exist, which claims they carry and which claims a sign-in requires.
A non-empty Scopes replaces the product’s own set of scopes as a whole: scopes_supported of discovery then lists exactly the scopes of the catalog (plus openid, which is always registered), and a request for any other scope is refused with invalid_scope. If your clients use offline_access, channel or avatar, list them in the catalog explicitly.
Keys of a claim in Claims:

Veriqa:AuthPageDesign

Look and feel of the sign-in page — full walkthrough in Configuration & customization. ChannelButton, ChannelLayout and Cancel are stated on the same ownership levels as the QrCode section below — the ui_config record → the client (Veriqa:OpenIddict:Clients[]) → this global section — and the nearest level that states a key wins it on its own.

Veriqa:AuthPageDesign:QrCode

The QR code on the sign-in page: whether to show it and at which resolution to draw it. The section is stated on three ownership levels, and the nearest level that states it wins: the ui_config record → the client (Veriqa:OpenIddict:Clients[].QrCode) → this global section. Showing the QR and its resolution are two different settings, and a level wins each of them on its own: a client that states only ShowQrCode keeps the global resolution. The resolution itself is ONE setting stated per channel: PixelsPerModuleByChannel and PixelsPerModule are two entries of the same level, and a level exhausts both of them before the lookup moves on to the level below. What that means in practice: a client that states only PixelsPerModule wins the resolution for every channel — the per-channel map of the global section is never reached. To keep a channel’s own resolution, name it in the map of the same level that states PixelsPerModule. An example. The global section states PixelsPerModule: 6 and a PixelsPerModuleByChannel entry telegram: 14, while the app-client client states only PixelsPerModule: 8: A PixelsPerModule outside the range on the client or ui_config level does not apply — the resolution comes from the level below and the host still starts. Such a value is reported by ONE warning in the log per configuration snapshot: the whole catalog of values (the global section, the client entries, the ui_config records) is checked at startup and on every configuration reload, rather than on every request. In the global section such a value remains a deployment error and prevents startup.

Veriqa:Localization and Veriqa:ChannelDisplay

Veriqa:RateLimit

Per-endpoint limits, each a permit count over a window in seconds. Defaults: Webhook limits deserve attention when you scale: they apply per instance, and a channel provider that retries aggressively can trip them.

Veriqa:InitiatorContext

Context about the device that started the transaction, shown to the user before they confirm — see Initiator context.
The offline GeoIP path expects a GeoLite2 database, which Veriqa does not ship. Download it from MaxMind yourself and accept their EULA — the library is Apache-2.0, the license sits on the data. The standalone image already carries the library: mount the database and set GeoIp:DatabasePath to switch it on. In your own host it arrives in the satellite package Veriqa.Core.AuthServer.MaxMind (builder.Services.AddMaxMindGeoIp()).

Veriqa:HopMode

Hop mode puts a short one-time link to your Veriqa server into the QR code instead of the channel link. Two of its settings are set node-wide and may be overridden for a single channel: A channel value, when set, wins over the node-wide one; an absent or empty channel value inherits it. The remaining HopMode.* keys and their defaults are listed in Keys in the declaration catalog, where HopMode.Enabled and HopMode.RedirectStyle show “not declared”: their defaults are the values above. One QR code for the whole transaction. Veriqa:HopMode:QrMode decides what a sign-in window or a confirmation page with several channels shows while hop mode is on for at least one of them: A page of one channel, a page under ShowQrCode: Never and a phone under DesktopOnly show no tabs whatever the value. In a desktop window wide enough for it, a page with the single QR code uses two columns — the QR code on the left, the channel buttons on the right; a narrower window, and a page that shows no channel buttons on the desktop (ShowChannelButton: MobileOnly or Never), keep one column. Veriqa:HopMode:ShowChannelButtonsOnAnyChannelTab (default true) decides, under UnifiedWithChannels, whether the “Any channel” tab shows the channel buttons beside the one QR code. With false that tab holds the QR code alone and its instruction asks only to scan it; the tab of each channel keeps its button. A phone under ShowQrCode: DesktopOnly shows no QR code, so it shows the buttons of every channel whatever the value. Under PerChannel and Unified the key has no effect.
We recommend switching hop mode on. It gives the best user experience: a lighter QR code that any phone camera reads at once, a “scanned” signal on the desktop page as soon as the phone opens the link, and a launch page where a direct jump into the messaging app is unreliable. It stays off by default for one reason: every hop link is built on Veriqa:OpenIddict:Server:Issuer, so with hop on the issuer has to be stated as an absolute https address, or the server does not start. The phone also has to reach that address — to try hop mode on your own machine, put Veriqa behind an HTTPS tunnel, see Testing the integration locally. Why hop mode is better is covered in Hop mode.
The Email channel under hop mode. The target of the Email channel is an email the phone itself opens — as it is without hop mode. While the sign-in by an emailed link is on (PullEnabled), the phone gets one page — the address field and the button that opens the email — under either RedirectStyle, and the page never opens the mail client by itself. With the emailed link off, Email follows RedirectStyle like any other channel: Interstitial gives the launch page, which opens the email by itself and keeps a button in case it did not open; Direct redirects straight to the email (302 to mailto:), and that redirect tells the sign-in page that the email is open in the mail app. See Email under hop mode.

Hop mode for one application

The same members can be stated inside the entry of an OIDC client (Veriqa:OpenIddict:Clients), and they then apply to that application only:
appsettings.json
For each channel the value is taken in this order: the channel value of the client entry → the general value of the client entry → the node (its channel value, then the node-wide one). A client entry that states nothing inherits the node. A value that cannot be read — "HopEnabled": "maybe", or a style name other than Interstitial / Direct — is skipped with a warning in the log, and the next value in that order applies. A style stated as a number that names neither of the two is skipped the same way, but without the warning. The other hop settings — what is collected about the scanning device, TokenLifetime and HopPath — are set for the node only. The endpoint behind the hop link (HopPath) exists only when hop mode is on at startup for a channel that is enabled on the node — node-wide, for that channel, or in at least one client entry. Client entries are read once at startup: if hop mode was off everywhere and you turn it on in a client entry, restart the host. Whenever hop mode is on, Veriqa:OpenIddict:Server:Issuer has to be an absolute https URL — in a client entry as well as on the node.

Infrastructure

Flat keys read by the host, mostly relevant when you run more than one instance or behind a proxy: With neither DataProtection option set, keys live in memory and are lost on restart — fine for development, broken for a scaled deployment.
Redis licensing. Redis server ≥ 7.4 ships under RSALv2/SSPL (8.x additionally offers AGPL). That does not affect Veriqa’s license, but if it affects your policy, Valkey (BSD) is a drop-in alternative.

Veriqa:Channels

Every channel lives under Veriqa:Channels:{Channel} and activates only with "Enabled": true. Full setup with the provider-side steps is in Channel setup. Two more keys go into the section of any channel, a channel of a third-party adapter included: SpareStartCodeEnabled (bool, defaults to true) switches the spare start code the sign-in page offers for a channel that keeps a working link, and BotName (text, not set by default) states the name the user sees the chat under — it wins over the name the channel reports. Both are keys of the core with the channel as a dimension, Channels.SpareStartCodeEnabled and Channels.BotName in the catalog, stated in the configuration of the host or for a tenant. What they change on the pages: Channel setup.

InboundVerification — a required key of every channel section

Veriqa:Channels:{Channel}:InboundVerification declares how the authenticity of an incoming event of that channel is verified. The value comes from a closed dictionary:
The key is required for every enabled channel. A deployment where a channel declared at core level is enabled ("Enabled": true) and the value is missing or outside the dictionary does not start: the message names the channel type, the address of the key and all four admitted tokens. State the value in the sections of disabled channels too — then flipping Enabled to true does not break startup. The startup check walks exactly the set of channels the core builds its enabled list from: all shipped channels are in it, a channel plugged in through the SPI with a bare AddChannel call is not — the key of its section is needed as well, but a missing value does not prevent startup, see Build a channel adapter in C#.
You declare the fact, not the adapter and not the core: Veriqa only sees that a check ran and passed, never what the check was — and deriving the value from the channel type would leave a third-party channel with nothing to describe it. The value for every built-in channel and mode is in Channel setup. A channel that fetches its updates itself (UpdateMode: Polling) while declaring anything other than outbound_fetch produces a Warning at startup; startup continues. The declared value reaches the audit record as an attribute of its own, so the audit log shows what a confirmation was protected by.

OutcomeNotice — where the receipt of an outcome appears

Veriqa:Channels:OutcomeNotice:DisplayIntent (key Channels.OutcomeNotice.DisplayIntent) says how you want the receipt of a terminal outcome to appear in the conversation. It is stated over three levels — core, tenant and application — and each of them overrides the one below it freely: the global section sets the intent of the whole installation, a tenant states its own with the member OutcomeNoticeDisplayIntent of its record, and a single application with the member of the same name in its OIDC client entry. A ui_config record states no intent. Whatever level answers, the value covers the outcome as a whole — a confirmation, a decline and an expiry are all shown the same way. The shipped value is ReplacePrompt — what the channels did before the setting existed, so a deployment that states nothing sees no change. An unreadable value is read as nothing stated and answers ReplacePrompt too; startup is not interrupted.
This is an intent, not a guarantee. Telegram and MAX honor it. WhatsApp, Instagram, Messenger, WeChat and Viber always send a new message, and Email shows no outcome receipt at all — as they ship today. An adapter that does not honor the intent is not failing: nothing is logged as an error and the result of the delivery does not change. With NewMessage the receipt arrives as its own message and the buttons of the question are taken away as part of the same delivery, where the platform can change a message’s buttons without rewriting its text; the wording of the question is never rewritten. Failing to take the buttons away is not a failure of the delivery either — and where a button does survive, pressing it only earns another receipt, because a terminal transaction cannot be replayed.

Veriqa:CompletionBehavior

What a channel does after a sign-in is completed. It is one action, stated for every channel at once or for a channel of its own, and — for the actions that show buttons — what those buttons are. The action is stated on three ownership levels, and the nearest level that states it wins: the client → the tenant → this global section. Above the global section the same members sit in the CompletionBehavior section of the level’s own record — for an application that is Veriqa:OpenIddict:Clients[].CompletionBehavior:Action and …:ActionByChannel. A ui_config record states no action: this is behavior, not presentation. The shipped value is None, so a deployment that states nothing sees no change. A value the setting does not accept never interrupts startup, and how far it reaches depends on its shape. A text that is not one of the names of the setting — in Action or in an entry of ActionByChannel — leaves the whole level unstated: neither member of that level is read, and the action comes from the level below. A number that names no action discards only the entry it stands in: with Action: "WelcomeMessage" and ActionByChannel: { "telegram": 7 } on one level, telegram takes that level’s WelcomeMessage. The action is ONE setting stated per channel: ActionByChannel and Action are two entries of the same level, and a level exhausts both of them before the lookup moves on to the level below. So a client that states only Action wins the action for every channel — the per-channel map of the global section is never reached. To keep a channel’s own action, name it in the map of the same level that states Action.
Here a sign-in through telegram is followed by a welcome message, and a sign-in through any other channel by nothing. What a channel performs is a fact of the channel, not of your configuration: An action the channel does not perform is never handed to it. It is replaced by the channel’s own default — None for every channel in the table — and the replacement is loud: a Warning in the log and an audit event channel_completion_behavior_clamped whose details read requested=<action>; applied=<action>. The sign-in itself completes as usual. The audit record names the tenant and the client of the sign-in — the levels the action may have been stated on — and, as its actor, the masked channel user. It carries the failure outcome, as every channel event does, although the sign-in succeeded. None is never replaced: doing nothing is honored on every channel, silently. Menu and Redirect send one message: a text with buttons under it. Redirect is a single button that opens a link; Menu is a list of buttons, one in a row, in the order you state them. The channels that perform them are in the table above; on any other channel the action is replaced, as described there.
Here a sign-in through telegram is followed by a menu of two buttons: the first opens a link, the second is handed to your code. A redirect needs the address only — the label ships as Open:
The items and the address are stated like the action. MenuItems and RedirectUrl live on the same three levels, each next to its own per-channel map, and a level exhausts its map and its plain member before the level below is read — so a client that states only RedirectUrl wins the address for every channel. RedirectButtonText is one value per level, with no per-channel map. A ui_config record states none of them. The labels are translated, the address is not touched. Text of an item and RedirectButtonText are looked up in the locale files of your host in the language of the recipient; a label no file carries is shown as you wrote it. The address is sent exactly as stated: nothing about the user is substituted into it. The text above the buttons is a message of its own — the kind post-login-menu or post-login-redirect, worded and overridden like any other message: Veriqa:MessageTemplates. What makes an item a link. An Action written as an absolute URL with scheme:// in front — https://…, tg://… — becomes a link button. Any other value is callback data: the button sends it back when pressed. The rule is the same one RedirectUrl is checked by. An address without // — mailto:…, tel:… — is therefore read as callback data, not as a link. A value with a space or a line break at either edge is not accepted at all — neither as RedirectUrl nor as an Action: it is reported like any other value outside the setting’s domain, and an item written that way takes its whole list with it. Whether a scheme is allowed on a button is the platform’s decision: an address it refuses fails the send, and the message does not go out. A link can carry a link handle. Write {handle} — in exactly this case — into RedirectUrl, an entry of RedirectUrlByChannel or the Action of a link item, for example https://t.me/shop_bot?start={handle}. When the message is sent, every {handle} in its addresses is replaced by one single-use value of 22 characters [0-9A-Za-z]; your server exchanges it for the identity of the user who signed in — Two bots. One message carries one handle, valid for Veriqa:TransactionEngine:CompletedRetentionSeconds from the moment it is issued. An address without the placeholder is sent as written and no handle is issued; there is no separate setting to switch this on. In callback data {handle} is not replaced — the value goes out as written. If the handle cannot be issued — the sign-in belongs to no client, or the handle store refused the record — the buttons that ask for it are not sent and a Warning is written: Redirect sends nothing, Menu goes out without those items. The sign-in is not affected. A callback item is handed to your code. The press reaches the built-in channel adapter — the bot is yours and Veriqa runs in the same process — and is passed to the handler you register:
IMenuItemPressHandler and MenuItemPress come with the Veriqa.Core.Contracts package. The handler returns nothing: Veriqa has already acknowledged the press to the platform, and what it means is up to you. The event names no sign-in — the menu is shown after the sign-in is over. The handler is created when an item is pressed, in the scope of that event, and for no other event of the channel. An exception — thrown by its constructor or by HandleAsync — is logged and affects neither the user nor any sign-in. Return quickly. HandleAsync is awaited while the event is being processed. Until it returns the platform is not told that the event was received: a webhook left unanswered is delivered again, and your handler gets the same press twice; in polling mode the events of other users wait behind it. Put anything slow into a background queue of your own and return. Treat press.Value as untrusted input. A button outlives the configuration that drew it: an item you removed from MenuItems can still be pressed in an old message, and Veriqa does not check the value against the items configured today. Check the value, and what this user may do, in your handler. Without a registered handler callback items are not drawn. A Warning names how many were left out; the link items of the same menu are still sent. A press of a button sent before a restart that dropped the handler is left without processing. When nothing is sent. Menu with no items stated for the channel, Menu whose every item was left out, and Redirect with no address stated each write a Warning and send nothing; the sign-in completes as usual. A value the setting does not accept never interrupts startup and leaves its entry unstated, so the value comes from the next entry or the level below: a RedirectUrl that is not a URL, and a list in which any item has a blank Text or Action — one such item takes the whole list of that entry out. An empty list states nothing either: to switch a menu off on a level above, state the Action, not an empty list. The platform’s budget is checked before the send, by the limits the platform itself names: A message none of whose buttons can be drawn is not sent. How a post-completion message behaves — the welcome message and a message with buttons alike:
  • It follows every completed sign-in of a channel that performs the action — confirmed in the channel, confirmed on the web page, or completed by the steps that collect missing claims. A confirmation your server creates is not a sign-in and is followed by nothing.
  • It arrives as a message of its own, after the outcome receipt, and never replaces it.
  • A failure to send it is logged and does not change the result of the sign-in.
  • The text of the welcome is the message kind post-login-welcome — worded, localized and overridden like any other message: Veriqa:MessageTemplates.
  • Delivery is not recorded on the transaction. An event repeated in order earns no second message; the same event processed twice at the same moment may, as the product ships today.
In the catalog table the key CompletionBehavior.Action shows not declared in the Default column: the shipped None is applied where no level states the action, the same way as the shipped resolution of AuthPageDesign.QrCode.PixelsPerModule. CompletionBehavior.MenuItems and CompletionBehavior.RedirectUrl show not declared for a plainer reason: the product ships no menu and no address.

Veriqa:MessageTemplates

Everything the user reads in the messaging app and in email is a message. One message is two settings rather than one text: the contract declares which slots the message has, and the template carries the wordings that use those slots. Hence two addresses — one pair per message, not one line per text: The bracketed groups are optional: a wording that does not narrow along an axis drops that group whole — which is how one address serves every step of the lookup, from the most specific ({kind, transaction type, action type, surface, channel}) down to the broadest ({kind}). The kind never drops out: “the text of which message” is the question itself. One group is not independent: ByAction is read only under ByType. No step of the lookup names an action type without its transaction type, so a wording at {kind}:ByAction:{action type} would never be reached — the host refuses to start on it, saying the address is “narrowed by a combination of axes no step of its ladder addresses”. A confirmation’s action type is therefore worded at {kind}:ByType:confirmation:ByAction:{action type}. The kinds the product ships: A template’s value is a ladder of steps, fullest to minimal, as one array. A step is either a string (a text that suits any sink) or a structure { "Subject": "…", "Html": "…", "Plain": "…" }, where Html and Plain are the editions of the step: an email is composed once and carries both of its parts at once. Each edition is optional, but a step must state at least one — otherwise startup fails, naming the message kind and the number of the step. The one shown is the first step that states the edition asked for and has all of its slots filled, so the two parts of one email may come from different steps, and the subject is taken from the step chosen for Html. An edition no step states is one the email does not carry at all — that is how an HTML-only email is declared. The minimal step must lean on guaranteed slots only — checked at startup, on every step and for every edition separately. A declaration replaces the ladder whole: the steps stated at different addresses are never merged. A wording the product ships may sit at a narrower address than the one you write. The lookup goes level by level, and a level exhausts every step of the address — from the most specific down to the broadest — before the resolution drops to the level below. The shipped declarations are the fallback of the Core level, read at the same addresses; and Veriqa:MessageTemplates in your appsettings.json is that level. So a declaration of yours placed at a broader address than the shipped one is never reached: the narrower step answers first, and it answers from the product. Nothing fails and nothing is logged — your configuration is valid, it is simply never asked for. (A record above the core is a different level and is read before it, so a wording in a client entry is not shadowed this way — but it applies to that client alone.) The outcome receipts are where this is met first. Each of the three terminal kinds ships a wording at the bare {kind} and narrower ones for a sign-in: outcome-receipt-declined ships the four addresses without ByDevice; outcome-receipt-expired ships those four plus a step at ByType:login. A receipt of a sign-in shown in a messaging app asks about {kind, login, in-channel-messenger} — and about the channel, which no shipped receipt narrows by, and about the device the sign-in was started on — so restating outcome-receipt-confirmed:Templates changes nothing there. Word it at the same addresses the product used, the two device steps included:
appsettings.json
A sign-in started on a phone or a tablet gets a receipt of its own. The button of the channel opens the app on the same device, so the receipt tells the user how to get back to the sign-in page; a sign-in started on a computer keeps the short text. The device steps are the reason the example above states ByDevice: in the configuration of the host a shipped step that names the device is asked before your step at the same address without it, so a declaration of …:in-channel-messenger:Templates alone leaves phones and tablets with the shipped Sign-in confirmed ✅ Tap Back to return to the sign-in page.. A declaration in a client entry, a tenant record or a ui_config record is a level above the core and answers whole — no shipped step is reached from there. The contract stays at the root of the kind — it narrows by the transaction type and the action type and never by the surface — and the ladder ends with a step using no slot, because no slot of a receipt is guaranteed (below). A receipt of a confirmation is a different address: the shipped file states nothing under ByType:confirmation, so there the broad {kind} declaration is the step that answers and rewording it works as written. Veriqa does not know the device a confirmation is answered on, so the device steps never apply to it. If you want the receipt to tell the user how to get back to your page, word it yourself with a caller slot whose value you pass in slot_values when you create the confirmation (below). The surfaces a receipt is addressed with are in-channel-messenger (a channel that renders emoji — the messaging apps the product ships an adapter for, and any channel declaring as much of itself), in-channel-plain (a channel that does not), email-confirm-page (the confirmation page of the Email channel), core-page (the confirmation page Veriqa serves itself) and interaction-page (the page a confirmation with no browser callback leaves the user on). Outcome receipts are plain text only. All four outcome-receipt-* kinds ship with one edition — Plain — and the general rule “an edition no step states is one the email does not carry” does not apply to them: a receipt has a single part, and not showing it means not answering the user at all. So a receipt ladder overridden with the Html edition only yields no text whatsoever — that is a broken installation rather than a different look: the render fails, naming the message kind, and no other edition is substituted for it. Escaping a receipt on its way into a page is the page’s job, so plain text takes nothing away from your wording. A receipt can name what was confirmed. The shipped contract of the three receipt kinds declares no slots, so the shipped receipts say nothing about the action. Declare caller slots in the contract of a receipt on your level — under ByType:confirmation:ByAction:{action type} to word it per action — and the receipt is filled from the slot_values the confirmation was created with, by the same names. A caller slot of a receipt states MaxLength, as it does in the question:
A value the transaction does not carry (a sign-in, a confirmation created without it) leaves the step that uses it out, and the next step answers — so the minimal step of a receipt never counts a caller slot as guaranteed, even one marked Required. A receipt answering a decision from a channel the question was never shown in gets no caller values, and neither does a receipt on the entry page of a confirmation unless InteractionPage is in ConfirmationSubjectDisplay.Surfaces. The server slots described below are filled there all the same, except outcome_at on the entry page. A receipt can also carry what the product knows about the transaction. Its contract may declare the server slots app, browser, os, region (type string, Source: ServerInitiatorContext) and outcome_at (type datetime, Source: ServerSystem); a caller value never fills them. Naming one of those five Source: Caller instead is not refused at startup, but it does not hand you the slot: app is always the product’s, and the other four are yours only on the branches where the product has no value of its own. Use a name of your own instead. app names the application: the client_id of a confirmation, the client’s display name (or its client_id) for a sign-in. browser, os and region come from the initiator context, so only a sign-in has them, and they obey the same Enabled and DisplayFields of Veriqa:InitiatorContext as the sign-in prompt. outcome_at is the moment of the decision, or the deadline of an expired transaction; the entry page words its receipts before the outcome, so there it has no value. None of these slots is guaranteed in a receipt, even one marked Guaranteed, so the minimal step cannot rest on them. The welcome message greets the user by name. The kind post-login-welcome is the text sent when Veriqa:CompletionBehavior resolves to WelcomeMessage. Its shipped contract declares one slot — name (type string, Source: ServerIdentityClaims), filled from the name claim of the identity the sign-in resolved — and its shipped ladder has two steps:
A channel that gave no name leaves the first step out, and the second answers. The message is sent as plain text and in the language of the recipient — the one the channel reported, otherwise the language of the page the sign-in started on. name is the only claim a slot is filled from as the product ships today: a slot of the same source under another name gets no value, so a step that uses it is always left out. Word the message per channel under post-login-welcome:BySurface:in-channel:ByChannel:{channel}:Templates. The levels of the two settings differ. The contract resolves from Tenant, Application and Core; the template from those plus a ui_config record. So a record selected by a request parameter can pick which of your wordings a user sees, yet can neither widen the slot list nor introduce an action type of its own.
The Tenant level. No reader for the Tenant level is registered out of the box — the integrator writes one. Without it the resolution goes to Application/Core.
The Application level is the client entry. Above the core the address is relative to the record of the level, and the record of Application is the entry of the client in Veriqa:OpenIddict:Clients: the contract of a message for one application sits at Veriqa:OpenIddict:Clients:[n]:MessageTemplates:{kind}:…:Contract. Action types of confirmations your server creates are message kinds of their own, and they are declared only above the core — in the client entry, as MessageTemplates:{action type} with a Contract and Templates. A kind visible only in the host section Veriqa:MessageTemplates is refused by POST /api/transaction/confirmation with action_type_unknown. A complete example with the request and the answer: Server-to-server confirmation API. The device_type axis refines any step. Its value is the type of the device the transaction was started on, as the initiator context states it — desktop, mobile, tablet or unknown — and its group, ByDevice, is the innermost one: it stands under whichever node the rest of the address names, the bare {kind} included, and needs no surface. A step that names the device is asked immediately before the same step without it, so a wording without ByDevice keeps answering for every device no wording was stated for. Only the receipts are asked about the device today; a transaction with no initiator context — it is switched off, or the device type is not collected — is asked about without it. The contract of a message does not narrow by the device. The channel axis refines the surface, so it never appears without surface. The product ships six values along it — both wordings of the pre-filled message, under deeplink-prefill:BySurface:in-channel:ByChannel:whatsapp and …:ByChannel:email, and the request for the phone number in Instagram and in Messenger, under phone-number-request:BySurface:in-channel:ByChannel:instagram and …:ByChannel:messenger and at the same two addresses of phone-number-request-required; the other kinds state no value along this axis, and a step without a channel answers for every channel at once. Asking a key about an axis it does not declare is an error. Wording a message for one channel therefore takes the surface as well. The request for the phone number is the plain case: phone-number-request ships Share your phone number at the bare {kind} and a wording for each of two channels — Instagram and Messenger, which adds what to do without a number in the profile (phone number) — and it is asked about with the surface in-channel and the channel of the sign-in. The request for a Required number is the kind phone-number-request-required, shipped and addressed the same way. A wording for one channel sits at Veriqa:MessageTemplates:phone-number-request:BySurface:in-channel:ByChannel:{channel}:Templates:
appsettings.json
The other channels keep their shipped text. The same address in a client entry words the request for one application. The shipped contract of the kind declares no slots, so a wording that uses one needs a contract of yours at phone-number-request:Contract; the request fills the server slots app, browser, os and region from the transaction, by the rules described for the receipts above, and never a caller slot. End the ladder with a step that uses no slot: browser, os and region may have no value, a caller slot of this kind never has one, and marking a slot Guaranteed or Required does not change that. The text is rendered as plain text and translated into the user’s language like any other wording.

Health endpoints

Readiness registers a check per configured dependency, so it reflects exactly what you wired up: no database configured means no database check. The remaining endpoints, the metrics and the traces: Observability.

Keys in the declaration catalog

The table below is built from the declaration catalog of keys: the name, the value type, the levels, the dimensions and the address on every level. The remaining keys are described in the sections above. The keys below are the ones Veriqa owns. The same catalog is open to what you ship: an extension of your own declares its keys on the core, application and ui_config levels through the same public API, and its declarations join this schema at startup — see Declare your own configuration keys. The “Default” column prints what the owner of the key declared: what the core level answers when its record states nothing. For a key that declares no such answer, the column says so outright — its defaults live in the sections above. “Declared, value not printed” stands where an answer is declared and has no text to it: the declared answer is null, or the key is declared secret — the schema of a secret key carries the fact of the answer and never its text. What an installation put into a setting is in no column of this table: what is printed here is a declaration, not a resolution.

Next steps

Channel setup

Telegram, WhatsApp, other channels and Email — end to end.

Preparing for production

What to check before shipping.