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:
- All request/response bodies are JSON. The
Content-Typerequest header is not enforced (the body is parsed as JSON regardless), but sendapplication/jsonanyway. - Every error is
{"error": "human-readable message"}with a proper 4xx/5xx status. Uncaught server errors return{"error": "Internal error"}with status 500. - All timestamps are UTC ISO-8601 strings (
2026-07-31T14:00:00.000Z). - No trailing slashes:
GET /api/envelopes/is a 404; useGET /api/envelopes. - A request body larger than the server's
post_max_sizereturns a clear413. Keep base64-encoded PDFs comfortably under that (the decoded document limit is 15 MB). - Responses carry
Cache-Control: no-store— never cache envelope state.
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
]
}
201→ full envelope summary includingid(env_…). Envelope starts as a draft; nothing is emailed yet. Plan quotas (v53, catalog-driven since v26): each plan's monthly envelope quota and seat limit live in the platform's plan catalog (defaults:starter5/mo · 1 seat,personal100/mo · 1 seat,businessunlimited · 4 seats,organizationunlimited · unlimited); a hit cap returns a clear403. Admin-provisioned and pre-v53 orgs areorganization(unlimited). The pre-v26 slugprofessionalis a permanent alias ofpersonal.- Multi-document envelopes (v72): pass
documents[]instead ofdocumentBase64and the sources are merged into ONE PDF at creation, in list order — the signer sees and seals a single continuous document (the "signer sees exactly what gets sealed" invariant is untouched). Fieldpageindexes the merged PDF. Per-source provenance (name,sha256,sourceFormat,pageStart,pageCount, all 0-based) is returned asdocumentsin the envelope summary and printed on the certificate under SOURCE DOCUMENTS. Images may be mixed in freely (each wraps to a page). LegacydocumentBase64behaves exactly as before (documents=nullin the summary). - AutoPlace anchors (v86): instead of
page/x/y, a field may giveanchor(≤120 chars) — the server locates that exact text in the FINAL document (post-merge, post-markup) and places the field at its position.anchorOffset {dx,dy}shifts from the hit;w/hfall back to sensible per-type defaults (signature 180×50, initials 60×32, date_signed 120×24, …);anchorAll: truerepeats the field at EVERY occurrence (initials-on-every-page). Put anchors in white or 1pt type in the source document so they don't print. An anchor not found in the document is a clear400naming it — so a caller can fall back to coordinates. Expansion is capped at 200 fields. - Negative pages (v88): field
pagemay be negative, counting from the END of the final document —-1= last page,-2= second-to-last. Resolved server-side after conversion/merge/omission, so "signature block on the last page" needs no page count. Out-of-range (e.g.-5on a 3-page document) is a clear400. - Field geometry is in PDF points, origin bottom-left,
pageis 0-based.type∈signature | initials | date_signed | name | first_name | last_name | email | text | checkbox | company | title | radio | dropdown | attachment(the DocuSign-standard palette).date_signed,name,first_name,last_name, andemailauto-fill from the signing session/signer record — the signer takes no action on them.text,checkbox,company,title,radio, anddropdownare signer-fillable in the signing UI;required: true(the default for text-like fields; checkboxes default optional) blocks signing until completed. - Page removal (v77): each
documents[]entry acceptsomitPages— 0-based page indices of the ORIGINAL upload to drop before merging (deduped server-side; out-of-range pages and removing every page across the envelope are clear400s). Fieldpageindices refer to the FINAL document (after removals + merge). Provenance is explicit: the summary'sdocuments[]entries carryomittedPages, and the certificate's SOURCE DOCUMENTS section notes "(N page(s) removed at preparation)" — the recorded SHA-256 remains that of the file as uploaded. A single document withomitPagesis valid (usedocuments[]with one entry). - Sender markup (v73):
markups[](≤100 ops) applies prep-time document edits — permanenttext(≤2000 chars,\nfor multi-line,fontSize6–48, default 11),strike(horizontal line through the box's middle),whiteout(opaque cover-up box),check(drawn checkmark);color∈black(default) |red|blue(whiteout is always white). Ops are burned into the PDF at create, before any signer can see it — the legal invariant holds because every signer sees, and the platform seals, the marked-up version. Page indexes the merged PDF when combined withdocuments[](merge happens first). Evidence: theenvelope.createdaudit event'sdocumentSha256is the post-burn hash and carriesmarkupCount, and a dedicatedenvelope.markup_appliedaudit event records the full op list on the tamper-evident chain. This is sender prep tooling (DocuSign-markup parity), never a post-send edit — after create the document is immutable as always. An op referencing a missing page is a clear400. - Attachment-request fields (v72):
type: "attachment"asks the signer to UPLOAD a file (PDF/PNG/JPEG, ≤10MB) during signing — DocuSign's "attachment" tab.required: trueblocks signing until uploaded. The file's pages are appended to the document before sealing (covered by the content hash and Ed25519 seal), a[Attached] filename (sha256 …)marker is stamped at the field position, the certificate lists every attachment (filename, SHA-256, uploader) under SIGNER ATTACHMENTS, and the audit trail recordssigner.attachment_uploaded. signerIndexrefers to the position in thesignerslist. Insequentialrouting, list order IS the signing order — only the first signer is emailed at send; each next signer is emailed automatically when their turn arrives.assuranceLevel:email(default) oraccess_code(signer must enter the code you supply; share it out-of-band).- Validation is strict: wrong types, out-of-range values, and over-length strings are clear
400s. Nothing is partially created on failure.
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):
POST /api/templates— same shape as envelope create, but signers are roles:{"name", "title", "message", "routing", "cc", "documentBase64", "signers": [{"role": "Donor"}], "fields": […]}→201withtpl_…id.GET /api/templates— list withfieldCount,roles,createdBy.GET /api/templates/{id}— full layout, routing, cc.GET /api/templates/{id}/document— the stored document.POST /api/templates/{id}/delete.
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):
- A user sees envelopes they created, plus every envelope owned by users who shared with them.
- Users with the
viewAllpermission (admins by default) see everything — including unattributed envelopes (API-key creations with no matchingsenderEmail). - The API key sees everything: integrations are the org acting as itself.
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)
- Signer tokens are stored in plaintext in the database (reminder emails need to rebuild the links). DB compromise ⇒ live signing links leak. The DB is shared with the main site — treat any SQLi anywhere on the domain as critical.
- One org's user emails are globally unique across the install — a second org can't reuse an email that exists in the first (single-org deployment today, so moot).
- No rate limiting beyond the access-code lockout. Don't expose the API key to anything you don't trust.
- Signer text much longer than its drawn box can visually overflow on the stamped PDF (stored value is always correct/complete).
- ~~Envelope
metadatais returned in webhooks but not in the envelope summary.~~ Fixed in v87: the summary now echoesmetadata.
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.