signit

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.

  1. 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.
  2. Copy the token. Signit shows it once and keeps only a fingerprint.
  3. Save it as the SIGNIT_READ_TOKEN environment 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
  1. Take an id from data and 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 $headers

The 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.

ParameterMeaning
updatedAfterOptional RFC 3339 time, inclusive, for example 2026-10-16T09:30:00.000000Z.
limit1–100, default 50.
cursornextCursor 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, with fieldId, key, type, value, provenance, actorKind, signerId and submittedAt. Signature Fields carry the adopted name.
  • signers: every Signer with status, completedAt, actorKind and the auditEventId of their completion.
  • state.signing: in_progress, completed (every Signer finished), declined, expired, voided or failed. state.terminal is true when no more answers can arrive.
  • state.artifacts: not_applicable until signing completes, then pending, failed if generation needs a retry, then ready with 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.

provenanceMeaning
signer_enteredThe Signer typed or chose it. false here is an explicit "no".
sender_prefill_confirmedThe Sender's suggested text, left unchanged and confirmed.
not_providedOptional text left out: value is null. "" means typed empty.
unchecked_defaultOptional checkbox left untouched: value is false.
adopted_signatureThe 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.

  1. The first run scans without updatedAfter.
  2. Follow nextCursor until hasMore is false. Save that scan's scanThrough as your checkpoint only after every page was processed.
  3. The next run asks for updatedAfter = checkpoint minus five minutes.
  4. Skip an Envelope whose updateRevision you already processed. It grows with every visible change: a submission, a final status, ready documents, a new external reference.
  5. Once a day, scan again without updatedAfter to 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": "..." } }.

StatuscodeWhat to do
400invalid_parameterFix updatedAfter or limit, or remove unknown parameters.
400invalid_cursor, cursor_expiredRestart the run from your checkpoint.
401unauthenticated, invalid_tokenSend a valid, unexpired, unrevoked Read Token.
404not_foundThe 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.
503unavailableSame bounded backoff; stop after a few attempts.
500internal_errorRetry later; quote the requestId if it persists.

On this page