Skip to main content
HoundDog API Documentation

Certificates

A certificate is a document a party (or its agent) submitted as proof of insurance, together with the data HoundDog read from it once it has been reviewed.

Operation Scope
GET /certificates read
GET /certificates/:id read
POST /direct_uploads read_write
POST /certificates read_write
POST /certificates/:id/invalidate read_write
POST /certificates/:id/restore read_write
GET /certificates/:id/download read
GET /parties/:id/insurance_documents read
GET /insurance_documents/:id read

What v2 serves as a certificate

Two kinds of rows are never served by any /certificates route (the list, the show, the download, invalidate and restore: their id is 404) nor in a party's certificates summary:

  • Moved documents. A tax form that was converted into a tax form record, or a document moved to the party's insurance documents. The W-9 is served only as a W-9 under its own rules (W-9s).
  • Tax forms uploaded as certificates. A row the classifier identified as a tax form, whatever its status, including one whose conversion to a tax form record failed. The exception is a row a person checked and decided is not a tax form (an undone conversion): it is served as a certificate again.

A tax form the classifier misses, or one still unclassified (the first moments after upload), is served as a certificate.

Certificates of your individuals are not served either (see Parties).

Parsed data is served after review

HoundDog reads every certificate automatically, and the read values are served only once the certificate is processed: in almost every case after a person has checked what was read (intake); a state-fund workers' compensation proof can also be processed automatically by the pipeline, or converted into one by a person. Until then the read values are not served, exactly as the dashboard does not show them: document_type, effective_on, expires_on, certificate_date, holder, producer, insured and coverages are null until status is Processed. status, intake_status, canceled and the download are always served.

To learn when a certificate's data is available, read the certificate again or subscribe to the Certificate Processed event (see Events). It is recorded whenever a certificate's status becomes Processed: when intake completes its review, when the pipeline automatically processes a state-fund workers' compensation proof, or when a person converts one; once per transition into Processed (a re-key that leaves the status Processed does not record it again). Certificate Submitted is recorded earlier, when the upload is accepted.

The certificate entity

A processed certificate:

{
  "id": 5521,
  "party_id": 191537,
  "project_id": null,
  "document_type": "CERTIFICATE_OF_LIABILITY_INSURANCE",
  "status": "Processed",
  "intake_status": "done",
  "canceled": false,
  "canceled_by": null,
  "canceled_at": null,
  "effective_on": "2026-03-01",
  "expires_on": "2027-03-01",
  "certificate_date": "2026-02-27",
  "holder": { "name": "Example Builders", "address": "1 Plaza", "city": "Austin", "state": "TX", "zip": "78701" },
  "producer": { "name": "Lee Insurance", "address": "9 Elm", "city": "Austin", "state": "TX", "zip": "78702" },
  "insured": { "name": "Acme Electric", "address": "12 Main St", "address2": null, "city": "Austin", "state": "TX", "zip": "78701" },
  "coverages": {
    "gl": {
      "present": true,
      "policy_number": "GL-99812",
      "effective": "2026-03-01",
      "expiration": "2027-03-01",
      "additional_insured": true,
      "subrogation_waiver": true,
      "limits": {
        "each_occurance": "1000000",
        "general_aggregate": "2000000",
        "products_completed_operations": "2000000"
      },
      "fields": {
        "general_aggregate_limit_applies_per_policy": true,
        "general_aggregate_limit_applies_per_project": false,
        "general_aggregate_limit_applies_per_location": false,
        "general_aggregate_limit_applies_other": false,
        "ab_included": false
      }
    },
    "wc": {
      "present": true,
      "policy_number": "WC-4410",
      "effective": "2026-03-01",
      "expiration": "2027-03-01",
      "additional_insured": null,
      "subrogation_waiver": true,
      "limits": {
        "per_statute": true,
        "other": false,
        "each_accident": "1000000",
        "disease_each_employee": "1000000",
        "disease_policy": "1000000"
      },
      "fields": {
        "competitive_state_fund": false,
        "state_exemption": false,
        "monopolistic_proof": false
      }
    },
    "eo": {
      "present": true,
      "policy_number": "EO-3301",
      "effective": "2026-03-01",
      "expiration": "2027-03-01",
      "additional_insured": null,
      "subrogation_waiver": null,
      "limits": { "aggregate_limit": "1000000" },
      "fields": { "deductible": "5000" }
    }
  },
  "uploaded_by_party": true,
  "uploaded_by_agent": false,
  "created_at": "2026-02-28T16:20:00Z",
  "updated_at": "2026-03-01T10:02:00Z",
  "links": { "self": "/api/v2/certificates/5521", "download": "/api/v2/certificates/5521/download", "party": "/api/v2/parties/191537" }
}

