import { apiOrigin } from '../../../lib/docs-env';
import EsignVerifier from '../../../components/EsignVerifier.astro';

{/*
  STABLE ADDRESS. Do not rename this page, do not move it to another directory,
  and do not replace it with a redirect that could later lapse.

  The slug is declared once as `ESIGN_VERIFICATION_DOCS_SLUG` in
  `packages/config/src/constants/kyc.ts` and is printed inside every certificate
  of completion. Certificates are pages inside a sealed PDF that is retained for
  up to seven years and is never re-rendered, so a rename produces a dead link
  inside documents that have already been issued and handed to counterparties.
  `apps/docs/__tests__/esign-legal-page.test.ts` asserts the page resolves at
  that constant.

  The page is deliberately identifier-free: it never takes an envelope,
  document, session or organization id. It explains how to verify; it is not a
  per-document link. A per-document link inside a file that is also emailed out
  would be an enumeration surface over signed contracts.
*/}

This page explains what an electronic signature produced by this platform is, what it is not, what
rights a signer has, and how any third party can check a signed document for themselves.

The address of this page is printed inside every certificate of completion. It carries no reference
to any particular agreement. To check a document, compute its SHA-256 and submit that value as
described in [Verify a signed document](#verify-a-signed-document) below.

## What our electronic signatures are

- Electronic signatures compliant with the US ESIGN Act and UETA.
- Valid electronic signatures under eIDAS Article 25(1): admissible, and not denied legal effect for
  being electronic.
- Tamper-evident. Every completed document is sealed and hash-verifiable, and any modification is
  detectable.
- Bound to a verified identity, with the verification evidence referenced in the certificate.
- Backed by a complete, timestamped audit trail, retained and reproducible on demand.

## What our electronic signatures are not

We do not make any of the following claims, and you should not make them on our behalf.

| We do not claim | Why |
| --- | --- |
| "eIDAS Advanced Electronic Signature (AdES)" or "AdES-compliant" | Article 26(a) and 26(c) are not met with a platform key: the signature-creation data is under our sole control, not the signatory's. What we apply is an electronic seal (Article 3(25)), not an advanced signature. |
| "Qualified Electronic Signature" or "legally equivalent to a handwritten signature" in the EU | Article 25(2) applies to qualified signatures only. |
| "Digitally signed with a trusted certificate", or a green check mark in a PDF reader | That requires a certificate from a trust list we do not participate in. |
| "Notarized", "witnessed" or "certified" | No notary or witness takes part in the ceremony. |
| "Legally binding" as an unqualified absolute | Whether an agreement binds depends on the underlying contract and the signer's capacity, not on the signing platform. |
| 21 CFR Part 11 compliance | The platform is not validated against that rule. |

## The signer's rights before signing

Before a signer can sign, they are shown an electronic-records consent disclosure and must agree to
it. The disclosure is written by us, is versioned, and is hashed. The signature record stores the
version and the hash, so the exact text a signer agreed to can be identified afterward, and the
certificate of completion records that they accepted it and when. Published text is never edited: a
change of wording is a new version.

The disclosure states all seven items ESIGN section 101(c) requires:

1. The right to receive the record on paper.
2. The right to withdraw consent, and what happens if it is withdrawn.
3. Whether the consent covers this transaction only or a category of transactions. Ours covers this
   transaction only.
4. How to withdraw consent and how to update contact details.
5. How to obtain a paper copy, and any fee. There is no fee.
6. The hardware and software needed to read, sign and keep the records.
7. Notice of changes to those requirements.

Each right has a stated procedure for both windows: one for while the signing session is live, and
one for after it ends. Neither is a "contact support" dead end.

- **Withdraw consent.** Exercisable in-product for as long as the signing session is live, through a
  control on the signing page. Withdrawing costs nothing and applies no penalty. What it does depends
  on when it happens, and the product says which case a signer is in rather than leaving them to
  guess:
  - **Before signing.** The signing session ends, no electronic signature is recorded, and the
    business that sent the link is told the signer stopped using electronic records. Withdrawing
    does not request a paper copy: that is the separate right described below. A later attempt to
    sign on that same link is refused rather than quietly accepted.
  - **After signing.** The document already signed still stands, because a signature cannot be taken
    back after the fact. The withdrawal is still recorded and the business that sent the link is
    still told. The product states that plainly instead of implying the signature was undone.
  - **Once the session has ended.** The disclosure the signer read states the route for this window:
    contact the business that sent the link. The right does not lapse; it is exercised by that route
    rather than by a control on a page the signer no longer has open.
- **Request a paper copy.** The right to a paper record survives the signature. While the signing
  session is live, a control on the signing page requests one: the request is passed to the business
  that sent the link, which sends the paper copy, and a repeat request while one is still outstanding
  does not create a second obligation. Once the session has ended, the disclosure states the route
  for that window: contact the business that sent the link. There is no fee either way. Separately
  and immediately, the signer can save their own copy from the download control, and, if the
  business turned that on, the signed record is emailed to them when signing completes.

While the signing session is live, both controls are rendered by the platform on **both** delivery
surfaces: the hosted signing page and the signing experience embedded in a customer's own site. An
integrator embedding the signing experience must not hide or suppress them. Once the session has
ended neither control is rendered, on either surface: 15 U.S.C. section 7001(c)(1)(B) requires the
disclosure to state the procedure, which it does for both windows, and the entitlement is undiminished
by the absence of a button.

## Document classes we do not sign

ESIGN section 103 (15 U.S.C. section 7003) carves several classes of document out of electronic-signature
coverage. Those classes cannot be created on this platform: they are not selectable values, and the
platform stores only the class an operator actually chose from the permitted list. When a workflow
author creates a document template they pick a permitted class and acknowledge the carve-out list.

The carved-out classes are:

- Wills, codicils and testamentary trusts.
- Adoption, divorce and other family law matters.
- The Uniform Commercial Code, except Articles 2 and 2A.
- Court orders and other official court documents.
- Notices canceling a utility service.
- Notices of default, acceleration, repossession, foreclosure or eviction from a primary residence.
- Notices canceling health or life insurance benefits.
- Product recall notices affecting health or safety.
- Documents required to accompany the transport of hazardous materials.

Two boundary cases read like carve-outs and are not. Leases and sales of goods fall under UCC
Articles 2A and 2, which section 7003(a)(3) excepts back in, so a residential lease is a permitted
class. It is the eviction **notice** on a primary residence that is carved out, not the tenancy
itself.

The permitted classes are: rental or lease agreement, service or sales agreement, employment or
contractor agreement, non-disclosure or confidentiality agreement, consent or authorization,
disclosure or acknowledgment, waiver or release, and other. "Other" exists so an operator is never
forced to mis-describe a permitted agreement to get it signed.

## Verify a signed document

Verification takes a **hash**, never the file. You compute the SHA-256 of the PDF you are holding and
send that value. We never ask you to upload a signed contract in order to prove it was signed, and
there is no variant of this endpoint that accepts bytes.

### Check a document on this page

The check below runs in your browser. It hashes the file locally, asks **every** region, and reports
the combined answer, so you do not need to know which region produced the document. The file is never
uploaded.

<EsignVerifier />

The numbered steps that follow are the same check written out. They are the specification, not a
shortcut: run them yourself, or build them into your own system, and you can confirm a signature
without trusting anything on this page. That matters here, because a page that prints the word
"Verified" is exactly the claim you are trying to check.

### 1. Hash the file you are holding

In a browser or in any runtime with the Web Crypto API:

```js
const bytes = new Uint8Array(await file.arrayBuffer());
const digest = await crypto.subtle.digest('SHA-256', bytes);
const sha256 = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');
```

On the command line:

```bash
shasum -a 256 signed-agreement.pdf
```

Either way the value is 64 lowercase hexadecimal characters.

Every download of a signed file also carries the same digest as an RFC 9530 response header, so a
system that fetched the file can compare without hashing it again:

```
Repr-Digest: sha-256=:<base64 of the same 32 bytes>:
```

That header is base64, while the endpoint below takes hex. It is the same digest in two encodings,
computed over the bytes of that response.

### 2. Submit the hash

<pre><code>{`curl -X POST ${apiOrigin('us')}/v1/esign/verify \\
  -H 'content-type: application/json' \\
  -d '{"sha256":"<the 64 hex characters>"}'`}</code></pre>

The endpoint needs no API key. It answers `200` for every verification outcome, including the
negative ones. A non-JSON body is `415`, a value that is not a 64-character lowercase hex string is
`422`, and an oversized or unmeasurable body is `413`.

### 3. Read the answer

The response always carries three fields: `protocol_version`, `match` and `status`.

`match` is the stable one-bit answer. It is `true` exactly when `status` is `verified`, and it will
never gain a third value.

| `status` | `match` | What it means | What to do |
| --- | --- | --- | --- |
| `verified` | `true` | The hash resolves to a sealed document, and the seal verifies under a published key and covers that exact record. | Accept the document. |
| `not_found` | `false` | This region holds no sealed document with that hash. | Try the other region before concluding. See [One region is not the whole answer](#one-region-is-not-the-whole-answer). |
| `not_signed` | `false` | The agreement exists but has not been signed, so there is no signature to report. | Nothing is wrong with the file. There is simply no signature yet. |
| `unsealed` | `false` | The signature was recorded, but the sealed artifact was not produced. | Ask the business that sent the document to complete it. This is not a sign of tampering. |
| `unknown_key` | `false` | The seal names a key that is not in the published key set for this region. | Retry, and if it persists, contact the business that sent the document. |
| `revoked_key` | `false` | The seal verifies, but the key that produced it has been revoked. | Treat the seal as no longer trustworthy and ask the business that sent the document for a re-issued copy. |
| `bad_signature` | `false` | The signature itself does not verify under the published key. The file or the seal has been altered, or the seal was never ours. | Do not accept the document. Contact the business that sent it. |
| `claim_mismatch` | `false` | The signature verifies and the seal is ours, but its claims name a different agreement, document, organization, mode or region. | Usually the wrong record was looked up. Re-check that the hash came from the file you mean. |
| `chain_unavailable` | `false` | The signature verifies, the seal is ours, and it covers this exact document. The signing-ceremony record that the seal points to is not held in this region, so we cannot confirm that one last claim. | Do not read this as a negative result about the document. Try the other region, then ask the business that sent it for a current copy. |
| `unsupported_format` | `false` | The document was sealed in a container format this region does not support. | Retry later, or try the other region. Do not read this as a negative result about the document. |
| `verification_unavailable` | `false` | This region could not verify anything at all right now. This is a statement about us, not about your document. | **Retry rather than conclude.** Do not record a negative result from this status. |

### Forward compatibility: two rules to build against

This endpoint is permanently public and permanently unauthenticated. There is no deprecation channel
and no way to reach an integrator later, so the two rules below are published as instructions rather
than as caveats. Following them is what lets us add a status without breaking your integration.

1. **Treat an unrecognized `status` as not verified.** Read `match` for the yes-or-no answer and read
   `status` only for the explanation. A value you have never seen is never a match.
2. **Treat `seal` as optional before you treat it as a tagged union.** `seal` is absent on five of the
   eleven statuses: `not_found`, `not_signed`, `unsealed`, `unsupported_format` and
   `verification_unavailable`. Check that it is present, then branch on `seal.format`. An unfamiliar
   `format` means "this region cannot check that seal", not "this document is not genuine".

Absent means the key is not on the wire at all. It is never `null`.

### 4. Check the seal yourself, offline

You do not have to take our boolean for it. When `seal` is present it carries the seal token exactly
as we stored it, and our public keys are published as a standard JWK Set.

```
GET /.well-known/esign-keys
```

Each entry is an Ed25519 verification key with `kty: "OKP"`, `crv: "Ed25519"`, `alg: "EdDSA"`,
`use: "sig"`, the base64url public key in `x`, its `kid`, a `status` of `active`, `retired` or
`revoked`, and a `notBefore` timestamp. Retired keys keep verifying and are never removed: a seal has
to stay checkable for the whole retained life of the document it covers.

The seal token is three dot-separated parts: `<format>.<base64url payload>.<base64url signature>`.
The signature covers the UTF-8 bytes of the **first two parts joined by a dot**, format prefix
included.

```js
const res = await fetch('/v1/esign/verify', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ sha256 }),
});
const body = await res.json();

// Rule 2: optional first, tagged union second.
if (!body.seal) throw new Error(`no seal to check (status: ${body.status})`);
if (body.seal.format !== 'ed25519_detached_token_v1') {
  throw new Error('this deployment cannot check that seal format');
}

// The kid lives inside the signed claims. `payload_json` is a JSON STRING.
const claims = JSON.parse(body.seal.payload_json);

const jwks = await fetch('/.well-known/esign-keys').then((r) => r.json());
const jwk = jwks.keys.find((k) => k.kid === claims.kid);
if (!jwk) throw new Error('no published key for that kid');

const key = await crypto.subtle.importKey('jwk', jwk, { name: 'Ed25519' }, false, ['verify']);

// Verify over the TOKEN, never over a re-serialization of the parsed claims.
const [format, payloadB64, signatureB64] = body.seal.token.split('.');
const signingInput = new TextEncoder().encode(`${format}.${payloadB64}`);
const fromBase64Url = (s) =>
  Uint8Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0));

const ok = await crypto.subtle.verify(
  { name: 'Ed25519' },
  key,
  fromBase64Url(signatureB64),
  signingInput,
);
```

**Verify over `seal.token`, never over a re-serialization of the parsed claims.** The signature covers
the exact payload string inside the token. Re-encoding a parsed object can reorder its keys, and a
re-encoded payload verifies only by accident. `payload_json` is published as a JSON **string** for the
same reason: it is byte-identical to the payload the signature covers.

The claims tell you which record the seal binds: the agreement and document ids, the final hash, the
signing region and mode, the position it pins in the document's event chain, when it was sealed, and
which key signed it. A verifying signature over claims that name a different record is what the
`claim_mismatch` status reports.

### What the response tells you, and what it does not

A caller who can present a matching hash already holds, or was handed, the document. The response
therefore returns the identifiers that are readable off the file itself: the agreement and document
ids, which document this is out of how many, the final hash, the signature methods used, whether the
record is live or test, the current position of the event chain, and any attestation recorded after
sealing.

Deliberately not returned: no party names, no email addresses, no phone numbers, no document title,
and no short human-quotable agreement reference. Party details are encrypted at rest and this path
never decrypts anything.

**The organization is identifiable.** The seal binds the owning organization's identifier as one of
its signed claims, and it has to: without that binding the same signature would validate against a
different organization's agreement. Because the endpoint publishes the seal token so that the seal can
be checked independently, it publishes those claims with it. Treat the owning organization's
identifier as disclosed to anyone who holds the document. The claims deliberately carry nothing
stronger: no signer name and no contact details ride in them.

### One region is not the whole answer

The platform runs as independent regional deployments and does not copy tenant data between them. Each
region answers only from its own database. A document sealed in one region returns `not_found` from
the other, because the record is not there.

**A single region's `not_found` is not a statement about whether the document exists.** A third party
implementing this check should query every published region and report the union of the answers, and
should never report "not found" while a region has not answered. This page is region-less and the
[check above](#check-a-document-on-this-page) already does it: it asks every region listed below and
reports the combined answer.

<table>
  <thead>
    <tr>
      <th>Region</th>
      <th>Verification endpoint</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>US</td>
      <td>
        <code>{apiOrigin('us')}/v1/esign/verify</code>
      </td>
    </tr>
    <tr>
      <td>EU</td>
      <td>
        <code>{apiOrigin('eu')}/v1/esign/verify</code>
      </td>
    </tr>
  </tbody>
</table>

### Test-mode records have no legal effect

Test mode is free, and a test organization can drive the whole signing ceremony. Every response
reports `mode`, and a `mode` of `test` means the record is a synthetic test artifact with **no legal
effect whatsoever**, whatever else the response says. Do not accept a test-mode record as a signed
agreement.

Test-mode documents are also sealed under a key that the published live key set does not contain, so a
test-mode seal can never verify as a live one.

## Retention

A signed document and its audit trail are kept for the retention period configured on the workflow
that produced them, up to seven years. The certificate of completion records the retention policy that
was applied and the region the record lives in. Deleting a signed agreement early would destroy the
thing the signature exists to evidence, so the default sits at the ceiling in every region.

Once a signing-ceremony record is no longer held, the verification endpoint answers
`chain_unavailable` for the documents it covered. That is a statement about what we still hold, never
a finding about the file: the seal is still checked, and it still has to verify under a published key
and cover that exact document before you see that status at all. You can also check the seal yourself
offline, with no help from us, for as long as you keep the file.

## Where to learn more

- [Modes and regions](/developers/modes-and-regions) for how test mode and regional isolation work.
- [Compliance settings](/guides/organizations/compliance) for data residency and the sub-processor
  list for your organization.