# InkSeal > InkSeal is a credit-based document signing platform with named sender and signer fields, reusable templates, structured value reads, and webhooks. A metered workspace spends one credit for the envelope, one for every signer who is not the sender, and one more when the envelope asks for a one-time code. Unmetered workspaces spend nothing. This file is the map for an agent working against InkSeal, and it carries no schemas on purpose. Field names, request bodies, status values, and enum members live in the OpenAPI contract, which a contract test holds in step with the running routes; read them there rather than from any prose, here or elsewhere. What is written below is what the contract cannot say on its own: how to authenticate without leaking a credential, the order the workflow runs in, and the rules to operate by. The API base URL is `https://inkseal.net/api/v1`. Every path named below is relative to it. ## Documentation - [OpenAPI contract](https://inkseal.net/api/openapi.json): The machine-readable contract, OpenAPI 3.1, served unauthenticated. Every operation, request body, response schema, error code, and webhook payload is defined here. Point a client generator or an agent at this and treat it as the source of truth. - [Documentation index](https://inkseal.net/docs/index.md): Every published document, as a markdown link list with one line each. Each entry links the markdown source; drop the .md for the rendered page. - [The whole corpus in one file](https://inkseal.net/llms-full.txt): This file, then every document listed below, then one line per operation in the contract, as one plain-text stream. One request instead of one per document, for a reader that would rather spend context than round trips. 4 documents are left out. The MCP tool reference, because the connected server supplies each available tool’s schema and description through tools/list, and the MCP server section above already covers connecting and operating rules. The long-form API reference, because it is the one document large enough to crowd out the rest, and what it holds is here twice over already: the authentication, error, rate-limit and idempotency material is in the sections above, and the endpoint list is in the contract summary at the end. The endpoint index, because the last section of the corpus is that same table, generated from the same contract. The changelog, because it records what changed and when, and every behaviour it describes as current is already in the documents above or the contract summary at the end. - [Getting started](https://inkseal.net/docs/getting-started.md): Mint a key and send one envelope with curl, in about five minutes. - [Agent quickstart](https://inkseal.net/docs/agents/quickstart.md): Wire an agent to InkSeal: how it authenticates, the draft then send order, and the rules it works under. - [Agent guardrails](https://inkseal.net/docs/agents/guardrails.md): Spend limits per key, send approval, recipient blocking, the kill switch, and which agent did what. - [MCP server](https://inkseal.net/docs/mcp.md): The hosted Model Context Protocol endpoint: URL, authentication, install lines, and every tool a connected agent gets. - [Connecting Claude and ChatGPT](https://inkseal.net/docs/agents/oauth.md): Sign an assistant in instead of giving it a key: what it may do, which workspace it acts in, and how to disconnect it. - [Agent Skill](https://inkseal.net/docs/agents/skill.md): The InkSeal skill: what it teaches an agent, and the one line that installs it for each client. - [TypeScript SDK](https://inkseal.net/docs/sdk.md): The typed client generated from the contract: every operation, idempotency keys, retries, and webhook verification. - [Command line](https://inkseal.net/docs/cli.md): One line to wire your agents up, and the whole signing workflow from a terminal or a script. - [API reference](https://inkseal.net/docs/api/reference.md): The human-readable contract: authentication, scopes, errors, every endpoint, and an end-to-end walkthrough. - [Endpoint index](https://inkseal.net/docs/api/endpoints.md): Every operation in the published contract with its method, path, scopes, and summary. - [Field detection](https://inkseal.net/docs/api/detection.md): How automatic field placement works, what a run returns, and where it stops. - [Templates](https://inkseal.net/docs/api/templates.md): Reusable roles and fields, schema introspection, sender values, field overrides, and versioning. - [Public links](https://inkseal.net/docs/api/public-links.md): Publish a template, share its public URL, and read response funnels and signed envelopes. - [Webhooks](https://inkseal.net/docs/api/webhooks.md): The event catalog, the delivery envelope, signature verification, retries, and deduplication. - [Paying for credits](https://inkseal.net/docs/api/payments.md): How a workspace buys credits today, and what an agent does when the balance runs out. - [Versioning](https://inkseal.net/docs/api/versioning.md): What may change inside v1 without notice, what may not, and how a breaking change would ship. - [Security](https://inkseal.net/docs/security.md): Key handling, scopes, the signing-link policy, document storage, and data retention. - [Changelog](https://inkseal.net/docs/changelog.md): Every shipped change to the API and the agent surface, newest first. - [InkSeal Agent Skill](https://inkseal.net/skills/inkseal/SKILL.md): The skill itself: when to reach for InkSeal, the draft then send order, the credit rule, and the checks to run before a send. - [Agent Skill: refusals](https://inkseal.net/skills/inkseal/references/errors.md): Every refusal an agent meets, with the one next action for each: stop and tell somebody, fix it, or wait and retry. - [Agent Skill: getting connected](https://inkseal.net/skills/inkseal/references/setup.md): What an agent with no key asks for, how the MCP server is added, and the REST path when there is no MCP client. ## Product - [InkSeal](https://inkseal.net/): what the product is, for a human reader. - [Pricing](https://inkseal.net/pricing): the credit packs, and what a send costs. - [API keys](https://inkseal.net/api-keys): the dashboard where a human mints a service credential and registers a webhook endpoint. It needs an interactive sign-in, so an agent holding an API key cannot reach it. ## MCP server InkSeal runs a hosted Model Context Protocol server at `https://inkseal.net/mcp`. It is this same product offered as tools: one endpoint, streamable HTTP, nothing to run locally. Both protocol eras answer there, the 2026-07-28 revision and the 2025 handshake, and `GET` and `DELETE` answer 405 because it keeps no session. Authentication is the credential described below, sent the same way, in the `Authorization` header. The tool list a client receives is filtered by that credential's scopes, so a key without `envelopes:send` is never offered the tool that sends, and a tool that is missing is a scope that was never granted rather than a fault to retry. The tools are `inkseal_get_account`, `inkseal_list_envelopes`, `inkseal_get_envelope`, `inkseal_upload_document`, `inkseal_detect_fields`, `inkseal_create_envelope`, `inkseal_update_draft`, `inkseal_preview_envelope`, `inkseal_send_envelope`, `inkseal_remind_or_void`, `inkseal_list_templates`, `inkseal_get_template_schema`, `inkseal_create_public_link`, `inkseal_list_public_links`, `inkseal_get_public_link`, `inkseal_close_public_link`, and `inkseal_list_public_link_responses`. They are shaped around the workflow rather than around the endpoints, and the order is the same one written out below: upload, place fields, draft, preview, send, track. Two rules are worth knowing before you connect. `inkseal_create_envelope` only ever creates a draft; it cannot send, whatever it is asked. And `inkseal_send_envelope` refuses without an explicit confirmation, because that one call spends the envelope's credits and puts mail in a real person's inbox. No tool returns a signing link or a signer token, exactly as no endpoint does. Installing it is one line. Claude Code: `claude mcp add --transport http inkseal https://inkseal.net/mcp --header "Authorization: Bearer $INKSEAL_API_KEY"`. Codex: `codex mcp add inkseal --url https://inkseal.net/mcp --bearer-token-env-var INKSEAL_API_KEY`. Gemini CLI: `gemini mcp add --transport http --header "Authorization: Bearer $INKSEAL_API_KEY" inkseal https://inkseal.net/mcp`. Cursor, Hermes and VS Code have no add-a-server command, so they take a configuration block instead, and Cursor also takes a one-click `cursor://` install link. Those forms, the argument and result of every tool, and the errors a tool call can return are on the [MCP server page](https://inkseal.net/docs/mcp.md). Everything the tools do, the REST API does. An agent with no MCP client loses nothing but the wiring, and the workflow below is the same one. ## Paying A send costs one credit for the envelope, one for every signer who is not the sender, and one more when the envelope asks for a one-time code. `getEnvelope` prices the draft and returns the total with its lines, so quote a send from the read-back rather than working it out. Public link creation is free; each confirmed response spends the link’s quoted price on a metered workspace. Preparing is free: uploading, detecting fields, drafting, previewing, and reading anything back cost nothing, and the charge happens once, at the send. A metered workspace whose balance cannot cover the envelope refuses `sendEnvelope` with `insufficient_credits` before anything is mailed, and the details carry what the envelope needs and what the workspace has. An unmetered workspace is never charged and never refused. An agent cannot type a card number, so the fiat path ends with a human. Call `createCreditCheckout` (`POST /credits/checkout`) with a pack id, hand the returned URL to the person you are working for, and stop there. The URL is a Stripe payment page, it expires, and the balance moves only once the payment clears, arriving as an ordinary purchase row in `listCreditTransactions`. Do not poll `getCreditBalance` waiting for a card to be typed. That call needs `billing:manage` on top of `account:read`, and only a workspace owner can grant it, so a key without it should report the shortfall and let its human do the rest. The packs are `pack_5` (5 credits, $9), `pack_25` (25 credits, $39), `pack_50` (50 credits, $69), `pack_100` (100 credits, $119) and `pack_500` (500 credits, $499). Read the current balance and credit policy with `getAccountSummary` (`GET /account/summary`) before you quote a number, and name the pack that covers the work rather than asking for "some credits". ## Authentication Authenticate with `Authorization: Bearer `. Keys have prefix `isk_` plus 40 base62 characters (44 total), appear once at creation, and are stored only as hashes. Redacted example: `isk_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`. Keys stay bound to one workspace. No parameter widens access, and granting scopes you lack returns `scope_escalation`. `createAuthorizedSender` only requests enrollment: no mail goes out until an owner or admin approves in the dashboard. `updateAccountSettings` cannot change keyword approvals, even with `settings:write`; valid writes, including no-ops, return `forbidden`. Ask the owner to change it in the dashboard; `getAccountSettings` reads the effective flag. There are exactly eleven scopes: `documents:read`, `documents:write`, `envelopes:read`, `envelopes:write`, `envelopes:send`, `templates:read`, `templates:write`, `webhooks:manage`, `account:read`, `settings:write`, `billing:manage`. Operation `security` arrays require every listed scope (AND). Missing grants return `insufficient_scope` with required and missing scopes in the error details. `rotateCredential` mints a second secret on the same credential and returns it once. The outgoing secret keeps authenticating for a 24 hour grace window, so a deployment can roll over with both in use, and then it stops. Rotating again ends any predecessor's window immediately. `deleteCredential` has no grace period at all, which is what makes it the answer to a leak. ### Never disclose a credential An API key and a signer signing token are bearer credentials: whoever holds the bytes holds the access. **Never print, log, echo, or transmit an API key or a signer signing token.** Not into a chat transcript, not into terminal output, not into a commit or a file that will be committed, not into an error report, a bug ticket, a test fixture, a screenshot, or a message to another agent or service. Read a key from the environment, put it in the `Authorization` header, and let it go no further. Never write one into an example, including an example that is only meant to illustrate the shape. If a key does reach somewhere it should not, treat it as leaked: revoke it with `deleteCredential` and mint a replacement. Do not answer a leak with a rotation, because rotation deliberately leaves the exposed secret working for 24 hours. A signer signing token is the credential carried in a signing link. It authenticates one specific signer and lets whoever holds it sign on that person's behalf, so it is exactly as sensitive as an API key. The API never returns raw signer tokens and no workflow below needs one; if one turns up anyway, in an email body, a log line, a support ticket, or a browser URL, do not copy it, do not repeat it back, and do not store it. ## Attachment to signing Document sources may be PDF, supported Office formats, plain text, Markdown, PNG, JPEG, WebP, or self-contained HTML with the content type text/html and the extension .html or .htm. HTML is capped at 10 MB. It may use inline CSS, empty URL attributes, bare fragments, and data URLs whose media type is image/png, image/jpeg, image/webp, or image/gif. HTML is rejected without alteration at the first forbidden construct. The forbidden list is: script elements anywhere, including SVG; on-prefixed event attributes; javascript: and vbscript: URLs; iframe, frame, frameset, object, embed, applet, base, form, link, meta with http-equiv, video, audio, track, and portal elements; external, relative, or scheme-relative values in src, href, srcset, poster, background, xlink:href, data, formaction, or ping; CSS @import, @font-face, any url() that is not an allowed raster data URL, and image-set() with an external member in either style elements or style attributes; and every other data URL type, including SVG images and fonts. Chromium also disables JavaScript, runs offline, and aborts non-data and non-about requests before rendering. Nine steps, in order. Each names its operation so the request and response shapes can be looked up in the contract. Steps 3, 5, and 7 return before their work has finished, so each says what to wait for. 1. **Create the document.** `createDocument` (`POST /documents`), with the file name, content type, and size. It answers with a document id and a short-lived signed upload URL. Requires `documents:write`. 2. **Upload the bytes.** PUT the file to that upload URL with the content type you declared. This is a direct storage upload, not an InkSeal API call: it carries no `Authorization` header. If the URL has expired, go back to step 1 rather than reusing it. 3. **Confirm the upload.** `confirmDocumentUpload` (`POST /documents/{documentId}/confirm-upload`). The server checks that the object exists, checks its real size, validates its magic bytes against the declared content type, and queues normalization into a signable PDF. It returns while that work is still running. 4. **Wait for the document to be ready.** Poll `getDocument` (`GET /documents/{documentId}`) until its status leaves processing, or subscribe to `document.ready` and `document.failed`. Wait for a terminal state before going on. A document that failed cannot be repaired: re-upload it. Building an envelope on a document that is not ready answers `document_not_ready`, and reading its bytes or page images early answers `document_processing`. 5. **Place the fields.** Either detect them, with `startDetectionRun` (`POST /documents/{documentId}/detect`), then poll `getDetectionRun` (`GET /documents/{documentId}/detect/{runId}`) until it leaves processing or wait for `detection.completed` or `detection.failed`; or skip detection and place fields yourself. A completed run includes party, fill-hint, assignment-confidence, and suggested-name metadata. Use those hints to map signer roles, sender-owned fields, and stable field names. `createTemplateFromDetection` (`POST /templates/from-detection`) can turn the completed run into a draft template and reports low-confidence or unassigned fields for review. 6. **Assemble the envelope.** `createEnvelope` (`POST /envelopes`) accepts exactly one source. The ad hoc branch takes a ready unused document id, signers, and fields. Name fields that an integration must read later, and assign each field to the sender or one signer. The template branch takes a template id, recipients keyed by role, sender values, and optional field overrides. Use `listTemplates` (`GET /templates`) and `getTemplateSchema` (`GET /templates/{templateId}/schema`) to collect the required roles and values first. The server snapshots the current template version, so later template edits do not change existing envelopes. A direct-use document can back one ad hoc envelope. A template document can back many template-derived envelopes. 7. **Send it.** `sendEnvelope` (`POST /envelopes/{envelopeId}/send`). Requires `envelopes:send`. It validates the persisted envelope and recomputes readiness before changing state. Missing or invalid required sender values answer `envelope_not_ready` with field-level issues. A template envelope created with its send flag enabled already has durable send intent, so do not call this endpoint again. Metered workspaces spend the envelope's credits in the queued job, the same total `getEnvelope` quoted on the draft. Unmetered workspaces record zero-cost usage instead. 8. **Watch for the outcome and read values.** Poll `getEnvelope` (`GET /envelopes/{envelopeId}`) until its status is terminal, or subscribe. The detail response includes current readiness and a map of every named field value. `listEnvelopeFields` (`GET /envelopes/{envelopeId}/fields`) returns assignment, geometry, stored values, validation, fill attribution, and signature metadata. `signer.signed` carries that signer's named values, while `envelope.completed` carries the final named values for the envelope. Neither webhook contains signature bytes or URLs. Watch for `envelope.send_failed` as well as `envelope.sent`; a failed send returns the envelope to draft. 9. **Collect the result.** `getEnvelopeDocument` (`GET /envelopes/{envelopeId}/document`) streams the completed PDF, carries its SHA-256 in a response header, and requires `documents:read`. `getEnvelopeAuditTrail` (`GET /envelopes/{envelopeId}/audit-trail`) returns the signing evidence in pages of 20 by default and requires both `envelopes:read` and `account:read`. `getEnvelopeFieldSignature` (`GET /envelopes/{envelopeId}/fields/{fieldId}/signature`) retrieves one signature image through a credential-authenticated route and requires `envelopes:read`. The field id in its path is the `id` of a field returned by `listEnvelopeFields`. Asking for the document before the envelope is completed answers `not_completed`. `getEnvelopeEvidence` (`GET /envelopes/{envelopeId}/evidence`) streams one ZIP holding the attested completion evidence, the full audit trail and a manifest of every file's SHA-256, lists missing evidence as gaps, and requires `documents:read`, `envelopes:read` and `account:read`. Where public links are enabled, `createPublicLink` publishes a template at a shareable public URL. Each respondent fills and signs a separate document and proves their email. Each confirmed response costs the returned price on a metered workspace; tell the human that price before creating one. Reads need `templates:read`; creation and mutations need `envelopes:send`. Service credentials requesting a countersignature must name an active workspace member by email. Use `getPublicLink` for the funnel and `listPublicLinkResponses` for submitted responses and envelope ids. The public URL authenticates nobody; respondent session credentials are never returned. ## Operating rules ### Working on someone's behalf - **Never ask a human for a signing link, and never offer to produce one.** No endpoint and no tool returns a signer token or a signing link; there is nothing to ask for. Signers receive their own link by email when the envelope is sent, and that link is a bearer credential for one named person's signature. If a human pastes one at you anyway, do not use it, store it, or repeat it back. - **Read the envelope back before you send it.** `getEnvelope` (`GET /envelopes/{envelopeId}`) returns the recipients, the fields, and the current readiness. When the task you were given did not name the recipients explicitly, put that read-back in front of your human, with what the send will cost, and wait for an answer. A send cannot be recalled: `voidEnvelope` stops the signing, it does not unsend the mail. - **Spend deliberately.** An envelope costs one credit plus one per signer on a metered workspace, so adding a signer costs more. Read the price off the draft, say what a run will cost before it starts, and if `insufficient_credits` comes back, stop and follow the paying section above rather than looping. ### Retries and idempotency - Mutations that declare an `Idempotency-Key` request header in the OpenAPI contract honor it, and every GET ignores it. Use one on document creation and confirmation, detection, envelope creation and send, public link creation, template creation from scratch, from a detection run, or from a draft envelope, reminders, inbound actions, credential changes, webhook mutations, account settings, authorized sender enrollment, and the credit checkout. Pick the value before the first attempt so a retry can reuse it. - A key is scoped to the workspace and the credential that used it, so the same string from another credential is a different request and can never replay into someone else's answer. - The first use records a fingerprint of the method, the path, and the canonicalized body. A later request with the same key and the same fingerprint replays the original status and body verbatim, marked `Idempotent-Replay: true`, and that includes a deterministic 4xx: a request that failed validation replays the same failure. The same key with a different method, path, or body answers `idempotency_key_reuse` and does not run the handler, which means your client reused a key, not that the server is confused. A concurrent call while the first attempt is still running answers `idempotency_in_progress` with `Retry-After`. - A 5xx, a 429, and any non-JSON answer are not recorded, so retrying that key runs the work again rather than replaying a wrong answer. - **Never blindly retry `sendEnvelope`.** It is the one call that spends money and mails real people. Always give it a key, and on a timeout or a dropped connection retry with the same key: that either replays the original answer or runs the send exactly once. If it was sent without a key, do not retry at all; call `getEnvelope` and find out what happened first. The same care applies to `sendEnvelopeReminder` and `approveInboundRequest`, which also mail real people. ### Rate limits - Limits are per credential, per bucket, in a fixed 60 second window: reads 100 per 60s, covering every GET; writes 30 per 60s, covering the mutations; sends 10 per 60s, covering `sendEnvelope`, `sendEnvelopeReminder`, and `approveInboundRequest`. Detection runs carry their own separate throttle and daily budget. - The `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and `Retry-After` headers appear **only on a refusal**. A successful response carries none of them, so a client cannot read its remaining budget without being refused first. Do not write a client that reads headers and paces itself by them. Pace against the published ceilings, catch the refusal, sleep for `Retry-After`, then retry. - A refusal happens before the handler runs and changes nothing, so retrying a refused request is always safe. That covers `rate_limited` and also `service_unavailable`, which is what a request gets when the rate-limit store itself is unreachable and the server refuses to fail open. ### Webhook verification and deduplication - Verify every delivery before acting on it, and read the raw request body as bytes before parsing. Re-serializing parsed JSON changes the bytes and breaks the signature. - The signed string is the `X-InkSeal-Timestamp` header value, then a single ASCII period, then the raw body. Compute HMAC-SHA256 over it with the subscription secret used as UTF-8 bytes exactly as it was returned, render the digest as lowercase hex, strip the `v1=` prefix from `X-InkSeal-Signature`, and compare the two **in constant time** (`crypto.timingSafeEqual` in Node, `hmac.compare_digest` in Python). A plain equality check leaks the correct signature one byte at a time. Reject a delivery whose timestamp is more than about five minutes from your own clock, which is what stops an old capture being replayed at you. On mismatch, discard the delivery and do not answer 2xx. - Deliveries are at-least-once. The event id is derived from the transition rather than the clock, so every automatic retry and every manual retry of an event carries the identical id. Deduplicate on the event id and nothing else. Record the id before doing the work, not after, so a crash mid-handler does not become a double-handled event. Two subscriptions receiving the same transition get two different ids, which is correct: they are two independent deliveries. - Acknowledge with a 2xx as soon as the event is stored and do your own work afterwards. Anything that is not a 2xx, including a redirect and a timeout, counts as a failure and is retried on a backoff until the attempts run out. ### Recovering from errors Branch on `error.code`, never on the status or the message: one status carries many distinct conditions, and messages are reworded freely. An unrecognized code is a generic failure at its status class, not a crash. - `validation_error`, `invalid_body`: the request is wrong. Read the details, fix the request, resend. Resending it unchanged never helps. - `insufficient_credits`: a metered workspace has no credit to spend. Stop. Call `createCreditCheckout` (`POST /credits/checkout`) with a pack id, hand the returned URL to the human you are working for, and wait for them; an agent cannot complete a card payment, and polling the balance in a loop will not make one happen. That call needs `billing:manage`, which not every key holds, so a key without it should simply report the shortfall. An unmetered workspace never returns this code. - `invalid_state`, `invalid_status`, `envelope_exists`, `document_in_use`, `not_completed`: the resource is not in the state the call needs. Re-read the resource, decide from what you find, and do not retry blindly. - `envelope_not_ready`, `invalid_value`, `missing_role`, `unknown_role`, `unknown_value`, `invalid_override`: fix the named readiness, value, role, or override problem before sending the request again. - `version_conflict`: a template changed after it was read. Fetch its current version, reconcile the full replacement, and retry with a new idempotency key if the operation accepts one. - `document_processing`, `document_not_ready`, `pages_not_rendered`: not ready yet. Keep polling with backoff until the state is terminal, and give the poll a deadline. - `rate_limited`, `service_unavailable`: sleep for `Retry-After` and retry the same request. - `unauthorized`: the credential is malformed, unknown, revoked, expired, or past its rotation grace window. These are deliberately indistinguishable from outside, so do not retry with the same key; a human has to issue a new one. - `insufficient_scope`: the credential lacks a scope the operation needs, and the details name which. Scopes cannot be widened, so a human has to mint a credential that holds it. - `credential_limit_exceeded`: this key has its own spend ceiling and has reached it. Nothing was sent and no credit was spent. The details carry the limit, the window, and when it resets. This is a limit a human set on this key by design, so stop and tell them; waiting out the window is only worth it if the task genuinely runs that long. - `recipient_denied`: the workspace has told InkSeal that no agent may email that address or domain. The details name each refused recipient. Report it and stop; another spelling of the same address will not get through. - `approval_required`: this workspace requires a human to approve sends made by a credential, so `createEnvelope` cannot also send. Create the envelope without the send flag, then call `sendEnvelope`, which answers 202 with a status of pending_approval and an approval id. That is not a failure: nothing was sent, no credit was spent, the human has been emailed, and the envelope is still a draft. Report the wait, then poll `getEnvelope` until the draft leaves draft. Calling `sendEnvelope` again neither sends it nor emails a second time. ### Forward compatibility - Ignore response fields you do not recognize. New ones appear inside v1 without notice. - Treat an unknown enum value as inert, not as an error. New status values, failure reasons, and error codes ship inside v1. - Treat an unknown webhook event type as inert. A subscription created with a null event list receives every future type automatically, and a consumer that throws on one starts failing deliveries the day a new type ships.