API reference

Signature API

Send a document for signature and get back an executed copy plus an audit trail. Three rails — REST, MCP, and keyless x402 — one meter, and the same validation on every one of them.

Quickstart

Send an envelope, then poll. The send is synchronous: by the time it returns, the links are live and the first group has been invited.

curl -X POST https://sign.ounie.com/api/envelopes \
  -H "Authorization: Bearer sgn_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "document": { "url": "https://example.com/sow.pdf",
                  "title": "Statement of work" },
    "signers": [
      { "name": "A. Reyes",  "email": "a@acme.com" },
      { "name": "J. Okonjo", "email": "j@ours.com", "order": 2 }
    ],
    "subject": "Statement of work — Q3",
    "expires_in_days": 14
  }'

# then, for free, as often as you like:
curl https://sign.ounie.com/api/envelopes/<id> \
  -H "Authorization: Bearer sgn_live_…"

15 credits to send. 10 more only if and when every signer signs. Polling, reading the audit trail, downloading an executed copy you have already paid for, and withdrawing an envelope are free forever. Buy credits at ounie.com.

What this is not

Signature produces an executed record with an audit trail. It is not legal advice and makes no claim of ESIGN, UETA or eIDAS qualification. Signer identity is not verified.

Every response on every rail carries legalQualification: "none" and a human-readable notice, so a program can read the boundary rather than inferring it from marketing copy. The MCP tool what_this_is_not and the public /api/pricing return the same two lists as below.

Recorded — every one of these is something the server observed on a real request:

  • The sha256 of the exact document bytes each party was served
  • When each signer was invited, when they first opened the document, and when they signed
  • The client IP address and user-agent string observed on the signing request
  • The full name each signer typed, and their explicit consent to sign electronically
  • Every event in append-only order, timestamped in UTC

Not recorded, and therefore not claimed:

  • Signer identity. Nobody's ID is checked, and no document is compared against one.
  • Proof that the person at the keyboard is the person the email was addressed to.
  • Notarisation, witnessing, or any certificate issued by a qualified trust service provider.
  • Geolocation. An IP address is not a location and is not presented as one.
  • Handwriting. A typed name is a typed name, not a biometric signature.

If your matter needs a qualified electronic signature, a notary, a witness or verified identity, this is the wrong product. That is a real answer and it is better than a confident one.

Authentication

Three ways in, all resolving to the same Ounie account and the same wallet:

  • App keyAuthorization: Bearer sgn_live_…, minted at /dashboard/api-keys. Five active keys per account.
  • Fleet master key — your ounie.com developer key (ounie_live_…) works here too, once you have enabled “Use across Ounie apps” in your ounie.com settings. Off by default.
  • Session cookie — the shared .ounie.com session, which is what the dashboard uses. Same endpoints, no key needed in a browser.

For hosts that cannot set an Authorization header, every REST route and the MCP transport also accept ?api_key=sgn_live_….

POST /api/envelopes

Sends one envelope. Costs 15 credits, reserved before any work happens — so an account without the balance gets a 402 with the exact shortfall, and can never overdraw.

FieldTypeNotes
documentobjectExactly one of url, base64 or markdown. See below.
signersarray1–10. Each needs an email; name and order are optional.
subjectstringUp to 200 chars. Defaults to “Please sign: <title>”.
messagestringUp to 4,000 chars, shown above the document.
expires_in_daysinteger1–90, default 14.
webhook_urlstringhttps only. POSTed once when the envelope reaches a terminal state. Best-effort — polling is the contract.
agent_namestringWho the signer is told this came from. Shown on the signing page.

The document

Three shapes, because an agent will have the document in whichever one its previous step produced. Exactly one. Supplying two returns 400 document_ambiguous rather than being resolved by precedence — silently ignoring a field is how the wrong document gets signed.

