Skip to content

Changelog

Notable changes to the platform, the public API, and the SDKs are recorded here. Entries are added as releases ship.

You can retrieve a verification’s ID and photo images over the API

Section titled “You can retrieve a verification’s ID and photo images over the API”

There is a new endpoint that returns the images a verification collected, one at a time: GET /v1/sessions/{id}/idv-image/{artifact}. It serves three values. front is the front of the identity document as the applicant submitted it. selfie is the selfie or liveness frame, when the workflow ran a face check. portrait is the photograph of the document holder, cropped out of the document image. See Evidence images.

Keys you already created can do this now, and you do not have to change anything for that to be true. The endpoint is authorized by sessions:read_report, the same scope that downloads the PDF report. We did not mint a new scope, so any key already carrying that scope can retrieve these images as of this release, and so can any team member who can already open a verification in the console. If that is not what you want for a particular key, create a replacement without the reports scope and revoke the old one: see Rotate or revoke an API key. The scope is marked sensitive in the catalog at Create an API key, and the reason it is worth reading twice is that these images are biometric personal data in most jurisdictions, which is a different category from the text the report already contained.

Verifications recorded before this release have no cropped portrait yet. The crop is produced while the document is being read, so it is not created for a verification that was already recorded, and nothing reconstructs it from stored data. Asking for one that does not exist returns 404 with the code artifact_not_available, which is a normal answer and not a fault. Treat it as “use another image”: front is present for every verification that collected a document. Distinguish it from a plain 404, which means the verification id is wrong or belongs to another mode or region.

If you specifically need a portrait for an older verification, re-verify the identity step. A re-verification reads the document again, and that run produces a crop like any other, so available_images starts listing portrait for that verification once it completes. You can do this from the verification in the console, up to 20 times per verification. It asks the applicant for their document again, so it is worth doing when you need the crop, not as a bulk backfill.

Retrieving a verification now tells you which images exist before you ask for them. GET /v1/sessions/{id} gains an idv object carrying available_images, so you can request only what is there instead of probing for it. The same object carries capture_acquisition, which records whether the applicant photographed the document or uploaded a file. Three things about that field are worth stating plainly: it is absent as null for verifications recorded before we started recording it, which is not the same as “we do not know how this one was captured”; it is declared by the applicant’s own device and can be forged, so it is advisory; and it is never an input to a verification’s outcome. It is there to inform a human, not to gate anything.

Rate limit and billing. These reads are limited to 120 requests a minute on every plan, counted separately from other reads: see Rate limits. They are not charged per call, but they are subject to the same billing state as the rest of the API, so a wallet with no balance or a subscription past dunning returns 402 or 403 rather than the image.

Custom form rules now ignore capitals and accents

Section titled “Custom form rules now ignore capitals and accents”

A rule on a custom form screen compares the applicant’s answer to the value you wrote. Every condition now ignores capital letters and accents when it does that: a rule looking for cafe matches an answer of CAFÉ, and a rule looking for alice@acme.test matches an answer of Alice@Acme.test.

Three conditions used to compare exactly and no longer do. equals, does not equal, and is one of treated urgent and URGENT as different answers, while contains, does not contain, and ends with already ignored the difference. Two rules on one question could disagree about the same answer for that reason, and they no longer can. The editor states it under the Condition list wherever it applies.

This applies to verifications already recorded, not only new ones. A form’s rules are re-run over the answers stored with it every time someone opens the verification or generates its report, so a rule that did not match when the applicant submitted can match now, and one that did match can stop. Nothing in your data was rewritten and no rule was edited.

What that changes on a recorded verification is the reason, not the outcome. A form that was routed to review stays routed to review, and one that passed stays passed. What can change is which questions are named as the trigger under Needs your review and in the report. Where a form was flagged and its rules now name nothing, the card says the question that triggered it cannot be determined from the stored record, rather than showing an empty list.

One limit worth knowing. Only an accent written as a mark over a plain letter is ignored. A letter that is its own letter is not: ø, ł, đ, and ß still do not match o, l, d, and ss, so write a rule for each spelling you want to match. Spaces are not ignored either: a stray space at either end of your value keeps a condition from ever matching, or makes a does not condition match every applicant.

Screening rules are unchanged, and nothing moved on the wire. A workflow template can carry both custom form rules and screening rules; this changed only the custom form ones, and a screening rule’s eq, contains, and in still compare exactly. No request or response shape changed and no operator value was added, so a generated client needs nothing.

A verification can now ask the applicant to sign a document

Section titled “A verification can now ask the applicant to sign a document”

