> ## Documentation Index
> Fetch the complete documentation index at: https://docs.signsealship.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Closing Passport API: seal and verify room manifests

> Seal versioned, hash-chained Closing Passport manifests for a room and verify them publicly, with statusAtSnapshot, chainOk recompute, and coverage semantics.

A Closing Passport is a room-level, versioned, KMS-sealed evidence manifest: a canonical, deterministic snapshot of every attached document's cryptographic facts — original and sealed hashes, envelope verify codes, custody-certificate hashes — hash-chained to the versions before it and verifiable by anyone at a public link, without trusting SignSealShip.

Its honest limits, spelled out in every manifest's own coverage statement: document statuses in the manifest are a platform attestation at snapshot time, not independent evidence, and the passport says nothing about payment state, delivery confirmation, or any document that was not attached to the room.

## Seal a passport version

`POST /api/rooms/{roomCode}/passport`

Requires your partner key; the room must be yours. Each call mints the next version — a dated snapshot, never an overwrite. Repeated calls create new versions by design, capped at 20 versions per room. Rate limited with `partner-write` (60/min per key).

Sealing also builds a certificate PDF carrying the manifest's facts, seals it with the same Google Cloud KMS seal every first-party envelope gets, and archives it for the public download endpoint.

<ParamField path="roomCode" type="string" required>
  Your room's code.
</ParamField>

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://signsealship.com/api/rooms/{roomCode}/passport \
    -H "Authorization: Bearer sss_pk_your_key"
  ```

  ```javascript Node theme={null}
  const res = await fetch(
    `https://signsealship.com/api/rooms/${roomCode}/passport`,
    {
      method: "POST",
      headers: { Authorization: `Bearer ${process.env.SSS_PARTNER_KEY}` },
    },
  );
  const passport = await res.json();
  console.log(`https://signsealship.com${passport.verifyUrl}`);
  ```

  ```python Python theme={null}
  import os
  import requests

  res = requests.post(
      f"https://signsealship.com/api/rooms/{room_code}/passport",
      headers={"Authorization": f"Bearer {os.environ['SSS_PARTNER_KEY']}"},
  )
  passport = res.json()
  print(f"https://signsealship.com{passport['verifyUrl']}")
  ```
</CodeGroup>

<ResponseField name="version" type="integer">
  The new version number, starting at 1 and incrementing per room.
</ResponseField>

<ResponseField name="verifyCode" type="string">
  The public verification code for this version — 26 lowercase base32
  characters encoding 128 bits of entropy. Possession of the code is the
  authorization to verify.
</ResponseField>

<ResponseField name="verifyUrl" type="string">
  Root-relative path to the public verification page, `/v/room/{verifyCode}`.
  Prefix with `https://signsealship.com` before sharing.
</ResponseField>

<ResponseField name="manifestSha256" type="string">
  SHA-256 (lowercase hex) of the canonical manifest's UTF-8 bytes.
</ResponseField>

<ResponseField name="prevManifestSha256" type="string">
  The prior version's `manifestSha256` — the hash-chain link. `null` on
  version 1.
</ResponseField>

<ResponseField name="sealedSha256" type="string">
  SHA-256 (lowercase hex) of the sealed certificate PDF.
</ResponseField>

<ResponseField name="createdAt" type="string">
  ISO 8601 sealing time.
</ResponseField>

```json 200 OK theme={null}
{
  "version": 2,
  "verifyCode": "zyxwvutsrqponmlkjihgfedcba",
  "verifyUrl": "/v/room/zyxwvutsrqponmlkjihgfedcba",
  "manifestSha256": "9d3c...e1f0",
  "prevManifestSha256": "4b7a...c2d9",
  "sealedSha256": "71aa...0b3e",
  "createdAt": "2026-07-12T20:01:00+00:00"
}
```

Errors: `404` `{"error": "Room not found."}` for a room that is not yours, `400` `{"error": "Passport version quota reached (20 versions per room)."}` at the cap.

## Verify a passport (public)

`GET /api/verify/room/{verifyCode}`

Public — the verify code is the bearer. Rate limited with `public-read` (30/min per IP). Malformed and unknown codes return the identical generic 404, `{"verdict": "unknown"}`, so codes cannot be enumerated.

The manifest is returned parsed alongside its hash so any verifier can re-canonicalize and re-hash independently.

<ParamField path="verifyCode" type="string" required>
  The passport's 26-character verification code.
</ParamField>