ShapeWhat happens
urlFetched over https only, never through a redirect, with DNS checked so a public hostname pointing at a private address is refused. Magic bytes decide whether it is a PDF, not the Content-Type header.
base64Decoded and checked the same way. A data: URL prefix is tolerated.
markdownTypeset here as a plain single-column PDF: headings, paragraphs, bullet and numbered lists, rules, blockquotes. Up to 60,000 characters.
The markdown typesetter is not a layout engine. No tables, no images, no columns. That limit is stated rather than hidden, because silently dropping a table out of a contract is far worse than saying we do not do tables. For a designed document, render it elsewhere and pass the PDF URL.

Whatever arrives is hashed with sha256 immediately and never rewritten. That hash appears on the certificate, against every signer, on the signing page, and in the invitation email — so verifying is one command.

Ceiling: 8 MB, checked against the declared length and the bytes actually read.

Signers & order

Each signer gets their own link — 256 bits of entropy in the URL, a sha256 digest in the database. A database read can therefore neither sign anything nor open anyone's page.

order groups them. Equal values sign in parallel; a higher group is only invited once every lower group has signed, and its token is minted at that moment rather than up front. Omit it and everyone is invited at once.

"signers": [
  { "name": "A. Reyes",  "email": "a@acme.com", "order": 1 },
  { "name": "B. Chen",   "email": "b@acme.com", "order": 1 },
  { "name": "J. Okonjo", "email": "j@ours.com", "order": 2 }
]
// A and B are invited now. J is invited when both have signed.

Duplicate addresses are refused. So is any address on Ounie's shared suppression list or this app's own opt-outs — and that check runs before anything is quoted or reserved.

The response

{
  "ok": true,
  "envelopeId": "9f2c41ab-…",
  "reference": "9F2C41AB",
  "status": "sent",
  "artifactStatus": "none",
  "subject": "Statement of work — Q3",
  "expiresAt": "2026-09-07T…Z",
  "document": {
    "filename": "Statement-of-work.pdf",
    "sha256": "9f2c41ab7e…",
    "bytes": 184213, "pages": 6, "origin": "url"
  },
  "signers": [
    { "name": "A. Reyes", "email": "a@acme.com", "order": 1,
      "status": "invited", "invitedAt": "…",
      "viewedAt": null, "signedAt": null, "ip": null, "userAgent": null }
  ],
  "executedPdfUrl": null,
  "auditTrailUrl": null,
  "statusUrl": "https://sign.ounie.com/api/envelopes/9f2c41ab-…",
  "legalQualification": "none",
  "notice": "Signature produces an executed record…",
  "invited": 1,
  "deliveryWarnings": [],
  "creditsCharged": 15,
  "executionCredits": 10
}

“Invited” is not “delivered”. SMTP is asynchronous, so an accepted message can still hard-bounce minutes later. We say invited everywhere rather than promising delivery, and deliveryWarnings carries any address the provider rejected outright.

Polling & downloads

GET /api/envelopes/<id> is free and returns the same shape. Add ?events=1 for the full append-only audit log. Once the artifact is ready, executedPdfUrl and auditTrailUrl are freshly signed short-lived URLs, minted per request rather than stored.

Every status response is no-store. That is deliberate: a cached status is a status that can contradict a withdrawal.

GET /api/envelopes lists your envelopes, newest first. Also free.

Execution & holds

The instant the last signature lands the envelope flips to executed. That flip is unconditional and free. The 10-credit charge is a separate step that builds the executed PDF and the audit artifact.

If your wallet is short at that moment, nothing is lost: the signatures stand, the envelope is executed, and artifactStatus reads awaiting_credits with a hold block naming the shortfall. A signature that has been given cannot be taken back because a balance was short.

POST /api/envelopes/<id>/execute
# Idempotent. Already-ready returns 200 and charges nothing.
# Short wallet returns 402 with required_credits and buy_credits_url.

Withdrawing

DELETE /api/envelopes/<id> withdraws an envelope that has not been fully signed. Free — changing your mind should never cost anything.

Every signing link stops working on the next request. The signing page is served no-store precisely so that revocation is immediate rather than whenever an edge cache decides to expire.

The audit trail