The same certificate before review: "status": "Pending", "intake_status": "queued", and document_type, effective_on, expires_on, certificate_date, holder, producer, insured and coverages all null.

Field notes:

  • party_id is null for a certificate uploaded through the universal upload form and still unassigned to a party.
  • document_type is what the classifier decided the document is (for example CERTIFICATE_OF_LIABILITY_INSURANCE, CERTIFICATE_OF_WORKERS_COMPENSATION); null until the certificate is processed.
  • status is the certificate's review state as stored: Pending (received, awaiting processing or review), Processed, Rejected; see intake_status for the review progress.
  • intake_status: processing while automatic reading has not finished, queued once read and waiting for a person to check it, done once processed or rejected. A file sent through a direct upload that never reached storage stays processing; such an upload is recognized by the Certificate Upload Failed event (see Events) and by its download answering 404.
  • canceled is true for an invalidated certificate: it stays on file but no longer counts toward compliance.
  • effective_on and expires_on are the earliest effective and expiration dates among the coverages present on the certificate (for workers' compensation, the certificate's own policy dates; see below for a state-fund proof or a state exemption). certificate_date is the date printed on the certificate. Like the other read values, all three are null until status is Processed.
  • coverages is one map keyed by coverage prefix, each entry the same shape: present, policy_number, effective, expiration, additional_insured, subrogation_waiver, limits, and, for some types, fields. An entry exists for every coverage type whose <prefix>_present box is checked on the certificate or that has any recorded value; present mirrors the box, so an entry with present: false can exist when values were recorded with the box unchecked; a type with neither has no entry. The prefixes include gl (General Liability), auto, wc (Workers Compensation), umb (Umbrella), pl (Professional Liability), im (Inland Marine), gk (Garage Keepers), cg (Cargo), oh (On Hook), pollution, re (Rental Equipment), ava (Aviation), db (Data Breach), pa (Personal Auto), br (Builder's Risk), ti (Trailer Interchange), cl (Cyber Liability), am (A&M), ab (Assault & Battery), and any other coverage type HoundDog defines (eo in the example). The set of coverage types can grow: discover it with GET /coverage_types and treat an unknown prefix as a coverage like any other, rather than hardcoding the list.
  • Where a value lives in an entry follows one rule, the same one GET /coverage_types states for certificate_key: certificate_key names where the compared certificate value lives in coverages.<prefix>: present, additional_insured and subrogation_waiver at the entry's top level; the limit amounts in limits (every column whose name contains limit, except the General Liability general_aggregate_limit_applies_* boxes); everything else in fields (the type's other certificate fields, the GL aggregate-applies boxes and ab_included among them).
  • limits carries every limit the certificate records for that coverage: each key is the stored field name with <prefix>_ removed and then every leading liability_, limits_ or limit_ segment removed, repeatedly (so auto_liability_limits_combined_single is combined_single and auto_liability_limits_custom_limit is custom_limit, gl_limits_each_occurance is each_occurance, umb_limits_each_occurrence is each_occurrence, re_limit_coverage is coverage, ab_occurrence_limit is occurrence_limit, since a trailing _limit is kept); values are the stored strings, exactly as read, except the workers' compensation statutory boxes per_statute and other, which are booleans. A custom_name key is the label of a custom limit, a stored string.
  • fields carries a coverage type's other recorded values, keyed by certificate key, for example a deductible. For General Liability they include, among others, the boxes saying what the general aggregate applies to (general_aggregate_limit_applies_per_policy, _per_project, _per_location and _other, booleans, and general_aggregate_limit_applies_other_text, a string), ab_included (the policy includes Assault & Battery coverage), primary_non_contributory, coverage_trigger_occurance, coverage_trigger_claims_made, ll_endorsement and pool_pop_up_endorsement (booleans); for Workers Compensation, for example primary_non_contributory and exclusions (booleans), the state exemption values (state_exemption*), and, for a state-fund (monopolistic) workers' compensation proof, monopolistic_proof (boolean), monopolistic_jurisdiction (the state, e.g. "OH") and monopolistic_exp_date (an ISO date); a state-fund proof therefore appears as a wc entry even when its present box is unchecked. The wc entry's effective and expiration (and the certificate's effective_on and expires_on) are always the certificate's own workers' compensation policy dates; a state-fund proof's or a state exemption's own dates are only in fields (monopolistic_exp_date, state_exemption_eff_date, state_exemption_exp_date). Compliance judges a party by those dates instead only when its requirement accepts state-fund proofs or state exemptions. fields is omitted when the type has none.
  • Inside limits and fields, a value never recorded is left out (never null), while a box recorded unchecked is false. Members of the entity itself are always present, null when there is no value.

