Skip to content

fix(verification): match the API's session, retrieve and webhook shapes - #64

Merged
lena-sabadina merged 2 commits into
mainfrom
fix/verification-sync
Nov 8, 2026
Merged

lena-sabadina merged 2 commits into
mainfrom
fix/verification-sync

Conversation

@safenestdev

Copy link
Copy Markdown
Contributor

What changes for users

  • DocumentType.NATIONAL_ID (national_id) is the national ID card value the API accepts. DocumentType.ID_CARD used to send id_card, which the API rejects with a 400; it is kept as a deprecated alias that now sends national_id, so existing code starts working without changes.
  • createVerificationSession accepts chip_policy (optional | required | instead_of_photo) and returns it. It is never retried automatically: a retry after a lost response would create a second session and, on plans with a verification allowance, take a second unit. On a timeout, create a new session yourself.
  • VerificationLimitError (402, VERIFY_11014) is thrown when the monthly verification allowance is used up, with limitDetails (period, next_period_start, included_per_month, buy_credits_url). Previously this surfaced as a generic TuteliqError.
  • Session poll types follow the served response: created_at / expires_at are epoch milliseconds (were typed as strings), plus verification_mode, document_evidence, step_progress, chip_policy, capture, result.age_range, result.document_evidence and the full set of document fields (first_name, last_name, issue_date, nationality, chip_verified, national_id and more). Fields the poll never returns (barcode, document_authenticity, required face_match.distance, mrz_*) are no longer typed as present.
  • Document fields are released once: the extracted document fields, date_of_birth, failure_reasons and capture arrive only on the first authenticated poll after the session completes or fails. This is now documented on the method, the types and the README, with the verification.completed webhook as the push alternative.
  • VerificationSessionStatus.DOCUMENT_REQUIRED added. CANCELLED is deprecated: the API never returns it (a cancelled session reads as expired).
  • Retrieve types: getAgeVerification now returns AgeVerificationRetrieveResult (no age or face_confidence, which that route never returns). IdentityRetrieveResult no longer declares full_name, date_of_birth, document_type, country_code, which that route never returns.
  • New methods: getVerification(id) (GET /verify/{id}, includes the age) and getVerificationConfig({ country, region }) (which check a user in that market gets under this key, and the age threshold).
  • Webhooks: WebhookEventType gains VERIFICATION_COMPLETED, MINOR_DETECTED, CSAM_DETECTED, CSAM_CRITICAL. New WebhookPayload / WebhookEventData / VerificationWebhookData types (data.verification is optional: it is present on verification.completed from a session). Webhook.last_triggered_at and last_error are nullable.
  • verifyWebhookSignature(rawBody, header, secret, { toleranceSeconds = 300 }) verifies the X-Tuteliq-Signature-V2 header (t=<unix>,v1=<hex HMAC-SHA256 of "t.body">): constant-time comparison via Web Crypto, rejects timestamps outside the window in either direction, accepts a string or raw bytes, works in Node 18+, browsers and edge runtimes.
  • ErrorCode gains the verification codes.

Deprecated create fields

redirect_url, external_id, customer_id and metadata are marked @deprecated and no longer sent. The API's create route reads only mode, document_type and chip_policy and ignores anything else without an error, so dropping them changes nothing on the server; it only stops implying they take effect (and stops sending caller metadata that goes nowhere). The redirect URL is a dashboard verification setting. The fields stay on the input type so existing code still compiles.

Type-level changes to note for the release

Removing fields that never arrived (retrieve results, barcode / document_authenticity on the session result) and changing poll timestamps from string to number can surface compile errors in code that read them. Every such read was undefined (or a number) at runtime already.

Out of scope by design: the device-side session routes and direct multipart submit routes, which need a capture client and do not belong in a server SDK.

Bugs: failing tests first, then the fix

Reproductions run on unmodified main (one test per bug):

     × 1. DocumentType national ID is the value the API accepts 4ms
     × 2. create sends fields the API does not accept 2ms
     × 3. create cannot send chip_policy and drops it from the response 0ms
     × 4. create is retried after a timeout 3ms
     × 5. create is retried after a server error 2ms
     × 6. exhausted allowance is not surfaced as a typed error with its details 0ms
     × 7. session status lacks document_required 0ms
     × 8. webhook event types lack verification and CSAM events 0ms
     × 9. no getVerificationConfig / getVerification methods 0ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 9 ⎯⎯⎯⎯⎯⎯⎯
