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

# Identity-Bound Signing

> Bind a tenant-verified identity to one-click document execution via the SpitShake Identity Engine.

The **SpitShake Identity Engine** lets a tenant who has already verified a signer's identity (typically via Stripe Identity) hand that verification to SpitShake, so the signer lands on a one-tap Consent Modal instead of drawing a signature. SpitShake is stateless with respect to the identity check — it never calls Stripe itself.

## Architecture

<Steps>
  <Step title="Tenant verifies">
    The tenant (e.g. a Customs broker, a payroll processor) runs their own Stripe Identity verification. On success, Stripe returns the verified legal name and a verification session ID (`iv_…`).
  </Step>

  <Step title="Tenant mints a handoff JWT">
    The tenant server-side mints an HS256 JWT signed with the `identity_handoff_secret` on their SpitShake account. Payload includes `verified_name`, `stripe_verification_id`, and a short `exp` (15 min recommended).
  </Step>

  <Step title="Signer lands at the signing URL with the token">
    The tenant redirects the signer to `https://your-instance.com/s/:slug?t=<jwt>`.
  </Step>

  <Step title="SpitShake verifies + shows Consent Modal">
    SpitShake decodes the JWT using the tenant's configured secret, renders a legal-wording Consent Modal with the verified name interpolated, and waits for the single "Confirm & Sign" click.
  </Step>

  <Step title="Single click applies signatures to all fields">
    On click, SpitShake overrides every `signature` and `initials` field server-side with the verified name, writes the `intent.verified` audit entry, applies the per-page PDF audit footer, and dispatches the `signing.complete` webhook back to the tenant.
  </Step>
</Steps>

## Configuring a tenant for identity-bound signing

Four one-time setup steps:

1. **Set the tenant display and legal names**
   ```bash theme={null}
   PUT /api/settings/white_label
   { "white_label": { "display_name": "Acme", "legal_name": "Acme LLC" } }
   ```
   The signing page header becomes `Acme SecureSign`; the per-page PDF footer reads `Executed for Acme LLC.`

2. **Rotate (create) the handoff secret**
   ```bash theme={null}
   POST /api/settings/identity_handoff_secret/rotate
   { "password": "<admin-password>" }
   ```
   The response body contains `identity_handoff_secret` — **store this immediately in your tenant's secret store; it is not retrievable again.**

3. **Subscribe to the `signing.complete` webhook**
   Add a webhook URL on **Settings → Webhooks** and include `signing.complete` in the subscribed events.

4. **(Optional) Configure post-signature instructions**
   ```bash theme={null}
   PUT /api/settings/white_label
   {
     "white_label": {
       "post_sign_enabled": true,
       "post_sign_markdown": "**Next step:** Copy your routing info...\n- Routing: <span data-copy-value=\"123456789\">123456789</span>"
     }
   }
   ```
   Elements with a `data-copy-value` attribute render a click-to-copy button on the completion screen.

## Handoff JWT payload

| Claim                    | Type    | Required | Description                                                                      |
| ------------------------ | ------- | -------- | -------------------------------------------------------------------------------- |
| `iss`                    | string  | yes      | Account ID of the tenant (from GET `/api/settings` → `account.id`), as a string. |
| `iat`                    | integer | yes      | Issue time (unix seconds).                                                       |
| `exp`                    | integer | yes      | Expiration time. Keep short (≤ 15 min).                                          |
| `verified_name`          | string  | yes      | Legal name returned by Stripe. Applied verbatim to signature/initials fields.    |
| `stripe_verification_id` | string  | yes      | Stripe Identity session id (e.g. `iv_…`). Stamped on the PDF audit footer.       |
| `tenant_context`         | object  | no       | Free-form metadata echoed back on webhooks.                                      |

Signed with HS256 using the secret returned from `POST /api/settings/identity_handoff_secret/rotate`. SpitShake enforces `iat_leeway: 60` and `exp_leeway: 5` to handle clock skew.

## Example: minting a handoff JWT (Ruby)

```ruby theme={null}
require "jwt"

payload = {
  iss: account_id.to_s,
  iat: Time.now.to_i,
  exp: (Time.now + 15 * 60).to_i,
  verified_name: "Jane Doe",
  stripe_verification_id: "iv_1Abc2Def3Ghi",
  tenant_context: { legal_name: "Acme LLC" }
}
token = JWT.encode(payload, ENV["SPITSHAKE_HANDOFF_SECRET"], "HS256")
signer_url = "https://your-instance.com/s/#{submitter_slug}?t=#{token}"
```

## Standard (non-identity-bound) signing still works

SpitShake is verification-agnostic. If a signer lands at `/s/:slug` **without** a `t=` parameter, the standard signing flow runs unchanged — manual signature pad, no consent modal, no per-page footer, no `signing.complete` webhook. This is intentional dual-mode design: use identity handoff when the tenant has a verified identity, use manual signing when they don't.

## What lands on the PDF

When identity-bound:

1. Every `signature` and `initials` field is stamped with the verified name.
2. Each page of the signed PDF gets a footer strip: `Executed by SpitShake Identity Engine. Identity verified via Stripe ID: iv_… . Executed for {legal_name}.`
3. The appended audit trail page includes an "Authentication & Intent Record" block with the Stripe ID and one-click affirmation timestamp.

## The `signing.complete` webhook

Fired **only** when a submission was identity-bound. Payload shape:

```json theme={null}
{
  "event": "signing.complete",
  "submitter": { "id": 1234, "uuid": "...", "email": "jane@example.com", "name": "Jane Doe", "role": "Signer", "status": "completed" },
  "submission": { "id": 567, "slug": "...", "status": "completed", "template_id": "tpl_..." },
  "template":   { "id": "tpl_...", "name": "Contract" },
  "identity": {
    "verified_name": "Jane Doe",
    "stripe_verification_id": "iv_1Abc2Def3Ghi",
    "verified_name_source": "stripe_tenant_handoff",
    "completed_at": "2026-04-18T17:26:13Z"
  }
}
```

Use this to reconcile the signer against your Stripe records on your side.

## Security notes

* **Rotate immediately if leaked.** The secret grants the tenant the power to assert identity to SpitShake. Rotate via `POST /api/settings/identity_handoff_secret/rotate` — the old secret is invalidated the moment the new one is stored.
* **SpitShake never returns the stored secret.** `GET /api/settings/white_label` shows only `identity_handoff_configured: true/false`.
* **Client-supplied signatures are ignored in identity-bound mode.** The server authoritatively substitutes the JWT's `verified_name` into every signature/initials field before persistence, so a compromised client cannot forge a different name.
* **`intent_verified: true` is required** on the identity-bound submit request. Missing or false → 422.

## Related

* [`POST /api/settings/identity_handoff_secret/rotate`](/api-reference/settings/rotate-identity-handoff-secret) — rotate the secret.
* [`GET /api/settings/white_label`](/api-reference/settings/get-white-label) — read current configuration.
* [`PUT /api/settings/white_label`](/api-reference/settings/update-white-label) — update tenant branding and post-sign copy.
* [Webhooks guide](/guides/webhooks) — subscribe to `signing.complete`.
