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_idisnullfor a certificate uploaded through the universal upload form and still unassigned to a party.document_typeis what the classifier decided the document is (for exampleCERTIFICATE_OF_LIABILITY_INSURANCE,CERTIFICATE_OF_WORKERS_COMPENSATION);nulluntil the certificate is processed.statusis the certificate's review state as stored:Pending(received, awaiting processing or review),Processed,Rejected; seeintake_statusfor the review progress.intake_status:processingwhile automatic reading has not finished,queuedonce read and waiting for a person to check it,doneonce processed or rejected. A file sent through a direct upload that never reached storage staysprocessing; such an upload is recognized by theCertificate Upload Failedevent (see Events) and by its download answering 404.canceledis true for an invalidated certificate: it stays on file but no longer counts toward compliance.effective_onandexpires_onare 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_dateis the date printed on the certificate. Like the other read values, all three arenulluntilstatusisProcessed.coveragesis 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>_presentbox is checked on the certificate or that has any recorded value;presentmirrors the box, so an entry withpresent: falsecan exist when values were recorded with the box unchecked; a type with neither has no entry. The prefixes includegl(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 (eoin the example). The set of coverage types can grow: discover it withGET /coverage_typesand 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_typesstates forcertificate_key:certificate_keynames where the compared certificate value lives incoverages.<prefix>:present,additional_insuredandsubrogation_waiverat the entry's top level; the limit amounts inlimits(every column whose name containslimit, except the General Liabilitygeneral_aggregate_limit_applies_*boxes); everything else infields(the type's other certificate fields, the GL aggregate-applies boxes andab_includedamong them). limitscarries every limit the certificate records for that coverage: each key is the stored field name with<prefix>_removed and then every leadingliability_,limits_orlimit_segment removed, repeatedly (soauto_liability_limits_combined_singleiscombined_singleandauto_liability_limits_custom_limitiscustom_limit,gl_limits_each_occuranceiseach_occurance,umb_limits_each_occurrenceiseach_occurrence,re_limit_coverageiscoverage,ab_occurrence_limitisoccurrence_limit, since a trailing_limitis kept); values are the stored strings, exactly as read, except the workers' compensation statutory boxesper_statuteandother, which are booleans. Acustom_namekey is the label of a custom limit, a stored string.fieldscarries 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_locationand_other, booleans, andgeneral_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_endorsementandpool_pop_up_endorsement(booleans); for Workers Compensation, for exampleprimary_non_contributoryandexclusions(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") andmonopolistic_exp_date(an ISO date); a state-fund proof therefore appears as awcentry even when itspresentbox is unchecked. Thewcentry'seffectiveandexpiration(and the certificate'seffective_onandexpires_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 infields(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.fieldsis omitted when the type has none.- Inside
limitsandfields, a value never recorded is left out (nevernull), while a box recorded unchecked isfalse. Members of the entity itself are always present,nullwhen 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_typeis the type chosen on the dashboard:Additional Insured Endorsement,Subrogation Waiver,Loss Payable Endorsement,Primary & Noncontributory Endorsement,EndorsementorOther. Treat it as text: a document moved from the certificate review may carry another value, or none.nameandpolicy_numbermay be null.file_urlis 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.