Developers

API reference.

Everything the console does, your own software can do: create and send agreements, place fields by anchor text, track every state change, receive signed webhooks, and download the sealed original. Plain JSON over HTTPS — no SDK required.

Reference current as of 2026-09-05 · Base URL https://talyanasign.com · Your API key lives under Admin → API & webhooks in the console.

For: developers (or AI agents) integrating an application with the Talyana Sign e-signature platform at https://talyanasign.com.

1. Base URL, auth, and conventions

Base URL: https://talyanasign.com

Two authentication modes, both accepted on every org-scoped route:

Header Who Notes
X-API-Key: sk_… Server-to-server integrations The org-level key. Keep it server-side ONLY — never in browser JS. Sees every envelope in the org.
X-Auth-Token: <token> Console users (per-user sessions) From POST /api/auth/login. 7-day expiry. v25: sees only the user's own envelopes plus ones shared with them (admins/viewAll see all) — see §4b.

Connection test: GET /api/ping (either auth mode) → {"ok":true,"org":"…","plan":"…","auth":"apiKey|session"} — the right call for a "Test connection" button. A non-technical quick-connect kit (drop-in PHP class + step-by-step guide) ships at /connect/ on every install.

Conventions:

CORS: browser pages may call the API only from origins in the server's corsOrigin allowlist (comma-separated, configured server-side). Server-to-server calls (PHP curl etc.) are unaffected by CORS. If you embed the signing page or call the API from browser JS on another subdomain, that subdomain must be added to corsOrigin in the server's config.php first.

2. Envelope lifecycle

draft ──send──▶ sent ──all signed──▶ completed (sealed PDF + certificate)
                 │──any decline──▶ declined
                 │──admin void───▶ voided
                 └──expiry (default 30 days)──▶ expired