AssertionError: expected 'id_card' to be 'national_id' // Object.is equality
AssertionError: expected { mode: 'age', …(4) } to deeply equal { mode: 'age' }
AssertionError: expected undefined to be 'instead_of_photo' // Object.is equality
AssertionError: promise resolved "{ session_id: 's', …(4) }" instead of rejecting
AssertionError: promise resolved "{ session_id: 's', …(4) }" instead of rejecting
AssertionError: expected 'TuteliqError' to be 'VerificationLimitError' // Object.is equality
AssertionError: expected undefined to be 'document_required' // Object.is equality
AssertionError: expected [ 'incident.critical', …(4) ] to include 'verification.completed'
AssertionError: expected 'undefined' to be 'function' // Object.is equality
      Tests  9 failed (9)

Type reproduction on main: a session poll and an age retrieve exactly as the API serves them do not typecheck:

served.ts(8,9): error TS2741: Property 'distance' is missing in type '{ matched: true; confidence: number; }' but required in type 'FaceMatchResult'.
served.ts(9,30): error TS2322: Type 'null' is not assignable to type 'string[]'.
served.ts(9,53): error TS2740: Type '{ ocr_confidence: number; }' is missing the following properties from type 'DocumentDetails': name_extracted, dob_extracted, document_number, document_number_valid, and 6 more.
served.ts(12,5): error TS2322: Type 'number' is not assignable to type 'string'.
served.ts(13,5): error TS2322: Type 'number' is not assignable to type 'string'.
served.ts(16,7): error TS2739: Type '{ verification_id: string; status: VerificationRetrieveResult['status']; is_minor: false; face_matched: true; liveness_valid: true; failure_reasons: never[]; created_at: string; }' is missing the following properties from type 'VerificationRetrieveResult': age, face_confidence

After the fix, the same assertions (now in tests/verification.test.ts):

 ✓ tests/verification.test.ts > verification constants > DocumentType sends the national ID value the API accepts
 ✓ tests/verification.test.ts > verification constants > session status includes document_required
 ✓ tests/verification.test.ts > verification constants > exposes chip policy, check and document evidence values
 ✓ tests/verification.test.ts > verification constants > webhook events include verification and CSAM events
 ✓ tests/verification.test.ts > createVerificationSession > sends DocumentType.ID_CARD as national_id
 ✓ tests/verification.test.ts > createVerificationSession > sends chip_policy and returns it
 ✓ tests/verification.test.ts > createVerificationSession > does not send fields the API does not accept
 ✓ tests/verification.test.ts > createVerificationSession > is not retried after a timeout, so a lost response cannot create a second session
 ✓ tests/verification.test.ts > createVerificationSession > is not retried after a server error
 ✓ tests/verification.test.ts > createVerificationSession > surfaces an exhausted verification allowance with its details
 ✓ tests/verification.test.ts > getVerificationSession > returns the served result, including the one-time document fields
 ✓ tests/verification.test.ts > getVerificationConfig > hits GET /api/v1/verify/age/config with country and region
 ✓ tests/verification.test.ts > getVerificationConfig > sends no query string without arguments
 ✓ tests/verification.test.ts > getVerification > hits GET /api/v1/verify/{id}
 ✓ tests/verification.test.ts > getVerification > requires an ID
      Tests  34 passed (34)

plus 19 verifyWebhookSignature tests (a known vector, raw bytes, array header, modified body, wrong secret, swapped timestamp, both edges of the tolerance window, custom tolerance, malformed headers, legacy body-only signature, empty secret). The served shapes typecheck with no errors (tsc exit 0).

Checks

  • npm run build: clean
  • npx vitest run: 180 passed (146 before; 34 new in tests/verification.test.ts, existing verification tests updated where they asserted the old behaviour)

Closes #59

- DocumentType.NATIONAL_ID sends national_id, the value the API accepts;
  ID_CARD is kept as a deprecated alias that now sends the same value.
- createVerificationSession accepts and returns chip_policy, stops sending
  redirect_url, external_id, customer_id and metadata (never accepted by the
  API; now deprecated), and is never retried automatically.
- A used-up monthly verification allowance throws VerificationLimitError
  with its details.
- Session poll, retrieve and document types follow the served responses:
  epoch-millisecond timestamps, age_range, verification_mode,
  document_evidence, step_progress, chip_policy, capture, the full set of
  document fields, and the document_required status.
- New getVerification(id) and getVerificationConfig({ country, region }).
- WebhookEventType gains verification.completed, minor.detected,
  csam.detected and csam.critical; typed delivery payloads; nullable
  last_triggered_at and last_error; verifyWebhookSignature for the
  X-Tuteliq-Signature-V2 header.

@lena-sabadina lena-sabadina left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed verification and webhook changes. Merged main in and unified SpeakerStance; CI is green and the full suite passes on the combined tree.

@lena-sabadina
lena-sabadina merged commit 91a53fd into main Nov 8, 2026
6 checks passed
@lena-sabadina
lena-sabadina deleted the fix/verification-sync branch November 8, 2026 09:42
@safenestdev safenestdev mentioned this pull request Nov 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Verification: session types do not match the API, a documented document type is rejected, and webhooks are untyped

2 participants