Docs / API and agents
Signatures API
Send a credit agreement or personal guaranty for signature, sign it on an emailed link, and read the sealed copy: routes, states, permissions, errors.
An approved application can be sent for signature as a credit agreement (the customer's authorized signer signs, then the supplier countersigns) or as a personal guaranty (one named individual signs). Signing is native: VendorLogic renders the document, emails each signer a single-use link, records the signature and seals a signed copy with a certificate of completion. All routes are under /v1.
Routes#
| Method and path | Who | What |
|---|---|---|
POST /applications/:id/signature-requests | signatures.send, supplier side of the application | create a request; needs an Idempotency-Key header |
GET /applications/:id/signature-requests | signatures.view, either side | requests of one application, newest first (at most 50) |
GET /signature-requests/:id | signatures.view, either side | one request; the supplier also gets signer addresses and the event log |
POST /signature-requests/:id/void | signatures.void, supplier | cancel an open request, { reason } |
POST /signature-requests/:id/remind | signatures.send, supplier | new links to signers who have not acted, at most once a day each |
POST /signature-requests/:id/resolve | signatures.review, supplier | settle a request in needs_review: { action: "accept" or "void", reason } |
GET /signature-requests/:id/documents/:docId | signatures.view, either side | a 60 second link to the unsigned or the sealed file (audited) |
GET /sign/:token | public, the emailed link is the credential | what the signing page shows |
POST /sign/:token/sign | public | { typedName, signatureImage, consent: true } |
POST /sign/:token/decline | public | decline; ends the request |
A request of another organization is a 404, never a 403.
Create#
Body: kind (credit_agreement or personal_guaranty), expiresInDays (1 to 60, default 14). An agreement may name customerSigner (name, email); it defaults to the application's contact. The supplier countersigner is the person sending. A guaranty needs principalId (a principal named on the application, never chosen for you) and guarantorEmail.
| Status | When |
|---|---|
| 201 | { id, status: "draft", replayed }; the same key and kind answers again with replayed: true |
| 400 | invalid body, no Idempotency-Key, or a principal that is not on the application |
| 403 | no signatures.send |
| 404 | no such application, or it belongs to another organization |
| 409 | the application is not approved (not_approved), the key was used for something else (idempotency_conflict), or an open request of that kind already exists for that application and guarantor (open_request_exists) |
The terms printed on the document (limit, payment days) come from the latest approval record, not from anything the caller sends. When the supplier has published its own terms of the matching kind, the document is the schedule page followed by those terms, and the request records the version and hash it carried; see Supplier terms. Without them, the platform template is used, and its wording is placeholder text until counsel has reviewed it.
States#
draft (created, document being made) then sent (links emailed) then in_progress (someone signed) then completed (everyone signed) then sealed (signed copy stored, scanned and checked). Exits: declined, expired, voided, failed, and needs_review. Only sealed means the agreement is in force; completed shows as "Finishing".
The customer signs first and the supplier second; signers of the same order (none today) would sign in parallel. A signer gets a link only when it is their turn. Reminders go out on day 3 and day 7; after the second the supplier is told in the app. Requests expire at their deadline (hourly job).
Sealing#
After the last signature a queue job builds the signed copy: the unsigned pages exactly as the signers saw them, a signature page with each drawn signature, and a certificate of completion (signers, times, addresses, hashes, event log). The copy is scanned like any file. It is sealed only when the scan is clean, its SHA-256 matches the recorded one and the signers match the request. Anything else sets needs_review and tells the supplier. An accepted review is audited with its reason; a copy that failed the scan cannot be accepted, only cancelled and sent again. Everyone who signed then gets a fresh link to download their copy.
The signed copy carries no cryptographic PDF signature. A copy altered outside VendorLogic is detectable by comparing its SHA-256 with the one recorded in append-only rows and in the audit trail. The upgrade path is a PAdES signature or a connector provider.
The signing page#
The link holds 256 random bits, only its hash is stored, it lasts 30 days and belongs to one signer. A reminder replaces it. A wrong link is 404, an expired one 410. The page returns the document (a 60 second link, only after the scan released it), the consent text and where the signer stands; canSign is false while the file is still being prepared. Signing needs the consent, a typed name and a drawn PNG or JPEG signature. The signer's address and browser are recorded with the signature.
Evidence and retention#
Events, audit rows (signature.*: ids and times, never names, addresses or images) and a sealed request are append-only or immutable in the database. Signer personal data follows the retention clock of the application; the purge job may overwrite it only after retention and with no legal hold. A sealed document is never removable.
Related#
Setting and activation#
GET /v1/signatures/setting and PUT /v1/signatures/setting (documents.requirements.manage) read and set the supplier's mode: off, optional or required. POST /v1/applications/:id/activate (applications.decide) activates an approved application once. With required, it answers 409 signing_pending until the credit agreement and the newest request of every guarantor are sealed (cancelled requests are ignored). A signatures.review holder may send overrideReason (5 to 500 characters) to go ahead; the reason is kept in the audit event.
Signing through a connector (Documenso)#
If the supplier has an installed, healthy connector that holds the signatures:fulfil scope, new requests are created with provider connector:<slug> instead of native.
Core still writes the PDF, scans it and decides what counts; the connector only carries it to the provider.
- After generation the request waits queued. No native signing links are emailed and
/sign/<token>has nothing for these signers. - The connector pulls queued work, creates the request at the provider (the provider emails each signer in order) and reports its id: sent. Signers are pending at the provider.
- The connector reports each signer step (viewed, signed, declined) with the provider's own event id, so a retry lands once; each step is a signature event with source
connector:<slug>and an audit row. - When the provider holds the signed copy the connector uploads it. Core stores it as a document, scans it and then runs the same seal checks as a native copy (clean scan, matching hash, the right signers all signed).
A mismatch is needs review. The provider's audit log is stored beside it as a
signature_certificatedocument. - Remind is refused for these requests (the provider reminds); the reminder cron skips them. Void and expire archive the request at the provider on the connector's next pass.
- A provider that keeps refusing the document fails the request (
failed); send it again. - Uninstalling the connector (or it reporting
needs_attention) makes new requests native again. Requests already with the provider keep their provider.
The connector API is described in Connectors.