GET /certificates

Filters: party_id, since (ISO-8601, certificates created since then), plus page, per_page. A party_id of another account's party is 422 validation_failed naming party_id. project_id filters to one project's certificates. A party_id of one of your individuals is also 422.

GET /certificates/:id

The entity. 404 for an id v2 does not serve (see above).

Uploading a certificate

Two ways: send the file in the request, or upload it straight to storage first (better for large files and slow links) and pass a token.

POST /direct_uploads

Reserves a storage location for one upload. Body: filename (required), content_type.

{
  "url": "https://<bucket>.s3.<region>.amazonaws.com",
  "fields": { "key": "uploads/certificates/direct/...", "policy": "...", "x-amz-signature": "...", "...": "..." },
  "direct_upload_token": "eyJ...",
  "expires_at": "2026-10-07T15:03:11Z"
}

POST the file to url as multipart/form-data with every entry of fields first and the file last (up to 50 MB). Then create the certificate with the token. The token is bound to your account and expires; a token minted by another account is 422. This endpoint's response holds a one-time value, so it is never replayed under an Idempotency-Key (see Idempotency).

POST /certificates

multipart/form-data with:

Field Meaning
party_id Required
file The document (PDF, JPG or PNG for automatic reading). Send this or direct_upload_token
direct_upload_token From POST /direct_uploads, after the file reached storage
project_id Optional: the project the certificate is for. An empty value (null, or an empty form value) means no project

project_id needs projects on the account (enable_projects): any project is 403 feature_disabled naming the field. An empty value (null, or an empty form value) is what a new certificate gets anyway, so it is accepted and ignored.

Under project compliance, certificates count per project: send project_id with a certificate that is for one.

Response 201 with the entity (its parsed data null until processed). A Certificate Submitted event is recorded when the upload is accepted. The certificate then goes through the normal pipeline: automatic reading, then intake, then a compliance recompute. When its status becomes Processed, Certificate Processed is recorded and the party's status can change, which records Vendor Became Compliant or Vendor Became Noncompliant and fires their webhooks.

Errors: 400 (neither or both of file and direct_upload_token), 422 validation_failed (party_id not in your account, an expired or foreign token). A project_id not in your account is also 422 validation_failed.

POST /certificates/:id/invalidate

Invalidates the certificate (it stays on file and stops counting) and recomputes the party's compliance, which can make the party non-compliant and start a new request. Event: Certificate Invalidated. canceled_by reads API key "<key name>". Response 200 with the entity.

POST /certificates/:id/restore

Reverses an invalidation and recomputes compliance. Event: Certificate Restored. Response 200 with the entity.

GET /certificates/:id/download

Answers 302 with a short-lived link (ten minutes) to the stored file, whatever the certificate's status. Fetch it at once; do not store the link. 404 when there is no file: a direct upload that never reached storage (see intake_status above), or a certificate with no stored file.

Insurance documents

Insurance documents are the endorsements and other policy paperwork kept on a party beside its certificates: an additional insured endorsement, a waiver of subrogation, a primary and noncontributory endorsement, and the like. They are read-only through the API. Insurance documents of your individuals are not served (an individual's id is 404).

{
  "id": 2207,
  "party_id": 191537,
  "name": "AI endorsement 2026",
  "document_type": "Additional Insured Endorsement",
  "policy_number": "GL-4471023",
  "external_id": null,
  "file_url": "https://...",
  "created_at": "2026-09-14T15:20:00Z"
}
  • document_type is the type chosen on the dashboard: Additional Insured Endorsement, Subrogation Waiver, Loss Payable Endorsement, Primary & Noncontributory Endorsement, Endorsement or Other. Treat it as text: a document moved from the certificate review may carry another value, or none.
  • name and policy_number may be null.
  • file_url is a short-lived link to the file (two days), never null. Fetch it when you need the bytes; do not store it.

GET /parties/:id/insurance_documents

The party's insurance documents, newest first, in the collection envelope. 404 when the party is not in your account.

GET /insurance_documents/:id

One insurance document.