Field results API
Read your Envelopes' fields and submitted answers from your own systems.
Signit has three read-only HTTP endpoints for the Text, Checkbox and Signature Fields of the Envelopes you send. Use them to copy answers into a CRM, ERP or spreadsheet. They return only answers a Signer has already submitted; unsubmitted answers stay private to the Signer. Drafts are not visible.
A Read Token can only read. It cannot create, send, sign or change anything.
First request in five minutes
Start signed in to Signit, with at least one Envelope that a Signer has finished.
- Open Agent connection → Field results API (or go to
https://app.sign-it.ro/settings/api). Name a token, choose how long it lasts and select Create token. - Copy the token. Signit shows it once and keeps only a fingerprint.
- Save it as the
SIGNIT_READ_TOKENenvironment variable, then list your recently changed Envelopes:
curl -sS -H "Authorization: Bearer $SIGNIT_READ_TOKEN" \
"https://app.sign-it.ro/api/v1/envelopes?limit=10"const response = await fetch("https://app.sign-it.ro/api/v1/envelopes?limit=10", {
headers: { Authorization: `Bearer ${process.env.SIGNIT_READ_TOKEN}` },
});
if (!response.ok) throw new Error(`Signit returned ${response.status}`);
const page = await response.json();
console.log(page.data);$headers = @{ Authorization = "Bearer $env:SIGNIT_READ_TOKEN" }
Invoke-RestMethod -Uri "https://app.sign-it.ro/api/v1/envelopes?limit=10" -Headers $headers- Take an
idfromdataand read its submitted values. Each Envelope page in Signit also shows these requests, ready to copy, under Field results.
curl -sS -H "Authorization: Bearer $SIGNIT_READ_TOKEN" \
"https://app.sign-it.ro/api/v1/envelopes/ENVELOPE_ID/field-results"const response = await fetch("https://app.sign-it.ro/api/v1/envelopes/ENVELOPE_ID/field-results", {
headers: { Authorization: `Bearer ${process.env.SIGNIT_READ_TOKEN}` },
});
if (!response.ok) throw new Error(`Signit returned ${response.status}`);
const results = await response.json();
console.log(results.values);$headers = @{ Authorization = "Bearer $env:SIGNIT_READ_TOKEN" }
Invoke-RestMethod -Uri "https://app.sign-it.ro/api/v1/envelopes/ENVELOPE_ID/field-results" -Headers $headersThe five-minute target covers creating the token and these requests. Creating your account and getting a sample signed are not part of it.
Read Tokens
- Read-only, for the Envelopes of the organization you have selected when the request arrives. If you switch organization, the same token reads the other one.
- Valid for 30 days by default and at most 90. You can have 10 active tokens.
- Revoke a token at any time in Field results API; the next request fails.
- Every request is checked against your account as it is now. If your account is removed or can no longer send, its tokens stop working.
- Read Tokens and agent connections are separate: the agent connector does not accept a Read Token, and these endpoints do not accept an agent secret.
- Store the token in a secret store. Read responses, URLs and logs never contain it.
Send it as Authorization: Bearer <token>. Every response is
Cache-Control: private, no-store.
Endpoints
GET /api/v1/envelopes
Discovery: your sent Envelopes in (updatedAt, id) order.
| Parameter | Meaning |
|---|---|
updatedAfter | Optional RFC 3339 time, inclusive, for example 2026-10-16T09:30:00.000000Z. |
limit | 1–100, default 50. |
cursor | nextCursor from the previous page. Repeat the same other parameters, or none. |
The response has data (each item: id, status, subject,
externalReference, signingFormatVersion, definitionRevision,
updateRevision, updatedAt, sentAt, completedAt), hasMore,
nextCursor and scanThrough. Times are whole milliseconds on Signit's app clock,
written with six decimals; the last three are always 000. updatedAfter keeps
only whole milliseconds, so a sub-millisecond part is dropped and the filter can
return a little more than you asked. Deduplicate by updateRevision, as the steps
below describe.
GET /api/v1/envelopes/{id}/fields
The Field Definition frozen when the Envelope was sent: each field's id,
key, type, label, required, owning signerId, position, and for Text
Fields multiline and prefill. A prefill with locked: true is the
Sender's own text: it is part of the definition, never a Signer answer. Your
metadata pairs appear here and never reach Signers. signers lists the
Signers, envelope.definitionRevision identifies this definition.
GET /api/v1/envelopes/{id}/field-results
Only submitted values:
values: one entry per field of every Signer who finished, withfieldId,key,type,value,provenance,actorKind,signerIdandsubmittedAt. Signature Fields carry the adopted name.signers: every Signer withstatus,completedAt,actorKindand theauditEventIdof their completion.state.signing:in_progress,completed(every Signer finished),declined,expired,voidedorfailed.state.terminalistruewhen no more answers can arrive.state.artifacts:not_applicableuntil signing completes, thenpending,failedif generation needs a retry, thenreadywith the Final PDF and Certificate digests. Signing finished and documents ready are separate moments.
Answers of a Signer who has not finished never appear, so results can be partial while signing is in progress.
For signature-only Envelopes, field identities come from the recorded completion
evidence and use their frozen keys. These values include
source: "legacy_signature_evidence" and evidence: { signerId, auditEventId }.
If an older completion's field identities cannot be recovered, the response
uses projectionStatus: "legacy_evidence_only" with signerSignatures rather
than inventing field IDs. Submitted evidence remains available.
provenance | Meaning |
|---|---|
signer_entered | The Signer typed or chose it. false here is an explicit "no". |
sender_prefill_confirmed | The Sender's suggested text, left unchanged and confirmed. |
not_provided | Optional text left out: value is null. "" means typed empty. |
unchecked_default | Optional checkbox left untouched: value is false. |
adopted_signature | The name the Signer adopted for a Signature Field. |
actorKind is human_ui when the Signer used Signit, or agent_credential
when their connected agent signed for them.
Each response is read from one consistent database snapshot. Two requests can
differ if something changed in between: compare definitionRevision when you
combine fields with field-results.
External reference
On an Envelope page, in the External reference card, you can save your own ID for it
(up to 200 characters). It comes back as externalReference and changing it
counts as an update.
Polling for changes
Discovery is bounded polling, not a guaranteed event stream: a change can show up late, and an Envelope missing from a page is not deleted.
- The first run scans without
updatedAfter. - Follow
nextCursoruntilhasMoreisfalse. Save that scan'sscanThroughas your checkpoint only after every page was processed. - The next run asks for
updatedAfter= checkpoint minus five minutes. - Skip an Envelope whose
updateRevisionyou already processed. It grows with every visible change: a submission, a final status, ready documents, a new external reference. - Once a day, scan again without
updatedAfterto reconcile.
A cursor is valid for 15 minutes, for the same token owner and parameters.
After invalid_cursor or cursor_expired, start the run again from your
checkpoint.
Run the following polling job once per minute. Supply durable handle and saveState functions as described in the example. It restarts expired cursors at most three times and performs a full rescan at least daily. A failed later page retains the last completed checkpoint. Stop on 401, correct any 400 parameters, and retry the next scheduled run after outages.
A complete example:
// signit-poll-example
const OVERLAP_MS = 5 * 60 * 1000;
const RECONCILE_MS = 24 * 60 * 60 * 1000;
async function getJson(url, token, attempt = 0) {
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
if ((response.status === 429 || response.status === 503) && attempt < 5) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs =
Number.isFinite(retryAfter) && retryAfter > 0
? Math.min(60_000, Math.max(1_000, retryAfter * 1000))
: Math.min(60_000, 1000 * 2 ** attempt);
await new Promise((resolve) => setTimeout(resolve, delayMs));
return getJson(url, token, attempt + 1);
}
const body = await response.json();
if (!response.ok) {
throw Object.assign(new Error(body.error?.message ?? `Signit returned ${response.status}`), {
code: body.error?.code,
status: response.status,
});
}
return body;
}
// Run once per minute with the latest durably loaded state, without concurrent runs.
// state = { checkpoint: null, lastReconciledAt: null, seen: {} }.
// handle must durably apply each idempotencyKey at most once. A crash between
// handle and saveState can repeat it. saveState must atomically persist its argument.
async function pollSignit({
baseUrl,
token,
state,
handle,
saveState,
now = Date.now,
limit = 100,
}) {
const fullScan =
!state.checkpoint ||
!state.lastReconciledAt ||
now() - Date.parse(state.lastReconciledAt) >= RECONCILE_MS;
const filters = new URLSearchParams({ limit: String(limit) });
if (!fullScan) {
filters.set("updatedAfter", new Date(Date.parse(state.checkpoint) - OVERLAP_MS).toISOString());
}
const persist = async (next) => {
await saveState(structuredClone(next));
Object.assign(state, next);
};
for (let restart = 0; ; restart++) {
let cursor = null;
let scanThrough = null;
try {
do {
const query = new URLSearchParams(filters);
if (cursor) query.set("cursor", cursor);
const page = await getJson(`${baseUrl}/api/v1/envelopes?${query}`, token);
scanThrough = page.scanThrough;
for (const envelope of page.data) {
if ((state.seen[envelope.id] ?? -1) >= envelope.updateRevision) continue;
let results;
try {
results = await getJson(
`${baseUrl}/api/v1/envelopes/${envelope.id}/field-results`,
token,
);
} catch (error) {
// Unavailable to this credential is not proof of deletion.
if (error.status === 404) continue;
throw error;
}
const revision = results.envelope.updateRevision;
await handle(results, { idempotencyKey: `${envelope.id}@${revision}` });
// Business data first, then durable dedupe, with the old checkpoint.
await persist({ ...state, seen: { ...state.seen, [envelope.id]: revision } });
}
cursor = page.nextCursor;
} while (cursor);
// Only a fully processed scan advances either completed checkpoint.
await persist({
...state,
checkpoint: scanThrough,
lastReconciledAt: fullScan ? new Date(now()).toISOString() : state.lastReconciledAt,
});
return state;
} catch (error) {
if (["invalid_cursor", "cursor_expired"].includes(error.code) && restart < 3) continue;
// Keep the last completed checkpoint on outage, bad parameters or lost authority.
// Correct invalid_parameter input or renew credentials before the next scheduled run.
throw error;
}
}
}Errors
Errors look like { "error": { "code": "...", "message": "...", "requestId": "..." } }.
| Status | code | What to do |
|---|---|---|
| 400 | invalid_parameter | Fix updatedAfter or limit, or remove unknown parameters. |
| 400 | invalid_cursor, cursor_expired | Restart the run from your checkpoint. |
| 401 | unauthenticated, invalid_token | Send a valid, unexpired, unrevoked Read Token. |
| 404 | not_found | The Envelope does not exist, is not yours, or is still a Draft. |
| 429 | — | Wait Retry-After, or back off from 1 s doubling to 60 s. |
| 503 | unavailable | Same bounded backoff; stop after a few attempts. |
| 500 | internal_error | Retry later; quote the requestId if it persists. |