# HE Signs integration guide Target origin: **https://hesigns.homeequal.ai**. This is the intended address, not confirmation of deployment. The database runtime is separate from the local demo on port 4317. ## 1. Create a customer workspace An operator enrolls the company, its monthly allowance, and exact allowed application origins with `production/admin.mjs create-tenant`. The first key goes into a private output file, not a log. Use a separate workspace for sandbox and production. The database runtime now also provides `/account` and `/workspace`, with optional verified signup, private owner/team invitations, MFA, roles and optional hosted billing. See `ACCOUNTS-BILLING.md`. Google Workspace SSO and SCIM user provisioning are implemented; provider setup and acceptance remain required. See [CAPABILITIES-0.7.md](CAPABILITIES-0.7.md). Send `X-API-Key` only from your server. Keys expire after 90 days by default, support up to 365 days, and are stored as hashes. Issue replacement credentials with `POST /v2/credentials`, deploy the replacement in your application, then revoke the old key with `DELETE /v2/credentials/{id}`. A key can issue only scopes it already has. Keep the administrator key out of the application runtime. | Scope | Permission | |---|---| | `envelopes:read` | Read owned agreements, private signer links and artifacts. Treat this as sensitive access. | | `envelopes:write` | Prepare, create and void agreements; preview field placement. | | `envelopes:send` | Queue invitations, due reminders and completed copies. | | `templates:read`, `templates:write` | Read or change your versioned role templates. Preparation is a write-scope operation. | | `sessions:write` | Create and revoke embedded sessions for enrolled origins. | | `events:read` | Read agreement events and the durable cursor stream. | | `webhooks:manage` | Configure callbacks, inspect delivery jobs and request replay. | | `credentials:manage` | List metadata, issue scoped keys and revoke keys. | | `usage:read` | Read UTC monthly usage, allowance and delivery backlog. | | `exports:read` | List completed agreements and download batched proof-kit archives (1–10 agreements, up to 32 MiB of artifacts). | | `retention:manage` | Inspect retention, set/release legal holds, schedule/cancel closed-agreement deletion. Grant deliberately. | ## 2. Create an agreement Copy `sdk/server.mjs` into your server project. It has no external dependencies and requires Node 22. Use a harmless sample agreement first. Persist a random idempotency key in your own database **before** making the request. If the response is lost, retry the same body with that same key. A changed body with an existing key returns 409. The Node SDK retries one dropped connection for GET or idempotent creation only. It does not automatically repeat invitations or retry HTTP failures. Inspect status, request ID and Retry-After before retrying other operations. Python 3.10+ customers can download `/sdk/python.py` as `he_signs.py`. It uses only the standard library, verified HTTPS, no redirect forwarding, structured API errors and raw-byte webhook verification: ```python from he_signs import HESigns, verify_webhook signs = HESigns(api_key=server_side_secret) agreement = signs.create_envelope(prepared_agreement, persisted_idempotency_key) signs.invite(agreement["id"], signer=0) ``` JavaScript, Python and .NET SDKs are tested against the local HTTP API. They do not automatically retry non-idempotent actions or silently acknowledge webhook events. ```js import { readFile } from 'node:fs/promises'; import { HESigns } from './server.mjs'; const signs = new HESigns({ apiKey: process.env.HE_SIGNS_API_KEY }); const agreement = await signs.createEnvelope({ client_reference: 'your-order-identifier', document: { name: 'Service agreement.pdf', base64: (await readFile('./agreement.pdf')).toString('base64') }, routing: 'sequential', participants: [{ name: 'Alex Example', email: 'alex@example.com' }], fields: [{ signer: 0, type: 'signature', page: 1, x: 60, y: 100, width: 240, height: 55 }] }, persistedIdempotencyKey); ``` Coordinates use PDF points from the bottom-left, not screen pixels. Pages are one-based; signer indexes are zero-based. Every signer requires a signature field. Available types: `signature`, `initials`, `full_name`, `email`, `signed_date`, `date`, `text`, `checkbox`. Only date, text and checkbox accept signer-supplied values. `signed_date` records the UTC signing date. The sender must review all auto-placement suggestions before creating the agreement. Current bounds: up to 10 PDFs with 8 MiB combined source bytes and 100 pages, 1–25 signers, 200 fields and unrotated pages. Text detection supports existing text, literal anchors and explicit tags; optional local English OCR handles up to 10 scanned pages per pass. All suggestions require sender review. Reusable templates support 100 pages, 100 templates per workspace and 50 immutable versions. Choose an exact version and assign every role through `/v2/templates/{id}/prepare`, then create the returned envelope payload with a new idempotency key. For customer-controlled authorization, enroll only your **public** Ed25519 key with the service operator. Ask `/v2/envelopes/manifest` for the exact manifest, canonicalize and sign it on your own server, then supply `sender_authorization.signature` during creation. `proof.js` defines the canonical serialization. Never send a customer private key to HE Signs. Key enrollment changes require controlled operator configuration and a restart in this release; self-service customer signing-key rotation is not implemented. ## 3. Email or embed `await signs.invite(agreement.id, 0)` returns **202**, meaning durably queued. It does not mean emailed. A separate worker sends the invitation. Recipient delivery state `sent` means SMTP accepted the message, not that an inbox received it. Reminders are limited to three successful reminders, at least 24 hours apart. Ordered signing queues the next person's invitation after the previous signature commits. Completion queues a signed-copy message for each recipient. For an embedded experience, your server creates a session only after it authenticates its own user and confirms that the user may act as the selected signer: ```js const session = await signs.embeddedSession(agreement.id, { signer: 0, parent_origin: 'https://app.example.com' }); // Return only session.session_url to your authenticated browser user. ``` Then, in that browser: ```js import { mountSigning } from './browser.mjs'; const embedded = mountSigning({ container: document.getElementById('signing'), sessionUrl: session.session_url, onEvent(event) { if (event.type === 'signed') refreshStatusFromOurServer(); } }); // On navigation: embedded.destroy(); ``` Sessions expire after 15 minutes or at the agreement's expiry, whichever comes first. Renew from your authenticated server when needed. Revoke independently through `DELETE /v2/embedded-sessions/{session_id}`. Each session restricts framing to the exact enrolled origin through CSP. The browser SDK checks the sender origin **and** iframe window. `ready`, `signed`, `declined`, and `expired` events help the interface; they are not authoritative business confirmation. An expired frame explains how to reopen the session. Confirm status from your server or a verified webhook before releasing goods, changing a contract, or performing another business action. An embedded session remains a bearer capability; origin restrictions prevent unauthorized framing, not identity impersonation by someone who steals the URL. Do not log, forward, index, or place session URLs in analytics. ## 4. Receive and recover events Enroll `PUT /v2/webhooks` with `{ "url": "https://your-server.example/events/he-signs" }`. The generated signing secret is returned once. Store it in your secret manager. Existing queued events keep a snapshot of the old URL and secret when you rotate; retain the old verification secret until that backlog is drained. Disabling callbacks cancels queued jobs, though an in-flight request may finish. Callbacks contain a stable `id`, `type`, `created_at`, and `data.envelope_id`; participant events also carry `data.signer`. No signer email, document content, or private URL is included. Verify the exact raw body **before** JSON parsing: ```js import express from 'express'; import { verifyWebhook } from './server.mjs'; app.post('/events/he-signs', express.raw({type:'application/json'}), async (req,res) => { let event; try { event = verifyWebhook({ secret: process.env.HE_SIGNS_WEBHOOK_SECRET, rawBody: req.body, timestamp: req.get('HE-Signs-Timestamp'), signature: req.get('HE-Signs-Signature') }); } catch { return res.sendStatus(401); } await saveEventAndApplyBusinessUpdateOnce(event); // your durable transaction, unique event.id res.sendStatus(204); }); ``` Place this handler before a global JSON body parser. The SDK rejects timestamps outside five minutes. Synchronize server clocks. Deduplicate by event ID and acknowledge only after durable acceptance. Delivery is **at least once**: a network failure or process crash after receiver/SMTP acceptance can cause a duplicate. Webhook order is not guaranteed; query agreement status if an event arrives out of order. The worker retries up to eight attempts with increasing delays, then retains a dead job. Inspect `/v2/webhooks/deliveries` and deliberately replay a job when the underlying issue is fixed. Replay preserves the event ID and original destination snapshot. For backfill and recovery, poll `/v2/events?after=CURSOR`; save `next_cursor` only after processing that page. The stream includes pre-enrollment events. Page size is 100; follow `has_more`. `/v2/envelopes` and delivery inspection currently return the newest 100; use the event stream and known IDs for full synchronization. ## 5. Download and independently check Wait for `completed`, then call `await signs.proofKit(agreement.id)`. Store the returned ZIP in your own durable storage. It contains both PDFs, signed evidence and the offline verifier. Pin the HE Signs public key through an independently trusted channel. Customer-authorized agreements also need your independently trusted customer public key. The key bundled with the ZIP alone is not an independent trust anchor. Evidence integrity is separate from identity, legal acceptance, and trusted time. This release records private-link possession and the signing ceremony. Optional email codes add mailbox verification; configured service certificates and RFC3161 authorities add PDF seals and timestamp evidence. Provider activation and certificate trust must be verified separately. Government identity verification, QES, long-term validation, notarization and universal jurisdictional acceptance are not established. ## Errors and operations Every request returns `X-Request-ID`. Keep that ID for support, not private signing URLs or document bodies. A credential defaults to 120 API requests per minute across API processes. Honor `Retry-After` on rate-limit responses. A UTC monthly agreement limit is enforced transactionally and counts each new agreement once. These counts are metering, not a payment collection or invoicing system. Retry failed reads safely. Retry creation with the original idempotency key. Inspect queued delivery state before requesting another delivery. 401 means invalid/expired/revoked credentials; 403 means missing scope or an unenrolled origin; 409 means a state or idempotency conflict. No permissive browser CORS endpoint exposes customer keys. The interface currently uses English. A deployment can provide a licensed TrueType/OpenType font via `HE_SIGNS_PDF_FONT_FILE`; unsupported glyphs fail explicitly. Cyrillic names were tested using a local licensed system font. This is not a claim of complete Arabic shaping, RTL, CJK, accessibility, or international language coverage. ## Release 0.7 workflows See [CAPABILITIES-0.7.md](CAPABILITIES-0.7.md) for packet inputs, role policies, OCR, Google/SCIM setup, bulk manifest preparation, resumable exports, privacy workflows, regions and certificate/timestamp configuration. Download the .NET client at `/sdk/dotnet.zip` and the Node resumable export helper at `/sdk/download-export.mjs`. New scopes are `provisioning:manage` and `privacy:manage`; background exports use `exports:read`.