All transitions are enforced server-side with atomic guards; you can retry safely (a duplicate /send returns 409 and does NOT invalidate the first call's signing links).

3. Core endpoints

Create an envelope — POST /api/envelopes

{
  "title": "Hardship Assistance Agreement",        // required, ≤300 chars
  "message": "Please review and sign.",             // optional, ≤5000 chars
  "documentBase64": "<base64 PDF | PNG | JPEG>",    // required (unless documents[] below), ≤15MB decoded
  "documents": [                                    // v72 ALTERNATIVE to documentBase64: multi-document envelope
    {"name": "Consent Form", "documentBase64": "<base64 PDF|PNG|JPEG>"},   // 1–10 docs, ≤15MB combined,
    {"name": "W-9 Request",  "documentBase64": "<base64 PDF>",            // names ≤190 chars (default "Document N")
     "omitPages": [2, 5]}                            // v77 OPTIONAL: 0-based ORIGINAL page indices to remove at preparation
  ],
  "markups": [                                      // v73 OPTIONAL sender markup — burned into the PDF at create
    {"type": "text",     "page": 0, "x": 40, "y": 100, "w": 220, "h": 24, "text": "Amended per counsel", "fontSize": 12},
    {"type": "strike",   "page": 0, "x": 40, "y": 160, "w": 150, "h": 16, "color": "red"},
    {"type": "whiteout", "page": 0, "x": 40, "y": 200, "w": 160, "h": 40},
    {"type": "check",    "page": 1, "x": 300, "y": 100, "w": 20, "h": 20}
  ],
  "senderEmail": "agent@yourfirm.com",              // optional; gets decline/completion notices. v25: when it matches
                                                    //   a user on the account, the envelope is ATTRIBUTED to that seat —
                                                    //   it appears in that person's own console view (see §4b)
  "routing": "parallel",                            // or "sequential" (signing order enforced)
  "cc": [{"name": "Board Chair", "email": "chair@…"}],  // optional, completion copies
  "sendReceipt": true,                              // optional, default false (v78): at SEND time, email the sender a receipt listing who was emailed, and notify each CC they're copied. Completion emails unchanged.
  "webhookUrl": "https://your-app.example.com/hooks/talyanasign",  // optional, public https URL
  "metadata": {"applicationId": "HA-2026-0042"},    // optional, ≤60KB; echoed in webhooks
  "signers": [                                      // required JSON LIST (not object), 1–10
    {"firstName": "Dana", "lastName": "Donor",        // v52: preferred — or legacy "name": "Dana Donor"
     "email": "dana@example.com", "assuranceLevel": "email"},
    {"name": "Wes Witness", "email": "wes@example.com",
     "assuranceLevel": "access_code", "accessCode": "1234"}
  ],
  "fields": [                                       // required list, 1–200
    {"signerIndex": 0, "type": "signature",   "page": 0, "x": 50, "y": 600, "w": 180, "h": 50},
    {"signerIndex": 0, "type": "date_signed", "page": 0, "x": 260, "y": 600, "w": 120, "h": 24},
    {"signerIndex": 0, "type": "name",  "page": 0, "x": 50, "y": 560, "w": 180, "h": 22},
    {"signerIndex": 0, "type": "email", "page": 0, "x": 260, "y": 560, "w": 200, "h": 22},
    {"signerIndex": 0, "type": "text",     "page": 0, "x": 50, "y": 500, "w": 200, "h": 22, "required": true},
    {"signerIndex": 0, "type": "company",  "page": 0, "x": 50, "y": 470, "w": 170, "h": 22, "required": true},
    {"signerIndex": 0, "type": "title",    "page": 0, "x": 260, "y": 470, "w": 150, "h": 22, "required": false},
    {"signerIndex": 0, "type": "checkbox", "page": 0, "x": 50, "y": 430, "w": 16, "h": 16, "required": false},
    {"signerIndex": 1, "type": "initials", "page": 1, "x": 400, "y": 80, "w": 60, "h": 32},

    {"signerIndex": 0, "type": "signature", "anchor": "/sig1/",              // v86 AutoPlace: find this text in the
     "anchorOffset": {"dx": 0, "dy": -6}},                                   //   final PDF and place the field there
    {"signerIndex": 0, "type": "initials", "anchor": "/init/", "anchorAll": true},  // one field per occurrence
    {"signerIndex": 0, "type": "signature", "page": -1, "x": 54, "y": 150, "w": 180, "h": 50}  // v88: -1 = LAST page
  ]
}

Send it — POST /api/envelopes/{id}/send

No body. 200{"envelopeId", "status": "sent", "signingUrls": [{"signerId", "email", "url"}]}. Emails each signer their unique signing link (first signer only, if sequential). The signingUrls are the same links — you may present them directly in your own UI instead of (or in addition to) the email. Second call → 409.

Read state — GET /api/envelopes/{id}

200 → summary: status, signers[] (each with status: pending → consented → signed, plus orderIndex, notifiedAt), sentAt, expiresAt, routing, cc, sealHash etc. Append ?includeLinks=1 to also get per-signer signingUrl and the full fields layout.

Everything else

Endpoint What
GET /api/envelopes?q=dana&status=sent List/search (title, signer name/email; %/_ are literal). Max 100, newest first. Trashed envelopes are hidden; ?deleted=1 lists ONLY the trash (v74).
POST /api/envelopes/{id}/delete v74. Drafts: hard delete (rows + files gone; any org user). Terminal envelopes (completed/declined/voided/expired): moved to the org's trash — hidden from lists, restorable, sealed files + audit untouched; signer links and public verify keep working (admin/API key only). Sent envelopes: 409 — void first.
POST /api/envelopes/{id}/restore v74. Bring a trashed envelope back (409 if not trashed or already purged; admin/API key only).
POST /api/envelopes/{id}/purge v75 "Delete forever". Trashed envelopes only (409 otherwise): destroys the document files immediately; the row, hashes, and audit chain remain so /verify keeps working (documentPurged: true). Irreversible; audited as envelope.purged (via: delete_forever). Admin/API key only.
POST /api/envelopes/empty-trash v75. Purges every trashed, not-yet-purged envelope (same semantics as above, via: empty_trash) → {"purged": N}. Purged envelopes leave the ?deleted=1 list. Admin/API key only.
GET /api/envelopes/{id}/document Current PDF: original → work-in-progress → sealed, best available.
POST /api/envelopes/{id}/fields v87.2 — DRAFTS ONLY (409 once sent). Body {"fields": […]} REPLACES the entire field layout — same shape and validation as create (anchors are NOT accepted here; pass resolved coordinates; negative page counts from the end). Powers a sender "review & adjust placement" screen: create the draft with auto-placed fields, GET ?includeLinks=1 for the layout, let the sender nudge boxes, POST the adjusted list back, then /send. Audited as envelope.fields_updated.
GET /api/envelopes/{id}/audit Full audit trail + chainValid (hash-chain verification).
POST /api/envelopes/{id}/remind Manual reminder email to pending signers (optional body {"signerId": "sgn_…"}).
POST /api/envelopes/{id}/void Cancel. Body {"reason": "…"} optional. Sent envelopes: admin users or the API key. Completed envelopes can't be voided.
GET /api/envelopes/{id}/verify Public (no auth): seal present/intact, Ed25519 signature valid, audit chain valid. Printed on every certificate.
POST /api/verify Public: {"envelopeId", "documentBase64"} → does this file match the sealed original?
POST /api/convert Word → PDF (v51). {"filename": "consent.docx", "documentBase64": "…"}{pdfBase64, pdfSha256, sourceSha256} via the org's Microsoft 365 tenant. Show the PDF to a human before creating an envelope from it — envelopes stay PDF-only by design. 501 when not configured (see SETUP-M365-CONVERSION.md); 502 when M365 rejects the file.
POST /api/signup Public self-serve signup (v53): {"orgName","name","email","password"}201 {token, user, organizationName, plan}. Creates the org (plan starter) + first admin + a live session. Honeypot field website must be empty; throttled to 3/hour/IP.
GET /api/plans Public plan catalog (v26): active tiers with name, price, priceCents, envelopeQuota, userLimit, whiteLabel, blurb, buyable — what the console plan cards render. Edited in the platform console; repricing needs no deploy.
GET /api/ping Authenticated connection test → {ok, org, plan, auth} (v87).
GET /health Liveness probe (no auth).

Automatic reminders: hourly cron nudges unresponsive signers (first after 3 days, then every 3 days, max 3) and expires envelopes after 30 days (defaults; server-configurable).

Retention purge (v74): the cron also enforces each envelope's retentionDays (the policy printed on its certificate). Once that many days pass after completion, the document files are destroyed; the envelope row, hashes, and audit chain remain. After a purge: document endpoints return 410 with a retention message, the summary carries purgedAt, and GET /verify reports documentPurged: true alongside a still-valid sealSignatureValid/auditChainValid — verification can attest what was sealed and when even though the document itself is gone. retentionDays null or 0 = keep indefinitely. Purge applies regardless of trash state and is irreversible by design.

4. Templates

Store a reusable document + field layout (+ title, message, routing, CC):

To use one: fetch detail + document, then POST /api/envelopes substituting real names/emails for the roles (field layout carries over verbatim; the console's "Use template" does exactly this).

Templates hold a single document — to reuse a multi-document combination, keep the sources and re-create with documents[] (v72; the console enforces the same rule).

4b. Multi-user accounts: seats, visibility, sharing, profiles (v25)

The DocuSign-parity account model. One organization, one API key, many user seats — and each console user's envelopes are private to them by default.

Visibility rules (enforced on lists, direct reads, documents, audit — invisible envelopes return 404, never 403, so existence doesn't leak):

Attribution: console creations are attributed automatically. API-key creations are attributed to the user whose email matches senderEmail — this is how a portal sends "as" its signed-in person, and it's why integrations should always pass senderEmail.

Auto-provisioning seats (v87.1): an integration can create seats itself — POST /api/users with {"name", "email", "role": "staff", "invite": true} (API key, no password) runs the same invitation flow console admins use: the person gets an activation email and picks their own password. A duplicate email returns a clean 409 (treat as success). Attribution works the moment the seat row exists — activation is only needed for the person to open their own console. The quick-connect client wraps this as ensureSeat($name, $email).

Shared Access (user-level, DocuSign-style: sharing grants visibility into ALL of the owner's envelopes, past and future — the assistant-manages-the-sender arrangement):

Endpoint What
GET /api/users/me/shares {iShareWith: […], sharedWithMe: […]} for the session user.
POST /api/users/me/shares Body {"granteeUserId": "usr_…"} to grant; add "remove": true to revoke. Idempotent.
GET/POST /api/users/{id}/shares Same, on another user's behalf — requires the manageUsers permission.
GET /api/users/directory Active users (id, name, email) — any session user; powers sharing pickers without exposing roles.

Permission profiles — named capability bundles that override a user's role default. The catalog: send, viewAll, templates, void, manageUsers, manageOrg. Built-ins: admin = everything, staff = send + templates (own envelopes only).

Endpoint What
GET /api/profiles {catalog, builtins, profiles: [{id, name, perms, users}]}.
POST /api/profiles {"name", "perms": {send: true, …}}201. Unlisted perms default false.
POST /api/profiles/{id} {name?, perms?} to update; {"delete": true} removes it (assigned users fall back to their role default).
POST /api/users/{id} {"profileId": "prf_…"} assigns; {"profileId": null} clears to role default. Guard: you cannot strip the last active admin of user management (409).

Profile management requires manageUsers. GET /api/auth/me reports the session user's effective perms, profileName, and shares. All of this is also manageable in the console: Admin → Shared Access / Profiles, users table Profile column, and each user's own Account → My sharing screen.

Account self-service (console sessions only, v90)

Two endpoints exist for the console's Account → My account screen; they require an X-Auth-Token user session and refuse API keys (400):

Endpoint Body Effect
POST /api/auth/change-password {currentPassword, newPassword} Verifies the current password (403 if wrong), requires ≥10 characters, keeps the calling session signed in, revokes every OTHER session for that user, and emails a "your password was changed" notice.
POST /api/auth/profile {name?, phone?} Updates the user's own display name (2–190 chars) and phone. Email changes remain an administrator action.

GET /api/auth/me now also returns user.phone.

5. Webhooks

If webhookUrl is set on an envelope, Talyana Sign POSTs JSON to it on terminal transitions:

{"type": "envelope.completed", "envelopeId": "env_…", "status": "completed",
 "ts": "…", "sealHash": "…", "metadata": {…your metadata…}}

Events: envelope.completed (includes sealHash), envelope.declined, envelope.voided, envelope.expired. Exactly-once is NOT guaranteed (one retry on failure) — make your handler idempotent, keyed on envelopeId + type.

Verify authenticity: every delivery carries X-TalyanaSign-Signature: sha256=<hex> — HMAC-SHA256 of the raw request body with your org's webhookSecret (whsec_…, issued at org provisioning). Compare with a constant-time comparison. Also X-TalyanaSign-Event: <type>. (The same values are also sent as the legacy X-Signet-Signature / X-Signet-Event headers — integrations that verify those keep working forever.) Requirements: public https host (loopback/private IPs are rejected at envelope create), respond 2xx within 5s.

Webhooks are best-effort — poll GET /api/envelopes/{id} as the source of truth for anything critical.

6. Signer experience (what your users see)

The emailed link opens /sign?token=… — a guided, DocuSign-style flow: ESIGN/UETA consent → guided field navigation (Start/Next) → signature adoption (typed style, drawn, or uploaded) → done. Mobile-friendly (tested on WebKit/iPhone viewports). You normally do NOT need to build any signer UI. (Links issued before v54 pointed at /demo/sign.html?token=…; that page now redirects to /sign preserving the token, so old links keep working.)

If you want signing inside your own page instead, the widget is embeddable (/widget/talyanasign.js + a container div + the token — TalyanaSign.mount({...}); the legacy /widget/signet.js URL and Signet global are the same file and work forever), but the token comes from signingUrls — treat it as a bearer credential: anyone with the URL can sign as that person. Don't log it, don't put it in referrable markup.

Per-signer API (the widget drives these; listed for completeness): GET /api/sign/{token}/session, POST …/verify-access, GET …/document, POST …/consent (must send {"accepted": true}), POST …/attachment (v72: {"fieldId": "fld_…", "filename": "license.pdf", "dataBase64": "…"}{ok, filename, sha256}; PDF/PNG/JPEG ≤10MB; requires prior consent; re-upload replaces; 404 for another signer's field), POST …/signature ({"typedName"} or {"signatureDataUrl": "data:image/png;base64,…"} + {"fieldValues": {"fld_…": "value"}}; optional {"initials": "J.Q.D"} — signer-chosen initials for initial fields, derived from the name when absent), POST …/decline. Consent is required before signing; sequential turn order is enforced server-side; a signer who signed cannot decline or re-consent; voided/declined/expired envelopes stop serving the document (410).

7. Completion artifacts

On completion Talyana Sign appends a certificate of completion (signer identities, consent + signature timestamps, IPs, geo summaries, document hashes, audit chain tip, verify URL — plus, v72: a SOURCE DOCUMENTS section for multi-document envelopes and a SIGNER ATTACHMENTS section listing every uploaded file with its SHA-256 and uploader) and seals the PDF with an Ed25519 platform signature. Signer-uploaded attachments are appended to the document before hashing, so the seal covers them. GET /api/envelopes/{id}/document then returns the sealed PDF; sealHash in the summary is its SHA-256. Completion emails go to all signers, the sender, and any CC addresses automatically — each lists every signer with their signed-at time and carries the public verification link (v90).

Public verification (no auth). The certificate prints GET /api/envelopes/{id}/verify and a QR code for it (v90). That URL is content-negotiated: a browser (Accept: text/html) gets the human verification page — verdict, four checks, seal record, and a "check your own copy" drop zone that hashes the visitor's PDF locally with SubtleCrypto (nothing is uploaded) — while API clients, curl, and fetch() keep getting JSON:

{"envelopeId":"env_…","sealPresent":true,"documentIntact":true,"sealSignatureValid":true,
 "auditChainValid":true,"sealHash":"<sha256 hex>","sealedAt":"2026-…Z","platformPublicKeyPem":"-----BEGIN PUBLIC KEY-----…"}

Force JSON regardless of Accept with ?format=json. The same page also answers at /verify, /verify?id=env_… and /verify/env_…; with no id it lets a person drop a sealed PDF and reads the Agreement ID out of the certificate text. documentPurged: true (plus purgedAt) appears after a retention purge — the seal and audit chain still verify, the file itself is gone by design.

8. Error handling cheat-sheet

Status Meaning Your move
400 Validation — message says exactly what Fix the payload
401 Bad/missing key or expired session Check header
403 Role/turn restriction Expected: not-your-turn, non-admin void
404 Wrong id, or other org's resource Check id
409 State conflict (already sent, already signed, voided…) Re-fetch state; usually means a retry raced
410 Document retracted (voided/declined/expired) Terminal
413 Body exceeds server post limit Shrink the PDF
423 Access-code lockout (5 failures) Sender must re-send
500 Server error (logged server-side) Retry once; then report

Timeout-retry idempotency: create is safe to retry (worst case an extra draft — void it); send retry returns 409 with the original links intact; void/decline retries return 409 no-ops.

9. Known limitations (documented, accepted)

10. Quick-start (PHP, server-to-server)

The fast path (v87): every install serves a non-technical quick-connect kit at /connect/ — a step-by-step guide plus TalyanaSignClient.php, a dependency-free single-file connector covering ping, create/send (anchors or coordinates), status, sealed-PDF download, Word conversion, and webhook verification. Point a white-label subscriber's web developer at that page and they can wire their portal in an afternoon. The hand-rolled minimal version, if you'd rather not use the class:

function talyanasign(string $method, string $path, ?array $body = null): array {
    $ch = curl_init("https://talyanasign.com$path");
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 30,
        CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-API-Key: ' . TALYANASIGN_API_KEY],
    ] + ($body !== null ? [CURLOPT_POSTFIELDS => json_encode($body)] : []));
    $res = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    $data = json_decode((string)$res, true) ?? ['error' => 'invalid response'];
    if ($status >= 400) throw new RuntimeException("Talyana Sign $status: " . ($data['error'] ?? '?'));
    return $data;
}

$env = talyanasign('POST', '/api/envelopes', [ /* §3 payload */ ]);
$sent = talyanasign('POST', "/api/envelopes/{$env['id']}/send");
// Persist $env['id']; track completion via webhook or polling.

Keep TALYANASIGN_API_KEY (and the whsec_… webhook secret) in server-side config outside the web root and outside any synced folder.