There is a seventh check type, esign. It has the applicant read a document you wrote and sign it by typing their name. The signed copy is sealed as a PDF with a certificate of completion, and everyone named on it receives a copy.

You author documents under Documents in the console, next to Workflows, and publish an immutable version. A published version is never edited, which is what lets a signed document be checked years later against exactly the text that was signed. A document is filled in from the details the verification already collected, using a fixed set of variables: your organization and authorized signatory, the applicant’s identity information, and the data read off their ID document. See Documents applicants sign.

Two new webhook events. kyc.session.document_signed fires when a document is signed and the sealed copy is ready to verify. kyc.session.document_declined fires when the applicant declines, which is a normal outcome and not an error. Both carry document identifiers, never applicant personal data. Payload shapes are at Create a webhook endpoint.

In the Web SDK, @checktiv/sdk-web/esign renders the step. The signing surface is a hosted frame, so it needs no stylesheet of yours, and esignCopy translates every string in it. See Modules.

Anyone holding a signed PDF can check it themselves, with no account and no API key, by submitting its SHA-256 to a public endpoint. The address is printed inside every certificate. What these signatures are, what we do not claim about them, and what rights a signer has are published at Electronic signatures.

Pricing. E-signature is available on every plan. It is charged per check on Basic and Pro and included at no per-check charge on Max. The charge lands when the applicant submits the step, whether they signed or declined; an applicant who opens the document and never submits is not charged.

Custom form rules can compare, not just match

Section titled “Custom form rules can compare, not just match”

What this entry says about capitalization is no longer current. It records the behavior as it shipped in this release. Every condition now ignores capitalization and accents, including equals, does not equal, and is one of: see Custom form rules now ignore capitals and accents.

A rule on a custom form screen could ask three things of an answer: equals, does not equal, and is one of. It can now ask ten, though no single question offers all of them. A number or date question gains is greater than, is at least, is less than, and is at most. A text question gains contains and does not contain. An email address or phone number question also gains ends with, so a rule can match a company email domain or the last digits of a phone number.

Which conditions a question offers depends on its type, and the editor offers only those. An ordered comparison needs an order, which only a number and a date have; a substring test needs text, which a number, a date, and a choice do not have. A single choice, multiple choice, or yes/no question keeps the three conditions it always had. See Choose the right check types for the full table.

Conditions do not all treat capitalization the same way. contains, does not contain, and ends with ignore case, so a rule looking for urgent also matches an answer of URGENT. equals, does not equal, and is one of compare exactly, so the same rule written with equals does not match. Two rules on one question can disagree about case for that reason, which is easy to miss when you are reading them side by side. The editor states which behavior applies as you pick a condition.

If you read workflow templates over the API, check how your client handles op. A custom form rule’s when.op now accepts and returns seven more values: gt, gte, lt, lte, includes, not_includes, and ends_with. A client generated from an older spec may close that enum and reject a value it does not recognize, and a client that switches on op with no fallback branch will fall through on one. Regenerate against the current API reference, or add a fallback.

Custom form rules and screening rules are separate rule systems, and this changes neither one’s relationship to the other. A workflow template can carry both, and both travel in the same request body, but their operator vocabularies are independent and always have been. The substring test is includes on a custom form rule and contains on a screening rule, and they do not agree on capitalization: includes ignores case, contains matches exactly. gt, gte, lt, and lte differ too. On a custom form rule they compare numbers and YYYY-MM-DD dates; on a screening rule, numbers only. Read a rule’s operator against the rule system it belongs to rather than assuming one table covers both.

This will not surprise you spontaneously. Nothing was backfilled and no stored rule changed, so a workflow template you already read returns exactly what it returned before. A new value appears for the first time only after someone in your organization authors a rule that uses one, in the console or through the API. Writes are unaffected in either direction: eq, neq, and in behave exactly as they did.

The applicant name is now one shape everywhere: family_name (the family name or surname), given_names (an ordered array), and name_suffix for a generational suffix such as Jr. or III.

A surname of several words stays ONE family_name value: family_name: "Garcia Lopez". A middle name is simply another entry in the array: given_names: ["Jose", "Maria"]. There is no middle-name field, because there is no fixed number of given names. A person with one name sends family_name with an empty given_names array, which is a complete answer rather than a missing one.

Breaking: five fields were removed from the applicant and verification REQUEST bodies. first_name, middle_name, last_name, legal_name and full_name are no longer accepted on POST /v1/applicants, PATCH /v1/applicants/{id}, or the inline applicant object of POST /v1/sessions. Sending one returns a 422 validation_error naming the replacement and its shape.

