| title | Configuration |
|---|---|
| layout | default |
| parent | ESign |
| nav_order | 2 |
MisaConnect.ESign binds the Misa:ESign section to MisaESignOptions. All keys are case-sensitive in this table (the binder is, in practice, case-insensitive — but match the table for consistency with logs and validator messages).
| Key | Type | Required | Description |
|---|---|---|---|
Environment |
enum | yes | Sandbox or Production. Drives the host-validation check on BaseUrl and the default login/two-factor location (Sandbox ⇒ under /webdev/, Production ⇒ host root). |
BaseUrl |
string | yes | Absolute https:// URL — the host root. Sandbox: issued with your credentials. Production: https://esignapp.misa.vn/. Normalized to its origin, so any path you include (e.g. a trailing /webdev/) is tolerated and ignored for routing. |
AuthUnderWebdev |
bool? | no | Overrides where login/two-factor are served. null (default) derives from Environment; true forces /webdev/, false forces the host root. Does not affect refresh/resend (always /webdev/) or ESRM (always host root). |
ClientId |
string | yes | MISA client ID. |
ClientKey |
string | yes | MISA client key. |
UserName |
string | yes | MISA user. |
Password |
string | yes | MISA password. |
CredentialsMode |
enum | no | Static (default) or Dynamic. Static reads the four credentials (ClientId/ClientKey/UserName/Password) from these options. Dynamic means the four credentials are supplied per-call via a consumer-registered IMisaCredentialsAccessor, and the startup validator no longer requires the four static values (every other validation still runs). An absent key resolves to Static. |
| Key | Type | Default | Description |
|---|---|---|---|
Polling:Interval |
TimeSpan | 00:00:02 |
How often the SDK polls /Signing/status/{transactionId} after submission. |
Polling:TotalTimeout |
TimeSpan | 00:01:00 |
Maximum wall-clock time the SDK waits for the end user to confirm the sign on their MISA eSign mobile app. Throws SignTimeoutException if exceeded. |
| Key | Type | Default | Description |
|---|---|---|---|
TransportRetry:MaxAttempts |
int | 3 |
Total request attempts (incl. the first) for transient errors (429 / 5xx / connection failure / timeout). |
TransportRetry:BaseDelay |
TimeSpan | 00:00:00.200 |
Base delay for exponential backoff. |
TransportRetry:MaxDelay |
TimeSpan | 00:00:02 |
Cap for the exponential backoff. Retry-After headers on 429 are honored regardless. |
| Key | Type | Default | Description |
|---|---|---|---|
Errors:IncludeRawErrorMessage |
bool | false |
When true, MISA's raw userMsg / devMsg strings are surfaced on the thrown exception. Off by default to keep vendor messages out of consumer logs. |
| Key | Type | Default | Description |
|---|---|---|---|
Otp:DefaultResendLanguage |
string | en-US |
Language tag passed to /resend-otp-auth when the consumer's ResendOtpAsync call does not override it. MISA is the authority on supported values. |
| Key | Type | Default | Description |
|---|---|---|---|
Webhook:Mode |
enum | Both |
Polling (block-and-wait only), Webhook (non-blocking only), or Both. Refuses the disabled mode with InvalidOperationException. |
Webhook:Session:Ttl |
TimeSpan | 24:00:00 |
Lifetime of the in-memory signing-session record. Increase if MISA's retry window exceeds 24h. |
Webhook:Path |
string | /esign/webhook |
Sample-API URL path the webhook endpoint mounts at. |
Webhook:Secret |
string | null |
Sample-API shared-secret URL segment. When set, the endpoint mounts at {Path}/{Secret} and any POST to the bare {Path} returns 404. Use dotnet user-secrets to set in production. |
Webhook:AllowedIps |
string[] | null |
Sample-API CIDR allowlist for inbound webhook POSTs. Example: ["203.0.113.0/24"]. |
See specs/004-misa-esign-webhook/quickstart.md for the full webhook-mode walkthrough including hook registration, transport-layer auth, and end-to-end testing.
| Source | Use for |
|---|---|
appsettings.json |
Non-secret defaults (environment, base URL, polling/retry tuning). |
User secrets (dotnet user-secrets) |
Local development credentials & webhook secret. |
Environment variables (Misa__ESign__UserName=...) |
CI, containers, sandboxes. |
| Key Vault / Secret Manager | Production credentials. |
Never commit real credentials. Sample appsettings ship with empty placeholders.
MisaESignOptionsValidator runs at startup (ValidateOnStart) and fails fast on:
- Missing required keys (
BaseUrl,ClientId,ClientKey,UserName,Password). TheClientId/ClientKey/UserName/Passwordrequirement applies inStaticmode only — inDynamicmode these four checks are skipped because credentials are supplied per-call viaIMisaCredentialsAccessor. BaseUrlnot absolutehttps://.- Host/environment mismatch (e.g.
Productionenv with a sandbox host). - Out-of-range polling / retry values.
The BaseUrl, host/environment, polling, transport-retry, OTP, and webhook validations run in both modes.
For most swappable ports you can register your own adapter before AddMisaConnectESign, or just after — both work because AddMisaConnectESign uses TryAdd* semantics for swappable services. Example:
services.AddSingleton<ITokenCache, RedisTokenCache>();
services.AddSingleton<ICertificateSelector, PickByIssuerDnSelector>();
services.AddSingleton<IOtpProvider, MyOtpProvider>();
services.AddSingleton<IWebhookDeliveryHook, MyDeliveryHook>();
services.AddMisaConnectESign(config);To supply MISA credentials per signing call (e.g. per-person credentials), set CredentialsMode = Dynamic and register a custom IMisaCredentialsAccessor. The default OptionsMisaCredentialsAccessor is registered via TryAddSingleton, so a consumer override only wins when it is registered before AddMisaConnectESign:
services.AddSingleton<IMisaCredentialsAccessor, MyAmbientCredentialsAccessor>();
services.AddMisaConnectESign(config); // CredentialsMode: DynamicUnlike the other ports above, this one is register-before only — do not register it "just after". A plain AddSingleton after the SDK's own TryAddSingleton appends a second descriptor: the options-default accessor stays constructible and both accessors surface through IEnumerable<IMisaCredentialsAccessor>, so it is undefined which one resolves. Registering before guarantees exactly one accessor is in effect.
Lifetime contract: the accessor MUST be singleton-registrable and resolve the current call's credentials from ambient (AsyncLocal) or options state read inside Get(). It MUST NOT be scoped — the SDK resolves the accessor from singleton collaborators, so a scoped registration is a captive-dependency failure at build. Get() is read per-call regardless of lifetime, so an ambient-backed singleton is sufficient and the only safe shape.
See docs/architecture.md for the full port list.