Verificación de identidad y firma digital, en tu propia app.
Identity verification and digital signature, inside your own app.
Un paquete de Flutter que corre la captura de documento, prueba de vida y comparación biométrica dentro de tu interfaz — y entrega la firma de documentos a la app de TodoLegal, sin que tu app llegue a tocar una llave privada.
A Flutter package that runs document capture, liveness and biometric comparison inside your own UI — and hands document signing off to the TodoLegal app, so your app never touches a private key.
Pega esto en tu agente de código
Paste this into your coding agent
Un prompt autocontenido para Claude Code (o cualquier agente capaz): revisa que Flutter esté instalado, hace las preguntas justas — o ninguna, si prefieres los valores por defecto — y conecta el SDK de punta a punta: dependencia, permisos de plataforma, theming derivado de tu propio tema, y el código de integración.
A self-contained prompt for Claude Code (or any capable agent): it checks that Flutter is actually installed, asks just enough questions — or none, if you'd rather take the defaults — and wires up the SDK end to end: dependency, platform permissions, theming derived from your own app's theme, and integration code.
You are integrating the **TodoLegal Identity SDK** (`todolegal_identity`), a
Flutter package that runs identity verification and hands off document
signing, embedded inside a partner's own app. Your job is to get it fully
working in this repo — dependency, platform config, theming, and
integration code — talking to the human only as much as they want to be
talked to.
## 0. Prerequisites — don't assume the environment is ready
This prompt may be the first thing run in a repo with no Flutter tooling
set up at all. Check before touching any files, so a missing SDK produces
one clear message instead of a wall of confusing errors starting three
steps from now.
1. **Confirm this is actually a Flutter project.** Look for `pubspec.yaml`
with a `flutter: sdk: flutter` dependency. If there's no Flutter app
here at all, stop and ask the human what they're integrating into —
don't guess or scaffold one yourself.
2. **Confirm the Flutter SDK itself is installed and runnable:**
```
flutter --version
```
- **Command not found:** stop here — don't edit anything yet. Tell the
human plainly that Flutter isn't installed, point them to
https://docs.flutter.dev/get-started/install, and wait for them to
confirm it's installed (and their shell/IDE restarted) before
continuing. Installing an entire SDK on someone's machine isn't
something to do without asking first.
- **Found, but old:** this package requires Flutter `>=3.9.0` / Dart
`^3.9.2`. An older toolchain fails dependency resolution with an
opaque version-solve error, not a helpful one — flag the mismatch and
suggest `flutter upgrade` before proceeding.
3. **No shell/tool access at all** (e.g. you're a chat session with this
prompt pasted in and no way to run commands)? Present step 2 above as
something to ask the human to run themselves and paste the output back,
rather than skipping the check entirely.
Platform-specific toolchain requirements (Xcode, CocoaPods, Android SDK)
are checked later in §7, once §4 has established which platforms are
actually in scope — no point demanding a fully green `flutter doctor` for
a platform nobody asked for.
## 1. Freshness check
Define once: **the live docs** = `{{DOCS_URL}}` (a placeholder until
TodoLegal's public docs site exists — every other reference in this file
to "the live docs" means this same URL).
This prompt embeds a versioned snapshot of the SDK's public API (bottom of
this file, "API surface snapshot"). Two different confidence levels apply
to it — see the snapshot's own header before you rely on any one part of
it.
- If the live docs are a real, reachable URL: fetch them. Where they
**disagree** with the snapshot below, the live docs win — the snapshot
may be older.
- If the live docs are still the placeholder, unreachable, or you have no
fetch capability: proceed from the snapshot below. Don't block on this.
- Regardless of either source: if you can read the actual installed
package source in this project (`.dart_tool/package_config.json` →
the resolved `todolegal_identity` path, or wherever `pubspec.lock` points
it), **that beats both** for the Dart surface's exact type/field names.
Never invent a constructor parameter that isn't visibly defined
somewhere.
## 2. Non-negotiable rules
These are security invariants, not style preferences. Violating them is a
credential leak or a false "verified" status shipped to production — flag
it loudly rather than working around it, even under time pressure to get a
demo running.
1. **`POST /sdk/sessions` (mint) and `GET /sdk/sessions/{id}` (confirm) are
partner-**backend-only** calls**, authenticated with `X-Partner-Id` /
`X-Partner-Secret`. These credentials must never appear in the Flutter
app — not in Dart source, not in an `.env` bundled into the app, not in
a build flavor file, nothing that ships to a device. The app only ever
handles a short-lived `session_token`.
2. **The outcome the app receives is never authoritative.**
`OnboardingSubmitted` means the device *finished submitting* — not that
verification passed. Only the partner backend calling
`GET /sdk/sessions/{id}` and seeing `step` reach `verified` or
`certificate_issued` may flip a user to "verified" in the partner's own
system. There is no webhook — this is polling, done from the backend.
3. **`SignatureRequest.returnUri` round-trips a single-use `nonce`.** The
backend (or app, if it holds the pending request) must validate the
`nonce` on return matches the one from the original request before
trusting a `SignatureCompleted` outcome. This is what `nonceMismatch`
exists to catch.
4. **`SignatureRequest.sandboxMode` must match the `sandboxMode` used by
the `startOnboarding` call for that same session.** Mismatched modes
are a session-continuity bug, not a config nicety.
See the note at the top of the API snapshot below about confirming
production availability before going live.
## 3. Discover before asking
Before asking the human anything, read what you can:
- `pubspec.yaml` — Flutter/Dart SDK constraints, existing dependencies,
whether this is a plain app or a package itself.
- The app's theme definition (commonly `lib/main.dart`, `lib/theme.dart`,
or `lib/app_theme.dart`) — look for a `ThemeData`/`ColorScheme` to derive
SDK theming from later.
- `android/app/src/main/AndroidManifest.xml` — existing permissions,
`<queries>` blocks, intent filters, any custom URL scheme already
registered.
- `ios/Runner/Info.plist` and `ios/Runner/Runner.entitlements` — existing
usage-description strings, `CFBundleURLTypes`, NFC entitlements.
- Whether `todolegal_identity` is already a dependency, and if so, at what
version/path — don't re-derive from the snapshot below if you can read
the actually-resolved source.
Only ask the human about things you couldn't determine this way.
## 4. Confirm scope — briefly, and only if they want to
Tell the human up front: *"I'll ask a few quick questions about how you
want this wired up. If you'd rather I just pick sensible defaults and get
something working, say **go** and I'll proceed — you can adjust anything
after."*
If they say go: default to **identity verification only** (the
lowest-blast-radius recipe — no signing/backend-chaining work implied),
sandbox mode, locale inferred from the app's existing localization config
(or `es` if none), theme derived per §5, `allowSkipChipRead: true`, and a
single "Verify identity" entry point that you name clearly in your summary
so they can relocate or extend it (e.g. to the full flow) afterward.
Otherwise, ask — collapse into as few turns as makes sense:
- **Which service(s)?** Identity verification only, signing only, or both?
(See "Recipes" in the snapshot — the answer decides what `intent` the
backend mints sessions with.)
- **Where do they launch from?** Not just *which* services — *where in the
app's navigation.* E.g. "a 'Verify your identity' button on the profile
screen" or "a required step during signup, before the home screen" for
onboarding; "a 'Sign' button on a document/contract detail screen" for
signing. This decides which screen(s) you'll actually edit.
- **Locale:** `es` or `en` (SDK ships its own strings; doesn't read the
host app's localization).
- **Sandbox or live?** Default sandbox; confirm production availability
with TodoLegal before agreeing to live (see the snapshot's note below).
## 5. Theming — derive, don't interrogate
Don't ask "what's your design system." Instead:
1. Locate the host app's `ThemeData` (or `ColorScheme.fromSeed(...)`, or
hand-rolled colors) from §3's discovery.
2. Propose a mapping to `IdentitySdkThemeTokens`:
| SDK token | Derive from |
|---|---|
| `primary` | `colorScheme.primary` |
| `surface` | `colorScheme.surface` (fall back to `scaffoldBackgroundColor`) |
| `text` | `colorScheme.onSurface` (fall back to body text color) |
| `radius` | An existing button/card corner radius if one is consistently used; otherwise default `12` |
| `fontFamily` | `textTheme.bodyMedium?.fontFamily`, if the app sets a custom font |
| `logo` | Ask only this one: do they have an existing brand-mark widget (`Image.asset`/`Image.network`/`Icon`) to pass in? Null is a valid answer — it just means no branding slot is shown. |
3. Show the human the mapping in one short message before writing code —
not as a question that blocks progress, just a "here's what I derived,
say something if any of this is wrong."
## 6. Backend: mint and confirm
**Confidence note:** the Dart client surface in §8 and the API snapshot
below is verified directly against the package's own source. This
section — the mint/confirm contract, field names, and `intent` values —
comes from the docs page only; the package's internal client explicitly
does *not* implement these routes (they're partner-backend-to-TodoLegal,
never called by the app), so nothing here confirms this shape from source.
Check it against the live docs or a TodoLegal contact before writing
backend code against it, rather than trusting it at the same level as §8.
The partner backend (not the app) is responsible for:
```
POST /sdk/sessions
headers: X-Partner-Id, X-Partner-Secret
body: { partner_reference_id, intent, allow_skip_chip_read, sandbox }
→ { session_id, session_token, expires_at }
```
Hand only `session_token` to the app (however the app already gets
per-user data from its backend — an existing auth'd endpoint, typically).
```
GET /sdk/sessions/{id}
headers: X-Partner-Id, X-Partner-Secret
→ current session state, including `step`
```
Poll this (no webhook exists yet) until `step` reaches a terminal value.
Only `verified` / `certificate_issued` mean the identity is actually
verified — see rule 2.
An evidence endpoint (`GET /sdk/sessions/{id}/evidence`) also exists on
the partner-backend side; its response contract isn't verified in this
file — check the live docs before building against it.
If this repo *is* the backend (not just the Flutter app), implement these
two calls wherever the rest of this backend's outbound partner
integrations live, matching its existing HTTP client/auth conventions
rather than introducing a new pattern.
## 7. Dependency and platform setup
**Toolchain check for whichever platform(s) §4 put in scope** — do this
before editing manifests, not after a confusing build failure:
```
flutter doctor -v
```
- **Android in scope:** Android toolchain should report no issues, and
SDK licenses must be accepted (`flutter doctor --android-licenses` if
not — this one's interactive, run it yourself in a terminal rather than
having the agent attempt it).
- **iOS in scope:** Xcode installed with its command-line tools selected,
and CocoaPods installed (`pod --version`) — the Podfile step below is
impossible without it. Installing Xcode/CocoaPods is a large, often
interactive system change; surface what's missing and let the human
install it rather than attempting it automatically.
- Either way, a connected device, running emulator, or simulator is
**not** required yet — only for the real run/round-trip in §9.
**`pubspec.yaml`:** published on pub.dev — use a normal version constraint
unless told otherwise:
```yaml
dependencies:
todolegal_identity: ^0.1.0
```
**Android (`AndroidManifest.xml`):** `todolegal_identity` has no native
Android code of its own — nothing merges in from "the package's manifest"
because it has none. `CAMERA` and `NFC` do get contributed transitively by
some of its dependencies, but **`RECORD_AUDIO` is not reliably declared by
anything in the dependency tree** — verify with a build and check
`build/app/outputs/logs/manifest-merger-debug-report.txt` for a `MERGED
from` line before trusting any permission is present by inheritance.
Simplest and verified-working: declare all three explicitly yourself,
matching the package's own example app:
```xml
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.NFC"/>
<uses-feature android:name="android.hardware.nfc" android:required="false"/>
```
(`required="false"` on the feature — chip read is skippable, don't block
install on devices without NFC.)
For `isCompanionAppAvailable`/signing handoff to resolve the companion app
at all (Android 11+ package visibility), add:
```xml
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="todolegal" />
</intent>
</queries>
```
And register the app's own return scheme (whatever `returnUri` you pass
into `SignatureRequest` uses) on the main activity:
```xml
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="YOUR_SCHEME" android:host="YOUR_HOST" />
</intent-filter>
```
**iOS (`Podfile`) — do this before anything else on iOS.** The package
depends on `permission_handler` for camera/mic access, whose CocoaPods
integration **compiles every permission's request code out of the binary
by default** unless you explicitly enable the ones you need. Miss this and
the failure is nearly undiagnosable: `Permission.camera.request()` returns
`denied` instantly, no OS prompt ever appears, no entry shows up under the
device's Settings for your app, and nothing in the error output mentions
permissions at all — a fresh install looks identical to a previously-denied
one. The `Info.plist` usage-description strings below do **not** fix this;
it's a separate, compile-time gate.
Add to your app's `ios/Podfile`, inside the existing `post_install do
|installer|` block (create one if you don't have it), then run
`pod install` again (or `flutter clean` + rebuild — this only takes effect
on the *next* `pod install`, not retroactively):
```ruby
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)']
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'PERMISSION_CAMERA=1'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'PERMISSION_MICROPHONE=1'
end
end
```
**iOS (`Info.plist`)** — these strings can't be injected by the package;
Apple requires the app's own `Info.plist` to hold them, or the OS silently
denies the permission with no dialog at all:
```xml
<key>NSCameraUsageDescription</key>
<string>[app-specific reason: document capture and liveness]</string>
<key>NSMicrophoneUsageDescription</key>
<string>[app-specific reason: liveness challenge audio]</string>
<key>NFCReaderUsageDescription</key>
<string>[app-specific reason: reading the document's chip]</string>
<key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
<array>
<!-- Standard ICAO 9303 eMRTD (ePassport/eID) LDS1 applet AID. -->
<string>A0000002471001</string>
</array>
```
For `isCompanionAppAvailable`/signing handoff to resolve the companion app
at all — iOS's equivalent of the Android `<queries>` block above, and just
as easy to silently skip since the failure looks identical to "app isn't
installed" either way:
```xml
<key>LSApplicationQueriesSchemes</key>
<array>
<string>todolegal</string>
</array>
```
Plus the app's own return scheme:
```xml
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array><string>YOUR_SCHEME</string></array>
</dict>
</array>
```
**iOS entitlements + capability** — enable "Near Field Communication Tag
Reading" on the App ID in the Apple Developer portal, then add to
`Runner.entitlements` (create the file if the app doesn't have one yet,
and wire it into every build config's `CODE_SIGN_ENTITLEMENTS`):
```xml
<key>com.apple.developer.nfc.readersession.formats</key>
<array><string>TAG</string></array>
```
Only `formats` goes in entitlements — `select-identifiers` goes in
`Info.plist` (above), not here. Without either half, chip read fails
immediately even when it's marked skippable for the session — offer the
user a working scan first regardless.
**iOS deployment target:** a fresh `flutter create` ships
`IPHONEOS_DEPLOYMENT_TARGET = 13.0`. This SDK's transitive dependencies
(NFC and document-scanner plugins) need **15.0** — raise it in the Xcode
project (all three build configs) before building, or `pod install`/the
build fails with a deployment-target conflict with no guidance pointing
back to this SDK as the cause.
## 8. Integration code
At the entry point(s) identified in §4:
```dart
final outcome = await TodoLegalIdentity.startOnboarding(
context,
sessionToken: sessionToken, // from your backend
options: OnboardingOptions(
locale: 'es', // or 'en'
theme: const IdentitySdkThemeTokens(/* from §5 */),
allowSkipChipRead: true,
sandboxMode: true,
),
);
switch (outcome) {
case OnboardingSubmitted(:final sessionId, :final certificateIssued):
// Device finished submitting — NOT verified yet. Tell your backend
// to poll GET /sdk/sessions/{sessionId} for the authoritative step.
case OnboardingReusedIdentity(:final sessionId):
// User already had a verified identity and reused it.
case OnboardingCancelled():
// User backed out. Not an error.
case OnboardingFailed(:final error):
// error is OnboardingError — handle exhaustively (see snapshot below).
}
```
For signing, check availability first if you want to offer an install
prompt, then request:
```dart
if (!await TodoLegalIdentity.isCompanionAppAvailable()) {
// show an "install the TodoLegal app" prompt
}
final outcome = await TodoLegalIdentity.requestSignature(
context,
sessionToken: sessionToken,
request: SignatureRequest(
documentBytes: bytes,
documentTitle: title,
returnUri: Uri.parse('YOUR_SCHEME://YOUR_HOST'),
sandboxMode: true, // must match the onboarding session's mode
),
);
switch (outcome) {
case SignatureCompleted(:final sessionId, :final documentId):
case SignatureDeclined(:final sessionId):
case SignatureCancelled():
case SignatureCompanionAppRequired(:final sessionId):
// companion app isn't installed; session is preserved, resume later
case SignatureFailed(:final error):
// error is SignatureError — handle exhaustively
}
```
Write exhaustive `switch` statements (Dart's sealed-type exhaustiveness
check should fail the build otherwise) — don't collapse variants into a
generic catch-all just to compile faster.
## 9. Verify before declaring done
1. `flutter analyze` — clean, no new warnings introduced.
2. A real build for whichever platform(s) are in scope (`flutter build apk`
/ `flutter build ios --no-codesign` at minimum) — the iOS
usage-description keys crash at runtime, not compile time, if missing,
so a successful build doesn't prove they're present; re-check §7 by eye.
3. A `sandboxMode: true` round trip: mint a session from the backend (or a
`curl` if the backend piece isn't in scope), run the flow on-device or
in a simulator, and confirm the terminal state via
`GET /sdk/sessions/{id}`.
4. Report back to the human: what got wired up, where (exact screens/
files), what's still manual (App Store Connect / Play Console
capability toggles that can't be automated), and remind them to confirm
production availability with TodoLegal before flipping anything to
live.
---
## API surface snapshot
*Dart client surface (`TodoLegalIdentity` and everything below it on this
page) verified directly against `todolegal_identity` v0.1.0 source, most
recently re-checked 2026-09-14 (unchanged since 2026-09-11: the package
was published to pub.dev in between, with platform-setup and packaging
changes but no change to the public Dart surface). The backend
mint/confirm contract in §6 is docs-sourced only — see the confidence note
there. Prefer the live docs or the actually-installed package source over
this snapshot if they disagree (see §1).*
*Before any build sets `sandboxMode: false`, confirm with your TodoLegal
contact that production is enabled for your account — don't assume it
behaves identically to sandbox without checking.*
### `TodoLegalIdentity` (the entire public surface)
```dart
static Future<OnboardingOutcome> startOnboarding(
BuildContext context, {
required String sessionToken,
OnboardingOptions options = const OnboardingOptions(),
})
static Future<SignatureOutcome> requestSignature(
BuildContext context, {
required String sessionToken,
required SignatureRequest request,
})
static Future<bool> isCompanionAppAvailable()
```
### `OnboardingOptions`
| Field | Type | Default | Controls |
|---|---|---|---|
| `locale` | `String` | `'es'` | `es` or `en`; SDK ships its own strings |
| `theme` | `IdentitySdkThemeTokens` | defaults below | visual theming |
| `allowSkipChipRead` | `bool` | `true` | lets user skip NFC chip read; biometric comparison is never skippable |
| `sandboxMode` | `bool` | `false` | sandbox vs. live environment |
### `IdentitySdkThemeTokens`
| Field | Type | Default |
|---|---|---|
| `primary` | `Color` | `0xFF0043D3` |
| `surface` | `Color` | `0xFFFFFFFF` |
| `text` | `Color` | `0xFF1A1A1A` |
| `radius` | `double` | `12` |
| `fontFamily` | `String?` | `null` |
| `logo` | `Widget?` | `null` — a real widget slots in directly, no asset-path field |
### `OnboardingOutcome` (sealed — handle every variant)
- `OnboardingSubmitted({sessionId, certificateIssued})` — device finished
submitting; **not** proof of verification.
- `OnboardingReusedIdentity({sessionId})`
- `OnboardingCancelled()`
- `OnboardingFailed({error})` — `error` is `OnboardingError`
### `OnboardingError` (enum)
`consentDeclined` · `documentCaptureFailed` · `chipReadFailed` ·
`livenessFailed` · `comparisonFailed` · `retryBudgetExhausted` ·
`permissionDenied` · `sessionExpired` · `networkError` · `serverError` ·
`unknown`
### `SignatureRequest`
| Field | Type | Default | Notes |
|---|---|---|---|
| `documentBytes` | `Uint8List` | required | unsigned document |
| `documentTitle` | `String` | required | |
| `returnUri` | `Uri` | required | your own deep link; SDK appends `session`, `status`, `nonce` on return |
| `stepUpRequired` | `bool` | `false` | extra auth step in companion app, for high-value docs |
| `sandboxMode` | `bool` | `false` | must match the session's `startOnboarding` mode |
### `SignatureOutcome` (sealed — handle every variant)
- `SignatureCompleted({sessionId, documentId})`
- `SignatureDeclined({sessionId})`
- `SignatureCancelled()`
- `SignatureCompanionAppRequired({sessionId})` — companion app not
installed; session preserved to resume later
- `SignatureFailed({error})` — `error` is `SignatureError`
### `SignatureError` (enum)
`nonceMismatch` · `sessionExpired` · `networkError` · `serverError` ·
`unknown`
### Session lifecycle (`step`, from the partner backend's `GET /sdk/sessions/{id}`)
`created` → `consent_captured` → `document_captured` → `chip_read`
*(optional, skippable)* → `liveness_passed` → `comparison_passed` →
`verified` → `certificate_issued` → `complete`
Terminal-but-off-the-happy-path: `failed` (a step exhausted its retry
budget — see the error enums for why), `cancelled` (user abandoned, not a
retry exhaustion), `reused_identity` (user had a prior verified identity
and consented to reusing it).
### Recipes (`intent` when minting a session)
- **Identity verification only** — `intent: "onboarding"`. Mint → app runs
`startOnboarding` → backend polls to a terminal state. No document ever
issued or signed.
- **Signing without prior verification** — `intent: "one_shot_signing"`.
For a user already verified some other way. Mint → app runs
`requestSignature` directly → backend confirms.
- **Full flow** — `intent: "onboarding"`, confirm `verified` /
`certificate_issued`, then mint a *separate* session for signing (its
own `session_token`) and run `requestSignature`. Onboarding and signing
are always separate sessions, chained when the same user needs both in
one visit.
Cómo funciona
How it works
Dos actores además del usuario: tu backend, que emite y confirma sesiones, y tu app, que corre la interfaz. TodoLegal nunca recibe tus credenciales de partner directamente desde el dispositivo.
Two actors besides the user: your backend, which mints and confirms sessions, and your app, which runs the UI. TodoLegal never receives your partner credentials directly from the device.
Emite una sesión
Mint a session
POST /sdk/sessions con tus credenciales de partner. Recibes un session_token de corta duración — es lo único que el dispositivo del usuario va a ver.
POST /sdk/sessions with your partner credentials. You get back a short-lived session_token — the only thing the user's device ever sees.
Corre el flujo embebido
Run the embedded flow
Pasas el session_token a TodoLegalIdentity.startOnboarding. Todo corre dentro de tu app, con tus colores — el SDK habla directo con el backend de TodoLegal, no con el tuyo.
You pass the session_token to TodoLegalIdentity.startOnboarding. Everything runs inside your app, in your colors — the SDK talks directly to TodoLegal's backend, not yours.
Confirma el resultado autoritativo
Confirm the authoritative outcome
El resultado que recibe la app dice que el dispositivo terminó de enviar la sesión — no que la verificación fue aprobada. Confirma con GET /sdk/sessions/{id} antes de tratar al usuario como verificado.
The outcome the app receives says the device finished submitting — not that verification passed. Confirm with GET /sdk/sessions/{id} before treating the user as verified.
Pide una firma (cuando la necesites)
Request a signature (when you need one)
TodoLegalIdentity.requestSignature entrega el documento a la app de TodoLegal por un enlace de app. La llave privada y la contraseña del certificado nunca pasan por tu app.
TodoLegalIdentity.requestSignature hands the document to the TodoLegal app over an app link. The private key and certificate password never pass through your app.
Permisos requeridos
Required permissions
La cámara y el micrófono son obligatorios en ambas plataformas. El chip NFC solo si no desactivas la lectura de chip con allowSkipChipRead.
Camera and microphone are required on both platforms. NFC only if you don't disable chip reading with allowSkipChipRead.
Captura del documento y prueba de vida en el paso de verificación.
Document capture and the liveness step.
RequeridoRequiredLa prueba de vida corre como video con audio, no solo foto.
The liveness challenge runs as video with audio, not a still photo.
RequeridoRequired+ uses-feature android.hardware.nfc
Lectura del chip eMRTD del documento. Declara la feature con required="false" para no bloquear la instalación en equipos sin NFC.
Reads the document's eMRTD chip. Declare the feature with required="false" so install isn't blocked on devices without NFC.
<package android:name="com.todolegal.todolegalapp" />
Visibilidad de paquetes (Android 11+) para poder detectar si la app de TodoLegal está instalada antes de pedir una firma.
Package visibility (Android 11+) so you can detect whether the TodoLegal app is installed before requesting a signature.
Requerido para firmaRequired for signingscheme personalizado
En tu actividad principal, para recibir de vuelta al usuario cuando termina de firmar en la app de TodoLegal.
On your main activity, to receive the user back once they finish signing in the TodoLegal app.
Requerido para firmaRequired for signingCaptura del documento y prueba de vida. Sin esta clave, iOS deniega la cámara en silencio — sin diálogo de permiso.
Document capture and liveness. Without this key, iOS silently denies camera access — no permission dialog at all.
RequeridoRequiredLa prueba de vida usa video con audio.
The liveness challenge uses video with audio.
RequeridoRequired+ entitlement de lectura NFC
Lectura del chip eMRTD. Requiere además la capacidad "NFC Tag Reading" habilitada en tu App ID del Developer Portal — consulta la documentación del lector NFC del SDK para el formato exacto del entitlement.
Reads the eMRTD chip. Also requires the "NFC Tag Reading" capability enabled on your App ID in the Developer Portal — check the SDK's NFC reader docs for the exact entitlement shape.
OpcionalOptionalscheme personalizado
Tu propio esquema de retorno, registrado para recibir al usuario de vuelta tras la firma.
Your own return scheme, registered to receive the user back after signing.
Requerido para firmaRequired for signingGuía paso a paso
Step-by-step guide
De cero a la primera sesión de onboarding corriendo en tu app.
From zero to your first onboarding session running in your app.
1. Agrega la dependencia.1. Add the dependency.
dependencies: todolegal_identity: ^0.1.0
2. Declara los permisos.2. Declare the permissions.
Ver la sección Permisos requeridos arriba — cópialos en tu AndroidManifest.xml e Info.plist antes de continuar.
See Required permissions above — copy them into your AndroidManifest.xml and Info.plist before continuing.
3. Tu backend emite una sesión.3. Your backend mints a session.
curl -X POST https://api.todo.legal/sdk/sessions \ -H "X-Partner-Id: <TU_PARTNER_ID>" \ -H "X-Partner-Secret: <TU_PARTNER_SECRET>" \ -H "Content-Type: application/json" \ -d '{ "partner_reference_id": "usuario-12345", "intent": "onboarding", "allow_skip_chip_read": true, "sandbox": true }'
Responde con session_id, session_token y expires_at. Entrega el session_token al dispositivo — nunca las credenciales de partner.
Responds with session_id, session_token and expires_at. Hand the session_token to the device — never the partner credentials.
4. Tu app corre el onboarding.4. Your app runs onboarding.
final outcome = await TodoLegalIdentity.startOnboarding( context, sessionToken: sessionToken, options: const OnboardingOptions( locale: 'es', allowSkipChipRead: true, sandboxMode: true, ), ); switch (outcome) { case OnboardingSubmitted(:final sessionId): // El dispositivo terminó de enviar. Confirma con tu backend.// The device finished submitting. Confirm with your backend. break; case OnboardingReusedIdentity(:final sessionId): break; case OnboardingCancelled(): break; case OnboardingFailed(:final error): // error es OnboardingError — trátalo de forma exhaustiva// error is OnboardingError — handle it exhaustively break; }
5. Tu backend confirma el resultado.5. Your backend confirms the outcome.
curl https://api.todo.legal/sdk/sessions/<SESSION_ID> \ -H "X-Partner-Id: <TU_PARTNER_ID>" \ -H "X-Partner-Secret: <TU_PARTNER_SECRET>"
Revisa step: solo trata al usuario como verificado cuando llegue a verified o certificate_issued. No hay todavía un mecanismo de webhook — la confirmación es por consulta (polling).
Check step: only treat the user as verified once it reaches verified or certificate_issued. There's no webhook mechanism yet — confirmation is by polling.
Los 3 servicios
The 3 services
Toda la superficie visible del SDK vive en una sola clase: TodoLegalIdentity.
The SDK's entire visible surface lives in one class: TodoLegalIdentity.
startOnboarding
Corre consentimiento, captura de documento, lectura de chip opcional, prueba de vida y comparación biométrica — embebido en tu app.
Runs consent, document capture, optional chip read, liveness and biometric comparison — embedded in your app.
| Opción | Option | Qué controla | What it controls |
|---|---|---|---|
| locale | es o en. El SDK trae sus propios textos, no lee las localizaciones de tu app. | es or en. The SDK ships its own strings, it doesn't read your app's localizations. | |
| theme | Colores, radio y tipografía inyectados desde tu app, para que las pantallas del SDK se sientan parte de la tuya. | Colors, radius and typeface injected from your app, so the SDK's screens read as part of yours. | |
| allowSkipChipRead | Permite saltar la lectura NFC del chip. La comparación biométrica nunca es saltable. | Lets the user skip the NFC chip read. Biometric comparison is never skippable. | |
| sandboxMode | Corre contra el entorno sandbox de TodoLegal en vez de verificación real. | Runs against TodoLegal's sandbox environment instead of live verification. |
requestSignature
Entrega un documento a la app de TodoLegal para firma. Tu app nunca ve una llave privada ni una contraseña de certificado.
Hands a document to the TodoLegal app for signing. Your app never sees a private key or a certificate password.
| Campo | Field | Qué es | What it is |
|---|---|---|---|
| documentBytes | El documento sin firmar. Se sube bajo la sesión — nunca va en la URL del enlace de app. | The unsigned document. Uploaded under the session — never placed in the app-link URI. | |
| returnUri | Tu propio deep link de retorno. El SDK le agrega session, status y nonce. | Your own return deep link. The SDK appends session, status and nonce to it. | |
| stepUpRequired | Pide un paso extra de autenticación en la app de TodoLegal, para documentos de alto valor. | Requests an extra authentication step in the TodoLegal app, for high-value documents. | |
| sandboxMode | Debe coincidir con el modo usado en el startOnboarding de esta sesión. | Must match whatever mode the matching startOnboarding call used. |
isCompanionAppAvailable
Revisa si la app de TodoLegal está instalada, antes de pedir una firma — útil para decidir si mostrar un botón de instalación primero.
Checks whether the TodoLegal app is installed, ahead of requesting a signature — useful for deciding whether to show an install prompt first.
Combina los servicios
Mix & match
No todos los partners necesitan las tres cosas. El campo intent al emitir la sesión decide qué corre.
Not every partner needs all three. The intent field when minting a session decides what runs.
POST /sdk/sessions con intent: "onboarding"with intent: "onboarding"startOnboarding(...)GET /sdk/sessions/{id} hasta llegar a un estado terminaluntil it reaches a terminal statePara partners que solo necesitan saber "¿esta persona es quien dice ser?" — sin emitir ni firmar ningún documento.
For partners who only need to know "is this person who they say they are?" — with no document ever issued or signed.
POST /sdk/sessions con intent: "one_shot_signing"with intent: "one_shot_signing"requestSignature(...)GET /sdk/sessions/{id}Para un usuario cuya identidad ya verificaste por otro medio — se salta por completo la captura de documento y biometría.
For a user whose identity you already verified some other way — skips document capture and biometrics entirely.
POST /sdk/sessions · intent: "onboarding"· intent: "onboarding"startOnboarding(...)GET /sdk/sessions/{id} → confirma verified / certificate_issued→ confirms verified / certificate_issuedPOST /sdk/sessions nueva sesión · intent: "one_shot_signing"new session · intent: "one_shot_signing"requestSignature(...)Onboarding y firma son sesiones separadas — cada una con su propio session_token. Encadénalas cuando el mismo usuario necesite verificarse y firmar en una sola visita.
Onboarding and signing are separate sessions — each with its own session_token. Chain them when the same user needs to verify and sign in one visit.
Ciclo de vida de una sesión
Session lifecycle
El valor de step que verás al consultar GET /sdk/sessions/{id}.
The step value you'll see from GET /sdk/sessions/{id}.
Cualquier paso agotó su presupuesto de reintentos. Ver errores para la causa específica.
Any step exhausted its retry budget. See errors for the specific cause.
El usuario abandonó el flujo. Terminal — no hubo presupuesto de reintento involucrado.
The user abandoned the flow. Terminal — no retry budget was involved.
El usuario ya tenía una identidad TodoLegal verificada y aceptó reutilizarla en vez de capturar todo de nuevo.
The user already had a verified TodoLegal identity and consented to reusing it instead of capturing everything again.
Referencia de errores
Error reference
Ambos son tipos enumerados — el compilador te obliga a manejar cada valor.
Both are enum types — the compiler forces you to handle every value.