Skip to main content
HoundDog API Documentation

Document signing

Documents your parties sign electronically (agreements, waivers, forms), sent from signing templates through the e-signature service, or recorded when they were signed elsewhere.

Operation Scope
GET /signing_templates read
GET /signing_templates/:id read
GET /signing_requests read
GET /signing_requests/:id read
POST /signing_requests read_write
POST /signing_requests/:id/resend read_write
POST /signing_requests/:id/invalidate read_write
POST /signing_requests/upload_external read_write
GET /signing_requests/:id/download read

Rules for every signing route

  • Reads always work; writes need document signing on. Every write is 403 feature_disabled when document signing is off on the account.
  • No signing link is ever returned. The link a signer uses is sent to the signer by email and shown only on the party's own compliance portal. Neither it, nor any provider id or token, appears in any response, so a signing seat's entry in the party's outreach has link null. To reach a signer who lost the email, use resend.
  • Templates are read-only through the API. Build and edit them on the dashboard.
  • Individuals: the signing requests of your individuals are 404 on every /signing_requests/:id route and never listed; their ids as party_id in a body or query are 422 naming party_id.
  • Order of checks on a write: the feature gate (403 feature_disabled), then ids in the path (404), then the body (422 validation_failed, and the 422 signing_template_invalid cases known before any call to the e-signature service), then conflicts (409 conflict), then the e-signature service (422 signing_template_invalid or 502 upstream_failed). A request refused by a validation is never answered with a conflict.
  • updated_since sees record edits. A request marked no longer needed (by any path) and a request completing do update updated_at; signatures and reminder counters on its recipients do not.

Signing templates