<CodeGroup>
  ```bash curl theme={null}
  curl https://signsealship.com/api/verify/room/{verifyCode}
  ```

  ```javascript Node theme={null}
  const res = await fetch(
    `https://signsealship.com/api/verify/room/${verifyCode}`,
  );
  if (res.ok) {
    const check = await res.json();
    console.log(check.chainOk, check.manifest.coverage.doesNotCover);
  }
  ```

  ```python Python theme={null}
  import requests

  res = requests.get(f"https://signsealship.com/api/verify/room/{verify_code}")
  if res.ok:
      check = res.json()
      print(check["chainOk"], check["manifest"]["coverage"]["doesNotCover"])
  ```
</CodeGroup>

<ResponseField name="manifest" type="object">
  The stored canonical manifest, parsed. See [the manifest](#the-manifest)
  below for its full shape.
</ResponseField>

<ResponseField name="manifestSha256" type="string">
  SHA-256 (lowercase hex) of the canonical manifest bytes.
</ResponseField>

<ResponseField name="sealedSha256" type="string">
  SHA-256 (lowercase hex) of the sealed certificate PDF.
</ResponseField>

<ResponseField name="version" type="integer">
  This passport's version number.
</ResponseField>

<ResponseField name="prevManifestSha256" type="string">
  The prior version's manifest hash, or `null` on version 1.
</ResponseField>

<ResponseField name="chainOk" type="boolean">
  Recomputed on every call — never a stored verdict. `true` only when both
  hold: the stored manifest re-hashes to `manifestSha256`, and the chain link
  is intact — on version 1, `prevManifestSha256` is `null`; on later versions
  it equals the prior version's stored `manifestSha256`. `false` means the
  stored manifest or its lineage does not check out; do not rely on the
  passport.
</ResponseField>

<ResponseField name="branding" type="object">
  The owning partner's branding block (`brandName`, `accentColor`,
  `logoUrl`), or `null`. Rendered as attribution only — the SignSealShip
  identity and disclosures always remain.
</ResponseField>

<ResponseField name="createdAt" type="string">
  ISO 8601 sealing time of this version.
</ResponseField>

<ResponseField name="downloadUrl" type="string">
  Root-relative path to the sealed certificate PDF,
  `/api/verify/room/{verifyCode}/pdf`.
</ResponseField>

## The manifest

The manifest is canonical JSON: object keys sorted ordinally, no insignificant whitespace, invariant culture, UTF-8, hashes in lowercase hex, non-ASCII characters escaped as `\uXXXX`, and `generatedAtUtc` in ISO 8601 at second precision, always UTC. The same inputs always produce byte-identical output — that determinism is what makes `manifestSha256` meaningful.

```json Manifest structure theme={null}
{
  "coverage": {
    "covers": [
      "sha-256 hashes of document artifacts held by signsealship for the listed orders at snapshot time",
      "the room passport version chain via prevManifestSha256"
    ],
    "doesNotCover": [
      "payment or refund state",
      "delivery confirmation (no delivered-shipment artifact hash exists)",
      "documents or orders not attached to the room at snapshot time",
      "statusAtSnapshot values, which are platform attestations rather than evidence",
      "anything that happened after generatedAtUtc"
    ]
  },
  "documents": [
    {
      "custodyCertSha256": "b2c4...9e0a",
      "envelopeVerifyCode": "abcdefghijklmnopqrstuvwxyz",
      "label": "Seller deed package",
      "orderCode": "an-order-public-code",
      "originalSha256": "5f6e...1a2b",
      "sealedSha256": "8c9d...3e4f",
      "statusAtSnapshot": {
        "basis": "platform_attestation",
        "note": "platform attestation by signsealship at snapshot time; not cryptographic evidence",
        "value": "NotarizationComplete"
      }
    }
  ],
  "passport": {
    "generatedAtUtc": "2026-07-12T20:01:00Z",
    "prevManifestSha256": "4b7a...c2d9",
    "reference": "TC-88412",
    "roomCode": "3f1c9a7e5b2d8c4a6e0f9b1d3a5c7e9f2b4d6a8c",
    "roomName": "1428 Maple St — Refinance",
    "version": 2
  }
}
```

### passport

Room identity and lineage: `roomCode`, `roomName`, `reference` (or `null`), `version`, `prevManifestSha256` (or `null` on version 1), and `generatedAtUtc`.

### documents

One entry per attached order, sorted by `orderCode` (ordinal). Two rules keep it honest:

* **`statusAtSnapshot` is an attestation object, not a bare string.** It always carries `basis: "platform_attestation"`, the verbatim `note` shown above, and the order's status in `value`. The status is what SignSealShip's records said at snapshot time — it is labeled as such precisely because it is not cryptographic evidence.
* **Hash and code fields appear only when the artifact exists.** `originalSha256`, `sealedSha256`, `envelopeVerifyCode`, and `custodyCertSha256` are omitted entirely — never `null` — when there is no artifact behind them. A missing key means no evidence is claimed; a reader can never mistake an empty slot for proof.

### coverage

The manifest enumerates what its hashes actually prove (`covers`) and what they do not (`doesNotCover`), verbatim as shown above. Delivery confirmation is always listed as not covered: the platform records carrier scans but holds no delivered-shipment artifact hash.

### Verifying independently

<Steps>
  <Step title="Recompute the manifest hash">
    Serialize the manifest with ordinally sorted keys, no insignificant
    whitespace, and non-ASCII escaped as `\uXXXX`; SHA-256 the UTF-8 bytes and
    compare the lowercase hex to `manifestSha256`.
  </Step>

  <Step title="Check the chain">
    Fetch the prior version's verify record and confirm `prevManifestSha256`
    equals its `manifestSha256`. The verify endpoint does both checks for you
    on every call and reports them as `chainOk`.
  </Step>

  <Step title="Check the sealed certificate">
    Download the PDF below, SHA-256 it, and compare to `sealedSha256`. The KMS
    seal itself is a CMS signature visible in any PDF reader's signature
    panel.
  </Step>
</Steps>

## Download the sealed certificate (public)

`GET /api/verify/room/{verifyCode}/pdf`

Streams the sealed Closing Passport certificate as `application/pdf` (filename `signsealship-closing-passport-v{version}.pdf`). The verify code is the bearer; rate limited with `public-read`. An unknown code — or a passport whose archived PDF is unavailable — returns the same generic `{"verdict": "unknown"}` 404.

```bash curl theme={null}
curl -L -o closing-passport.pdf \
  https://signsealship.com/api/verify/room/{verifyCode}/pdf
```

<Check>
  Sealing a passport also emits a `room.passport_sealed` event to subscribed
  webhooks — see [webhooks](/api-reference/webhooks).
</Check>
