Skip to main content

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.

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

1

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_…).
2

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).
3

Signer lands at the signing URL with the token

The tenant redirects the signer to https://your-instance.com/s/:slug?t=<jwt>.
4

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.
5

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.

Configuring a tenant for identity-bound signing

Four one-time setup steps:
  1. Set the tenant display and legal names
    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
    POST /api/settings/identity_handoff_secret/rotate
    { "password": "<admin-password>" }
    
    The response body contains identity_handoff_secretstore 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
    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

ClaimTypeRequiredDescription
issstringyesAccount ID of the tenant (from GET /api/settingsaccount.id), as a string.
iatintegeryesIssue time (unix seconds).
expintegeryesExpiration time. Keep short (≤ 15 min).
verified_namestringyesLegal name returned by Stripe. Applied verbatim to signature/initials fields.
stripe_verification_idstringyesStripe Identity session id (e.g. iv_…). Stamped on the PDF audit footer.
tenant_contextobjectnoFree-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)

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:
{
  "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.