An append-only log. Nothing in this app updates or deletes an event, because the executed PDF can be rebuilt from these rows and the rows cannot be rebuilt from the PDF. If the two ever disagree, the log wins.

EventWhen
createdThe envelope row exists and the document is stored
invitedAn invitation was accepted by the mail provider
delivery_failedThe provider rejected the address outright
viewedA signer opened their page for the FIRST time
document_downloadedA signer opened the document itself
signedA signature was recorded, with the typed name
declinedA signer refused
executedEvery signer had signed
expiredThe window closed with signatures outstanding
voidedThe sender withdrew it

The JSON artifact at auditTrailUrl carries every event plus a notRecorded array — so a program reading it can see the boundary as clearly as a person reading the certificate page can.

MCP

A Model Context Protocol server for Claude, Cursor, ChatGPT, the AI SDK and the Ounie AI Team.

Endpoint  https://sign.ounie.com/api/mcp   (legacy SSE: /api/sse)
Auth      Authorization: Bearer sgn_live_…

# Hosts that can't set headers (e.g. the Ounie AI Team):
https://sign.ounie.com/api/mcp?api_key=sgn_live_…
ToolCost
send_for_signature15 credits
get_envelopefree
list_envelopesfree
produce_executed_copy10 credits, idempotent
void_envelopefree
what_this_is_notfree · public
get_credit_balancefree
get_pricingfree · public
whoamifree

x402

Keyless pay-per-call in USDC on Base — no account, no key. https://sign.ounie.com/api/x402/send answers 402 with terms; sign and retry with an X-Payment header.

curl -X POST https://sign.ounie.com/api/x402/send \
  -H "Content-Type: application/json" \
  -d '{"document":{"markdown":"# Mutual NDA\n\n…","title":"Mutual NDA"},
       "signers":[{"name":"A. Reyes","email":"a@acme.com"}]}'

# → 402
{ "x402Version": 1,
  "accepts": [{ "scheme": "exact", "network": "base",
                "maxAmountRequired": "300000",
                "payTo": "0x…", "asset": "0x8335…2913",
                "extra": { "name": "USD Coin", "version": "2" } }] }
This rail prices the whole lifecycle up front — $0.300, or 25 credits equivalent. A keyless caller has no account to bill when the last signature lands days later, so send and execution are bought together. The honest consequence: if a keyless envelope is never signed, you paid for an execution that did not happen. On the credit rail you pay the 10 only when it actually executes. That is a real reason to use an account.

Two gates. Anything knowable from the request alone and always fatal — a malformed address, a document URL that resolves to a private address, a suppressed recipient — is a 4xx with no price in the body. You are never asked to sign a payment authorization for work that cannot succeed. Everything else is caught after verify and before settle, so an envelope that could not be delivered returns 402 with nothing moved on chain.

The response carries a statusUrl with a poll token. Polling it and downloading the executed copy are free — this call already paid for both.

Errors

StatusMeaning
400A Gate-1 refusal. Fatal, knowable from the request, and never quoted a price on x402.
401No valid session, key or master key.
402Insufficient credits (with required_credits, balance_credits, buy_credits_url), or an unsettled x402 payment.
404No such envelope, or not yours.
409The envelope is not in a state that allows this — already signed, already withdrawn, not yet executed.
422The envelope could not be sent and the reserve was refunded in full.
502The send or the artifact build failed. Nothing was charged.
503Signing is not configured on this deployment.

Gate-1 codes you can act on: document_required, document_ambiguous, document_url_must_be_https, document_url_blocked, document_must_be_pdf, document_too_large, signers_required, signer_email_invalid, signer_email_duplicate, too_many_signers, expires_in_days_invalid, recipient_suppressed, too_many_open_envelopes.

Limits

LimitValue
Signers per envelope10
Open envelopes per account50
Document size8 MB
Markdown length60,000 characters
Expiry window1–90 days (default 14)
Active API keys5 per account
Download link lifetime5 minutes, re-minted per request

Questions the docs do not answer: get in touch.