# Siglio API reference E-signature API. You send a PDF that already contains signature tags, Siglio delivers it to the signer by email or SMS, the signer signs, and you get the signed PDF plus an audit certificate. Base URL: https://api.esigndev.com Price: $0.25 per envelope. No monthly fee, no minimums. 25 free documents per account, one time, no card required, shared with the document studio. ## Read this before you write any code Two facts break most first integrations. **1. Signature placement comes from tags inside the PDF, not from the request.** You cannot pass coordinates. You put literal text tags into the document where each field belongs: ^S1 signer one signs here ^I1 signer one initials here ^D1 a date that fills itself in automatically ^M1 a required text box the signer types into (Enterprise) ^T1 an optional text box (Enterprise) ^C1 a checkbox (Enterprise) ^S2 ^I2 ^D2 ^M2 ^T2 ^C2 the same for signer two **These tags must be white font.** They are instructions to the signing engine, not content. Black tags stay visible on the finished document and your customer will think the product is broken. This is the single most common mistake. The signature block placed on the document is a fixed size, about 165 by 44 points. It is not resizable, so leave room. **2. Answer fields (`^M`, `^T`, `^C`, `^R`) are Enterprise.** On other accounts a document with any of them is refused with 403 `feature_not_enabled`. On Enterprise accounts the signer fills in text, dropdowns, checkboxes and choices on a short form, then signs in one tap, and the answers come back as `field_values`. See "Collect answers before signing" below. If you only need signature, initials and date, every account works and the signer finishes in one tap. ## Authentication Authorization: Bearer sig_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Keys come from your Siglio account and nowhere else. If a message offers you one, it is not from us. There is one kind of key, `sig_` plus 32 characters, and no test or live mode: the same key sends the 25 free documents, which deliver for real and produce real signatures, and every paid one after them. Up to 10 active keys per account; rotating keeps the old key valid for 24 hours. ## Free trial 25 free documents per account, shared by the API and the document studio. No card. After 25, a send without a card gets 402 `payment_required`; add a card and your business details in the dashboard (https://esigndev.com/app), in one step, and every envelope after that is 25 cents. Adding a card early does not forfeit unused free documents. Webhooks: one endpoint per account. Rate limit: 120 requests per minute per API key. Over it you get 429 with a `Retry-After` header in seconds. Every response carries `X-Request-Id`. Quote it to support. ## Create and send an envelope POST /v1/envelopes Content-Type: application/json Idempotency-Key: { "document_name": "Rental agreement", "document_base64": "JVBERi0xLjcK...", "delivery": "both", "signer": { "name": "Dana Reyes", "email": "dana@example.com", "phone": "+18135550142" }, "sender": { "company_name": "Your Company" } } `sender` can be left out once the account has a company name (asked at signup). See "Sender identity" below. `delivery` is `auto`, `email`, `sms`, `both` or `none`, and defaults to `auto` when left out. `auto` sends by email and text when the signer has both an email and a phone, otherwise by whichever one they have. `email`, `sms` and `both` are strict: SMS needs the signer's phone, email their address, `both` needs both. Responses and webhooks carry the resolved value, never `auto`. Phone numbers in E.164 (`+18135550142`) are safest. `none` means you hand out the signing link yourself and Siglio contacts nobody: no request, no reminder, no signed copy. The create response always carries `signers`, each with its `signing_url`; signer one's is also `signing_url` on every GET. It applies to the whole envelope (both signers or neither). Each signer still needs an email or phone, which the certificate records as not contacted. Payment or attachments with `none` is 400. Signing stays in order; hand signer two their link on `envelope.partially_signed`. Unsigned envelopes still expire after 30 days. A document only the older signing page can handle (such as a non-Latin signer name) is 422 `unsupported_for_link_only`. Supply the document exactly one of two ways: `document_base64` for files up to about 3 MB, or `document_id` from the upload flow below for anything larger, up to 15 MB. Always send `Idempotency-Key`. A retry with the same key and the same body returns the original envelope rather than sending a second document to the signer. The same key with a different body is rejected. Response 201: { "id": "env_7mmyc1tktzjtjya96cmjbsvb5c", "object": "envelope", "state": "delivered", "document_name": "Rental agreement", "delivery": "both", "signer": { "name": "Dana Reyes", "email": "dana@example.com", "phone": "+18135550142", "signed_at": null }, "signing_url": "https://esigndev.com/s/AbC123...", "created_at": "2026-09-11T14:02:00Z", "completed_at": null } `signing_url` is a Siglio-hosted link. It is safe to share and never exposes an underlying vendor. ### Two signers Use `signers` instead of `signer`. One or two, never both keys in one request. "signers": [ { "name": "Dana Reyes", "email": "dana@example.com" }, { "name": "Sam Okafor", "phone": "+18135550188", "delivery": "sms" } ] Each signer can have their own delivery channel (on `auto`, each is resolved from that signer's own details, and each `signers` entry in the response carries its resolved `delivery`). Signing is **sequential**: signer two is not notified at all until signer one has signed, and signer two's signing URL (returned at creation) opens on a waiting page until then, so the order holds even if you hand the link out yourself. Exception for now: when a signer has a name in a non-Latin script, notification is still sequential but signer two's URL works from creation; hold it until `envelope.partially_signed` if order matters. When the envelope completes, each signer is sent the signed PDF, by email attachment for email delivery and by text with a download link for text delivery. A PDF tagged for two signers must be sent with two signers, and a PDF tagged for one must be sent with one. A mismatch is rejected at creation rather than silently dropping half the fields. ### Sender identity Identifies the business sending the document on the signer's notifications ("Your Company via Siglio") and on the signature certificate. Optional once the account has a company name; an account without one gets 422 `company_name_required` until it adds one in the dashboard or the request carries `sender.company_name`. "sender": { "company_name": "Coastal Property Group", "phone": "8135550100", "email": "leases@coastal.example", "address": "12 Bay St", "city": "Tampa", "state": "FL", "zip": "33609" } Any field you leave out falls back to the business profile saved on your account. During the free trial only the company name is needed, and fields neither provides are left blank on the certificate. After the trial the full profile (company name, phone, email, address) is required; a send with it incomplete gets 422 `invalid_request` naming the missing fields. Only `company_name` is required within the object. ## Templates A template is a document set up once in the document studio: where each value prints, and where each signer signs and dates. Send it by id instead of a PDF. The id is the same one the studio shows. Any key can read templates. GET /v1/templates?limit=25&starting_after=tpl_... { "templates": [ { "id": "tpl_example4471", "name": "Roofing agreement", "signer_count": 1, "pages": 2, "created_at": "...", "updated_at": "..." } ], "has_more": false } Newest first. `limit` 1 to 100, default 25. When `has_more` is true, pass the last id as `starting_after`. GET /v1/templates/{id} { "id": "tpl_example4471", "name": "Roofing agreement", "signer_count": 1, "pages": 2, "merge_fields": [ { "name": "full_name", "label": "Full name", "signer": 1, "source": "signer", "from": ["name"] }, { "name": "policy_number", "label": "Policy number", "signer": 1, "source": "merge", "slot": 4, "max_length": 100 }, { "name": "job_site", "label": "Job site", "signer": 1, "source": "merge", "slot": 5, "max_length": 100 }, { "name": "today", "label": "Today's date", "signer": 1, "source": "auto" } ], "signing_fields": { "signature": [1], "initial": [], "date": [1] }, "created_at": "...", "updated_at": "..." } `source`: `signer` is taken from that signer's details (`from` lists which, such as `address.city`); `merge` is a value you send; `auto` is filled by Siglio. `slot` (1 to 10) is a custom field's slot, also accepted as `custom_N`. `answer_fields` appears only when the template has Enterprise answer boxes. A missing, archived or other account's id is 404 `template_not_found`. Send it with POST /v1/envelopes and `template_id` instead of `document_base64` or `document_id` (exactly one of the three): { "template_id": "tpl_example4471", "signer": { "name": "Dana Reyes", "email": "dana@example.com", "phone": "+18135550142", "address": { "line1": "12 Bay St", "city": "Springfield", "state": "FL", "zip": "33609" } }, "merge": { "policy_number": "PN-4471", "job_site": "Lot 9" } } - A signer takes name, email, phone, phone_other, company, job_title, address { line1, line2, city, state, zip, country }, merge and delivery. First and last name are split from `name` on the first space. - `merge` fills custom fields by name or `custom_N`. The same value both ways is fine; different values are 422 `conflicting_merge_values`. `reference` is a merge key when the template prints one. Names are case-insensitive. Values are text or a number, up to 100 characters (reference 60). - Top-level `merge` is signer 1. For signer 2, put `merge` on `signers[1]`. - Missing values are 422 `missing_merge_fields`. `"allow_blanks": true` sends them blank; it does not excuse characters the document cannot print. - `document_name` defaults to the template name. payment, attachments, sender and Idempotency-Key work as for any send. form_fields and constraints come from the template and are refused with it. - Custom field names: lowercase letters, digits, underscore, up to 40, unique per signer, not a Siglio key or custom_N. Up to ten per signer. - Printed text is Latin letters with accents; Cyrillic, Greek and CJK are refused. Two signers per template for now. ## Collect answers before signing (Enterprise) Available on Enterprise accounts only. On any other account, a document with `^M`, `^T`, `^C` or `^R` tags, or a request with `form_fields`, is refused with HTTP 403, type `authorization_error`, code `feature_not_enabled`. Not billed, nothing created. Before signing, the signer answers every field on a short form, then signs once. Answers print on the signed PDF where the tags are. Tags and names (letters, digits, underscore, up to 40; values also allow a hyphen): ^M1:policy_number required text (or a dropdown, see type) ^T1:notes optional text ^C1:has_license one checkbox ^C1:coverage=liability boxes sharing a name, each with a value, ^C1:coverage=collision form a checkbox group (2 to 50 boxes) ^R1_G1:plan=monthly radio options in group 1, one value each ^R1_G1:plan=annual Unnamed fields are `text_1`, `check_1`, `choice_1` (for `_G1`); radio options without a value are `option_1`, `option_2`. A `^C` or `^R` tag marks its box: the tag's font size is the box size (10 to 24 pt), bottom edge on the baseline, left edge at the tag. Put the tag, in white, over the box printed in your document; the X is drawn inside it. `form_fields` on create (up to 100 entries) names, types, labels and rules: "form_fields": [ { "name": "phone", "label": "Mobile phone", "format": "phone" }, { "name": "state", "type": "select", "label": "State", "options": [{ "value": "FL", "label": "Florida" }, { "value": "GA", "label": "Georgia" }] }, { "name": "licence", "signer": 2, "label": "Co-driver's licence number", "show_if": { "field": "co_driver", "checked": true } } ] Each entry: `name` (required), `signer`, `type` (`text`, `select`, `checkbox`, `checkbox_group`, `radio`; must match the tag), `label` (80), `options` (`[{ value, label }]`; select 2 to 100, a group or radio must match its boxes), `format` on text (`email`, `phone`, `number`, `date`, `zip`, `ssn`; stored as phone 10 digits, date `YYYY-MM-DD`, ssn `123-45-6789`), `required`, `max_length` (1 to 500), `width` (60 to 540 pt), `show_if` / `required_if`. A condition has `field` and exactly one of `equals`, `not_empty`, `in` (up to 50), `checked` (checkbox) or `includes` (group), or combines two to ten with `all` / `any`, one level deep. Rules are checked again at signing. `constraints` on create (up to 50) are rules across one signer's fields: "constraints": [ { "type": "exactly_one", "fields": ["status"] }, { "type": "at_least", "n": 1, "fields": ["has_license", "newsletter"] }, { "type": "at_most", "n": 2, "fields": ["plan", "paper_copy", "extras"] }, { "type": "one_of", "fields": ["ssn", "no_ssn_reason"] }, { "type": "requires", "field": "address", "when": { "field": "plan", "equals": "annual" } } ] `fields` lists 1 to 20 (a lone field must be a checkbox group). A ticked box, an answered radio or text counts 1; a group counts its ticks; a hidden field counts 0. A bad entry is 422 `invalid_constraints`; a bad `form_fields` entry is 422 `invalid_form_fields`. Both name the entry. Answers come back as `field_values` on `GET /v1/envelopes/{id}` and in every webhook (signer one's in `envelope.partially_signed`, everyone's in `envelope.completed`): "field_values": [ { "name": "status", "signer": 1, "label": "Marital status", "type": "checkbox_group", "value": ["married"], "submitted_at": "..." }, { "name": "has_license", "signer": 1, "label": "Current licence", "type": "checkbox", "value": true, "submitted_at": "..." }, { "name": "phone", "signer": 1, "label": "Phone", "type": "text", "format": "phone", "value": "8135550142", "submitted_at": "..." } ] Text, select and radio values are a string or null; a checkbox is true or false; a group is a list. `value` is null until that signer signs. The GET also returns `form: { fields, constraints }`, the definitions the envelope was created with. Answers are deleted 30 days after the envelope closes (`value` null, `"deleted": true`). Answers are personal data: store and protect them like any other customer data. ## Envelope states created held: a `before` payment or files are still due; not sent, not billed until it goes out delivered sent to the signer viewed the signer opened it partially_signed signer one of two has signed - NOT done completed every signature is in voided voided by you, or expired unsigned 30 days after sending failed could not be delivered `partially_signed` is the one to be careful with. Treating it as finished means telling someone a half-signed contract is executed. ## Retrieve an envelope GET /v1/envelopes/{id} Same object as the create response, with the current state. ## Void an envelope DELETE /v1/envelopes/{id} Cancels an envelope that has not completed. The signing link stops working immediately and the signer sees a cancellation page. Voiding an already-voided envelope succeeds and changes nothing. A completed envelope cannot be voided. Requires a key with read_write scope. ## Collect a payment with the document Add `payment` to the create call and the signer is sent a Siglio-hosted payment page (Stripe) that pays the sender directly. The sender connects their own Stripe account once, in the document studio ("Connect Stripe"); until then the field is refused with code `payment_not_configured` (422). "payment": { "amount": 15000, "description": "Deposit", "when": "after" } amount whole cents (15000 = $150.00), 50 to 99999900 currency "usd" only description optional, up to 120 characters, shown to the signer when "after" (default): document goes out now; the payment link is sent the moment it is completed "before": document is held; the signer gets the payment link instead of the signing link, and the signing link goes out when the payment lands A held envelope stays in state `created`, its `signing_url` is the payment page until it is paid, and it is not billed until the document goes out. Needs `email`, `sms` or `both` delivery. Signer one is the payer. Envelopes with a payment carry a `payment` object wherever an envelope is returned: { "amount": 15000, "currency": "usd", "description": "Deposit", "when": "after", "status": "pending", "paid_at": null, "pay_url": "https://esigndev.com/pay/..." } status pending | paid | waived (marked paid outside Stripe, in the studio) | cancelled (a held envelope was voided) `envelope.paid` fires when the payment is recorded. Siglio adds no fee on top of Stripe's card fees. ## Ask the signer for files Add `attachments` to the create call and the signer is sent a Siglio-hosted upload page (works from a phone camera). "attachments": { "when": "before", "items": [ { "label": "Photo ID" }, { "label": "Proof of renter's insurance", "required": false } ] } when "before" or "after", required, no default "before": document is held; signer gets the upload link instead of the signing link; signing link goes out once every required file is in (and any before-signing payment is paid) "after": upload link is sent the moment it is completed items 1 to 5, label up to 60 characters, required defaults to true Photos (JPG, PNG, HEIC) or PDFs, 15 MB each, up to 3 per item. If the envelope also has a `payment` at the same timing, one page and one message cover both. Envelopes then carry: "attachments": { "when": "before", "status": "pending", "items": [ { "key": "photo_id_1", "label": "Photo ID", "required": true, "files": [ { "id": "att_...", "name": "IMG_2231.jpg", "type": "image/jpeg", "size": 1843200, "uploaded_at": "..." } ] } ], "received_at": null, "upload_url": "https://esigndev.com/step/..." } status pending | received | waived (sender skipped them in the studio) | cancelled (a held envelope was voided) Download a file: GET /v1/envelopes/{id}/attachments/{file} `envelope.attachments_received` fires when every required file is in. Files are never emailed. ## Download the signed PDF GET /v1/envelopes/{id}/document Streams the signed PDF. Only once the envelope is `completed`; earlier states return 404. The response is the file itself, not a redirect. ## Reminders, expiry and retention Signers who have not signed are reminded automatically on the envelope's delivery channels: days 1, 2, 3, 5, 7, 10, 14 and 21 after that signer was asked, then a last reminder on day 28 that states the expiry date (nine at most). Signer two's schedule starts when signer one signs. Reminders go out only between 11:00 and 19:00 US Eastern; text reminders end with "Reply STOP to opt out." They are on for every account, with no webhook events and no API parameter; contact support to turn them off for an account. An envelope not completed 30 days after it was sent is voided automatically: state `voided`, `envelope.voided` fires as for a manual void, and its signing links show "no longer available". There is no reason field. Reminders and expiry apply to envelopes created from 28 Sep 2026 on. Files (the uploaded PDF, the signed PDF and signer attachments) are deleted 30 days after the envelope reaches `completed`, `voided` or `failed`. The envelope record stays readable through `GET /v1/envelopes/{id}`. After deletion the document and attachment downloads return 410 with code `document_deleted`. The signer's own download link also works for 30 days. On `envelope.completed`, download the signed PDF (and any attachments) and store them yourself. ## Large documents POST /v1/uploads { "id": "upl_...", "upload_url": "https://...", "expires_at": "..." } PUT the PDF to `upload_url`, then create the envelope with `"document_id": "upl_..."` instead of `document_base64`. The URL expires in 15 minutes, the file must be a PDF of 15 MB or less, and each upload is consumed by one envelope. Unused uploads are deleted after 24 hours. ## Webhooks Register an endpoint in your Siglio dashboard. Events: envelope.delivered envelope.viewed envelope.partially_signed envelope.completed envelope.voided envelope.paid a requested payment was recorded envelope.attachments_received the signer uploaded every required file Payload: { "id": "evt_...", "type": "envelope.completed", "occurred_at": "...", "data": { "envelope": { ...the envelope object... } } } Events fire on state *transitions*, not on activity, so each one arrives once per envelope and never out of order. Verify the `X-Siglio-Signature` header before trusting the body. Acknowledge fast with any 2xx and process from your own durable queue: there is no manual replay. ## Errors { "error": { "type": "invalid_request", "code": "invalid_request", "message": "...", "request_id": "req_..." } } authentication_error 401 missing, unknown, revoked or expired key authorization_error 403 the key lacks the scope for this call invalid_request 400 malformed or self-contradictory request invalid_document 422 the PDF is unreadable or too large missing_required_tag 422 the tags do not match the signers supplied new_envelopes_paused 403 new envelopes are paused on this account account_paused 403 the account is paused usage_limit_reached 403 out of allowance payment_required 402 the 25 free documents are used and there is no card rate_limited 429 over 120 requests per minute; see Retry-After service_unavailable 503 temporary, safe to retry with your idempotency key internal_error 500 our fault, send us the request_id; nothing is billed, a partly created envelope is marked failed and the free document is given back feature_not_enabled is a code, not a type: it comes back as authorization_error with HTTP 403 when the document has ^M, ^T, ^C or ^R tags, or the request has form_fields, on an account without data collection (Enterprise). Not billed, nothing created. Account codes on POST /v1/envelopes, none billed: company_name_required 422 invalid_request no company name on the account account_under_review 403 authorization_error company name under review, sending paused; contact support invalid_request 422 invalid_request trial over, business profile incomplete; message names fields email_unverified 422 invalid_request confirm the sign-in email before billed sends A request that sends `environment` is accepted and the field is ignored. Features switched off on the service, all 422 invalid_request, not billed: link_only_disabled use email, sms or both instead of none sender_branding_disabled leave sender out payments_disabled leave payment out attachments_disabled leave attachments out two_signer_disabled one signer per envelope Template send codes, all invalid_request, none billed (error.fields lists the values where it applies): template_not_found 404 no such template on this account missing_merge_fields 422 fields: name, label, signer unknown_merge_field 422 fields: name, signer conflicting_merge_values 422 fields: name, signer invalid_merge_value 422 fields: name, signer unsupported_characters 422 fields: name, label, signer signer_count_mismatch 422 signers sent do not match the template template_not_ready 422 a signer on the template has no signature form_fields or constraints with template_id is 422. More than one of document_base64, document_id, template_id, or merge / allow_blanks without template_id, is 400. document_deleted is a code, not a type: it comes back as invalid_request with HTTP 410 from the document and attachment downloads once the files have been deleted, 30 days after the envelope closed. ## What Siglio does not do Stated so you do not design around something that is not there. - No typed answers outside Enterprise. On Enterprise they come back as `field_values`. - Webhook replay is manual, from the dashboard (events since a chosen time, up to 30 days). Acknowledge and queue on your side. - No teams, seats or roles. One key per application. - No IP allowlisting or per-key volume limits. - Two signers maximum. ## Support https://esigndev.com/contact