{
  "id": 7,
  "name": "Master Subcontract Agreement",
  "description": null,
  "document_name": "Subcontract Agreement",
  "active": true,
  "repeatable": false,
  "send_automatically": false,
  "sending_trigger": null,
  "trigger_coverage_prefix": null,
  "state_restriction": { "type": null, "states": [] },
  "adds_approval_override": true,
  "approval_override_coverage_prefix": "wc",
  "approval_override_term": { "type": "years", "amount": 1, "prefilled_field_id": null },
  "succeeds_template_id": null,
  "recipients": [
    { "id": 21, "role_name": "Subcontractor", "signer": "party", "name": null, "email_address": null },
    { "id": 22, "role_name": "Project Manager", "signer": "fixed", "name": "Sam Ortiz", "email_address": "[email protected]" }
  ],
  "prefilled_fields": [
    { "id": 40, "name": "Waiver End Date", "field_type": "single_line_text_field", "source": "value", "required": false }
  ]
}
Field Meaning
document_name The name signers see, when it differs from name
repeatable The template allows several open requests per party at once (for example one per payment draw)
send_automatically, sending_trigger, trigger_coverage_prefix Automatic sending, as configured on the dashboard
state_restriction type is only_selected, exclude_selected or null; states are two-letter codes
adds_approval_override, approval_override_coverage_prefix, approval_override_term Completing the document grants an approval override for that coverage. approval_override_term.type is forever, days, years, prefilled_field (the end date comes from a prefilled field, prefilled_field_id) or field (a legacy term read from the signed document). The whole term is null when the template adds no override. prefilled_field_id is null when that field is missing or disabled: such a template cannot be sent or uploaded through the API (422 signing_template_invalid)
succeeds_template_id The template this one replaces as a newer version
recipients The signing seats: signer is party (filled with the party's contact for that seat) or fixed (always the same person; only fixed seats show a name and email_address)
prefilled_fields Values written into the document before it is sent. source is party (from the party's record), date (computed by HoundDog) or value (given by the sender). Definitions only, never values. required is true only for the field an override term reads, when the template adds an override and that field takes a value with no default

GET /signing_templates

Filter: active (true or false). Collection envelope.

GET /signing_templates/:id

One template.

Signing requests

{
  "id": 9012,
  "party_id": 191537,
  "project_id": null,
  "signing_template_id": 7,
  "status": "pending",
  "title": "Master Subcontract Agreement for Acme Electric",
  "message": null,
  "signing_order": "sequential",
  "completed_at": null,
  "invalidated_at": null,
  "invalidated_reason": null,
  "uploaded_externally": false,
  "concurrent": false,
  "recipients": [
    { "id": 30111, "name": "Dana Ruiz", "email_address": "[email protected]", "role_name": "Subcontractor", "signer": "party",
      "signed": false, "signed_at": null, "contact_count": 1, "last_contacted": "2026-10-07T14:03:30Z" }
  ],
  "created_at": "2026-10-07T14:03:11Z",
  "updated_at": "2026-10-07T14:03:11Z",
  "links": { "self": "/api/v2/signing_requests/9012", "party": "/api/v2/parties/191537" }
}
Field Meaning
status pending, completed or no_longer_needed
party_id null for an old request sent to a person outside any party (no longer possible to create)
title The display name: the stored title, or when none was stored, "<template name> for <party name>" (which shows the template's internal name)
signing_order sequential or parallel, served lowercase (stored uppercase); null for an external upload
invalidated_at, invalidated_reason When and why the request was marked no longer needed
uploaded_externally Recorded with upload_external
concurrent Created as one of several allowed open requests of a repeatable template
recipients One per seat; empty for an external upload. No link of any kind
links self, party (when the request has one), and download once the request is completed

Fields of features your account does not use are present with an empty value.

The approval override a completed request generates is not linked from this entity; read the party's approval overrides.

GET /signing_requests

Filters: party_id, signing_template_id, project_id, status, updated_since, plus page, per_page. Old party-less requests are listed; party_id excludes them.

GET /signing_requests/:id

One request.

POST /signing_requests

Builds a document from a template, creates it in the e-signature service and sends it.

Body field Meaning
signing_template_id Required; the template must be active (422 naming it otherwise)
party_id Required
project_id Scopes the one-open-request rule to that project
title Repeatable templates only (422 naming title for any other template; at most 255 characters): the label signers see for this request. Omitted, it is stored blank (the template's internal name is never used as the label)
message A note included in the signing email
prefilled_values {"<prefilled field id>": "value"} for fields whose source is value, each value a string. Naming any other field is 422 naming prefilled_values.<id>. The field an override term reads takes an ISO 8601 date (2027-03-01) and is required when the template adds an override, its term type is prefilled_field, and that field takes a value with no default
supersede Default false; see conflicts below

Seats are filled as on the dashboard. With contact routing, each party seat is filled by the contact who holds that seat's role (per-slot holder), else the contact who holds the signing role (generic holder), else the primary contact. Without contact routing, every party seat is the primary contact. A party seat that resolves to no email address is 422 validation_failed naming party_id; a blank fixed seat is 422 signing_template_invalid. A template whose override term is prefilled_field is 422 signing_template_invalid when the override could never be granted: the term's field is missing or disabled, the field is filled from the party's record, or the field's own value (its default, or a computed date, when you give none) is not a date the override can be read from (a value you give must be an ISO 8601 date). project_id is 403 feature_disabled naming the field: it is accepted only on project-compliance accounts. Sent empty (null or blank) it is ignored, on the create and on upload_external.

Conflicts. When the party (and the project_id, where given) already has an open, non-repeatable request for this template, the create is 409 conflict, its conflict member naming that request ({"resource": "signing_request", "id": n}). Send supersede: true to mark the open request no longer needed (Signing Request Invalidated) and create the new one in one transaction: if the create then fails, the open request stays open. Repeatability is read per open request (a request created as concurrent, or whose template now allows several open requests), and a repeatable open request never conflicts and is never superseded, so a repeatable template's create always adds a request. If another process opens a request for the same template at the same moment, you get 409 conflict naming the winner.

The response may already be no_longer_needed (201): when the party already meets what the document would grant, it is created closed (on accounts where closing satisfied requests is on, the default; not for archived, paused or incomplete parties).

What goes out. The initial emails are vendor outreach: with vendor email off nothing is emailed (and a signing request sends no SMS). An archived party is 422; a paused party is accepted. On a multi-factor compliance party with two or more required factors, only seats routed away from the primary contact are emailed at creation; the primary's own seats are covered by the combined compliance email, and fixed co-signer seats are first emailed by the signing follow-up schedule or when an earlier seat signs. On such a party, a request for a template the profile does not require, or whose signing requirement is already satisfied, is emailed to its party seat by no schedule at all: use resend to reach the signer. Any write that creates a record on such a party can email the main contact at once (the combined email).

E-signature service failures. 422 signing_template_invalid (naming the template) when the template cannot be sent as configured: a signer seat with no signature field, any refusal from the service other than rate limiting, a request timeout or an authorization failure (a template it no longer has included), or a create answer that cannot be matched to the template's signing seats. 502 upstream_failed for a template read that answers without a usable template, rate limiting, a request timeout, an authorization failure on our side, or a server error that persists after retries, a timeout or a dropped connection, and when the request's time budget for the e-signature service (about 20 seconds in all) runs out. The service's own message is never relayed. In every failure case no request is kept, and a superseded request stays open. Retry a 502 with the same Idempotency-Key.

Response 201 with the entity. Events: Message Sent for each initial email sent.

POST /signing_requests/:id/resend

Sends one reminder to each current signer who has not signed and has an email address, and records one Manual Signing Reminder Sent event for each. No body.

  • 200 with the entity even when nobody was emailed (every signer signed or has no address).
  • A signer whose address is suppressed is not emailed either (still 200). A suppressed address keeps its Manual Signing Reminder Sent event and gains a child Email Skipped (Suppressed) event linked by parent_event_id. To learn who was actually reached, read the children: GET /events?parent_event_id=<reminder event id>.
  • 422 validation_failed when the request is completed or no longer needed, when the party is archived, or when the account cannot send vendor email ("Vendor email is disabled for this account"). A paused party is reminded.

POST /signing_requests/:id/invalidate

Marks the request no longer needed. One-way. Body: reason (optional, at most 255 characters; stored as invalidated_reason, which the party can see on the request page; omitted, it reads "marked No Longer Needed through the API"). Event: Signing Request Invalidated.

  • 422 validation_failed when it is already completed or already no longer needed (including when another process closed it at the same moment).
  • Recomputes the party's compliance. On a multi-factor compliance party whose profile requires this template, a replacement request follows: created and sent a few seconds later in the background when nothing else satisfies the requirement, or at the next nightly renewal check when the last signature has lapsed. Nothing is re-created for an archived or paused party or on an account that disables requests (the invalidation itself is accepted). On a party with two or more required factors the combined compliance email can follow under its own schedule.
  • The response is the invalidated request and never contains the replacement. Poll GET /signing_requests?party_id=<id>&status=pending (or use updated_since) to find it.

POST /signing_requests/upload_external

Records a document signed outside the e-signature service as completed. multipart/form-data:

Field Meaning
file Required: the signed document
signing_template_id, party_id Required. An inactive template is accepted
project_id The project the document is for
supersede Default false; see conflicts
prefilled_values In this multipart request prefilled_values is a JSON-encoded string, for example prefilled_values={"40":"2027-03-01"}, holding only the override-term field, under the create's rule (422 naming prefilled_values when it is not a JSON object; the create's 422 signing_template_invalid cases for a prefilled_field term apply too)
  • completed_at is the upload time. recipients is empty.
  • Conflicts follow the create's rule: an open, non-repeatable request for the same template version and the same project (no project matches no project) is 409 conflict unless supersede: true, which marks it no longer needed ("satisfied by an externally uploaded signed document") in the same transaction as the upload. Repeatable open requests are never touched.
  • The override term. When the template adds an approval override, the upload grants it for a forever, days or years term, and for a prefilled_field term from the value you send in prefilled_values. A template whose term is the legacy field type is 422 signing_template_invalid, before anything is created.
  • An archived party is 422 validation_failed, and so is an empty file.

Side effects: the granted override recomputes compliance, so the party can become compliant (Override Added, then the compliance events). Where the account's JobTread integration copies signed documents and the party is linked, the file is pushed to JobTread. Event: External Signed Document Uploaded, naming your key. On a multi-factor compliance account the party's compliance is recomputed at once.

Re-read the party's requests after an upload. A request another process opened in the same moment stays open, and every compliance recompute also closes any other open request whose override is now satisfied (Signing Request Invalidated; on accounts where closing satisfied requests is on, the default; not for archived, paused or incomplete parties), possibly for other templates, versions or projects than the one you uploaded. This applies to every write on this page that recomputes compliance. It applies to every write on W-9s that recomputes compliance too.

Response 201 with the entity.

GET /signing_requests/:id/download

Streams the signed document (Content-Disposition: attachment). For a document signed through the e-signature service it is the copy without the service's audit page (which lists signer IP addresses); when that copy cannot be produced, the full document with the audit page is served, as on the dashboard. For an external upload, the stored file under its own name and type. 404 until completed and when there is nothing to download; 502 upstream_failed when the document cannot be retrieved in time (one budget of about 25 seconds covers the whole retrieval; when little of it is left after the fetch, the full document with the audit page is served instead of the stripped copy). Against a very slow e-signature service the time limit is best effort: each read is bounded, but a read that keeps trickling data can run past it.