Old field Send instead
first_name given_names, an array: given_names: ["Jose"]
middle_name another entry in the same array: given_names: ["Jose", "Maria"]
last_name family_name
legal_name and full_name family_name with given_names

Check your read path too. first_name, middle_name and last_name were also removed from the applicant RESPONSE. On the REST API a stale write is a loud 422, but a stale read is silently undefined, so a client that still reads applicant.last_name gets no error and no value.

Two fields are NEW on the applicant response, both read-only: legal_name, the verbatim name as captured from the applicant or from a scanned document, and localized_name, that name in its original script when a scanned document provides one. Neither is accepted on any request shape.

In the Web SDK, a stale name key is dropped with a console warning, not rejected. collectUserInfo().submit() puts only legalName, familyName, givenNames, nameSuffix, email, phone, dateOfBirth and address on the wire. firstName, middleName and lastName are not among them, so a JavaScript host that still passes one gets a console warning naming the discarded key. The submission still succeeds, no error is raised, and the value is never stored. TypeScript hosts get a compile error when the object is passed inline, but not when it reaches submit() through a variable or a spread. Search your integration for those three keys rather than relying on a failed request to find them.

Why one shape. A verification that runs a background check reads the family name and the given names as separate values. A whole name is never split into them, because guessing which words are the surname screens a different person. Offering a single undivided name as an alternative meant offering a shape that could not complete the checks you configured.

Breaking: a name is required in parts for background checks. In the Web SDK, collectUserInfo().describe() now reports nameComponents as 'required', 'requested' or 'optional' for the verification in front of you. Build your form from it, and never hide the family-name and given-names inputs. A submission carrying only legalName is rejected with name_components_required when the verification runs a background check.

Not affected: identity verification, custom forms, and sanctions screening, which reads a verbatim legal name directly. legalName on its own remains valid for all three. A person with one name is accepted everywhere.

describe().captureStructuredName is deprecated in favor of nameComponents and still ships unchanged for one release. Do not branch on it: it reports what the operator declared, never what the server enforces, and its default is false, so it reads false even on a workflow whose background check cannot run without the components.

The name fields also carry explicit bounds now. An empty family_name is a 422 rather than an accepted blank, and given_names has a per-entry length limit and a maximum number of entries.

Your metadata keys round-trip exactly as sent

Section titled “Your metadata keys round-trip exactly as sent”

metadata is opaque customer data, and its keys are yours. A bag sent as {"orderId": "abc"} came back as {"order_id": "abc"}, and a bag carrying both spellings lost one of them. Both are fixed: keys are returned byte-identical to what you sent. If you built a workaround for the old behavior, remove it.

The same fix restores camelCase keys on the workflow-template document object across the template endpoints, and stops BCP-47 locale keys (en-GB) being mangled.

The published OpenAPI document was structurally invalid: its own keywords were being rewritten (requestBody to request_body, securitySchemes to security_schemes) and every $ref dangled, so generated clients did not build. It is now valid and its references resolve.

Phone is normalized consistently on every applicant ingress

Section titled “Phone is normalized consistently on every applicant ingress”

The inline applicant on POST /v1/sessions now validates and normalizes phone to E.164, matching POST /v1/applicants and PATCH /v1/applicants/{id}. Send an E.164 value (+12025550199), or send a national number together with an Accept-Language header so the country can be resolved. A bare national number with no country hint is rejected rather than stored under a guessed country.

The permission scopes you grant an API key are now fewer and clearer, grouped by what they control (verification, session tokens, reports, adjudication, configuration, compliance and billing). Every scope now has a plain-language description in the console and in Create an API key, and scopes that carry extra risk (full applicant data, the full-evidence report, skipping a verification step) are called out where you grant them.

A handful of scopes were renamed or combined as part of this change, and a few unused ones were removed. If you use API keys, check the scope names you grant against the current list and re-issue any key that still references an old name; the removed scopes were not gating anything. See Choosing scopes for your key for guidance on common integration patterns.

Identity document verification — biometric face check

Section titled “Identity document verification — biometric face check”

The id_verification check now supports an optional biometric face check. Configure it per workflow template with the Face match control:

  • Selfie — a single selfie matched 1:1 against the document photo.
  • Active liveness — a guided face scan (movement-based, no flashing lights) that confirms a real, present person before matching to the document.

The biometric check is strictly opt-in and the modes are mutually exclusive; the default stays document-only. Active liveness is processed within your region, with EU streams pinned to the EU region.

Reviewers can now view the captured identity-document images (front, back, and the biometric image) on the applicant detail page. Every view is authorized and audited, images are streamed through the console with no shareable link, and a deleted image shows an “Image removed” state.

See Choose the right check types, What the applicant sees, and Biometric data.