# Delete Title Entry
Source: https://docs.cardinalgray.com/api-reference/v1/delete_title
DELETE /title/{id}
This endpoint deletes a title entry and any related entries
Deletes the requested title entry and any duplicate pointer entries that reference it.
Successful responses use HTTP `200` and include a `statusCode: 200` field in the response body alongside `entryId`, `message`, and `deletedCount`.
# Get Title Entry
Source: https://docs.cardinalgray.com/api-reference/v1/get_title
GET /title/{id}
Retrieve a title entry by its ID
Returns the title entry with document paths rewritten to presigned URLs where applicable.
For preview organizations, complete entries may include [`account_facts`](/guides/account-facts) — crystallized yes/no judgements for title review derived from the reconciliation engine. This field is not generally available and should be treated as preview / not-GA surface.
## Physical title return (`title_collection`)
For organizations enrolled in physical title collection, entries whose signing state requires the original paper title to be returned (e.g. Texas) include the persisted `title_collection` projection — the same object returned by [`POST /title/{id}/sign`](/api-reference/v1/title_id_sign) when the collection was created. `collection_id` is stable; asset URLs (`label_url`, and `qr_url` when present) are presigned and expire at `assets_expires_at` — do not cache them indefinitely.
```json theme={null}
{
"entryId": "123e4567-e89b-12d3-a456-426614174000",
"state": "TX",
"title_collection": {
"collection_id": "11111111-2222-4333-8444-555555555555",
"mode": "LIVE",
"carrier": "UPS",
"service": "UPS Electronic Return Label",
"tracking_number": "1Z999AA10123456784",
"label_url": "https://assets.cardinalgray.com/labels/11111111-2222-4333-8444-555555555555.pdf?X-Amz-Signature=...",
"qr_url": "https://assets.cardinalgray.com/labels/11111111-2222-4333-8444-555555555555-qr.png?X-Amz-Signature=...",
"qr_verified": true,
"assets_expires_at": "2026-08-01T12:00:00Z",
"return_address": "c/o Cardinal Gray, Inc., 8080 Westpark Drive, STE 87808, Houston, TX 77063",
"instructions": [
"Place the original vehicle title in an envelope.",
"Print the UPS return label and attach it to the envelope.",
"Drop the package off at any UPS location and keep the receipt."
],
"created_at": "2026-07-20T12:00:00Z"
}
}
```
`title_collection` is additive and absent for organizations, states, or environments where physical title collection is not enabled. See [Sign Title Documents](/api-reference/v1/title_id_sign) for the full field contract.
# List Title Entries
Source: https://docs.cardinalgray.com/api-reference/v1/get_title_list
GET /title
Retrieve and filter title entries using query parameters
Use this endpoint to list title entries, look up a single entry by VIN, or return aggregate counts by title state.
## Response Shapes
Default list responses use:
Title entries in the current page.
Number of entries in the current page.
Pagination token for the next page, when more results are available.
Whether another page of results is available.
When `vin` is supplied, the API returns a single title entry or `404` if no matching entry exists. Single-entry reads rewrite document paths to presigned URLs.
When `stateMap=true` is supplied, the API returns a map of state codes to counts, such as:
```json theme={null}
{
"CA": 42,
"TX": 17
}
```
# Update Title Entry
Source: https://docs.cardinalgray.com/api-reference/v1/patch_title
PATCH /title/{id}
Update a title entry by providing specific fields to update
Partner requests may only update `account_data`. Requests containing other top-level fields return `403` with a `disallowedFields` array.
An empty JSON object (`{}`) is accepted as a no-op update and returns the current merged title entry.
# Create Title Entry
Source: https://docs.cardinalgray.com/api-reference/v1/post_title
POST /title
Create a new title entry with vehicle information or document uploads
Create a title entry from structured `account_data`, document URLs, or a raw array of document URLs. If an entry with the same VIN already exists for your organization, Porter updates the existing entry instead of creating a duplicate.
## Response Variants
This endpoint returns HTTP `200` for successful creates and updates.
Returned for new entries and for existing entries when enrichment is requested.
`pending` for new entries, or `enriching` when an existing VIN is updated and a sync flag was requested.
When an existing VIN is updated without sync flags, the API returns the full patched title entry instead of the minimal `{entryId, job_status}` response.
## Notes
* A raw JSON array is accepted as a legacy shorthand for `{ "attachments": [...] }`.
* If `sync_public` or `sync_private` is requested without an issuing state, the API automatically enables `sync_nmvtis`.
* `dppa_exemption` is optional on create. When provided, it is passed through to enrichment requests.
# Pull Title Data
Source: https://docs.cardinalgray.com/api-reference/v1/title_id_data
POST /title/{id}/data
Pull or resync data for an existing title entry
Requests a DMV, public, or NMVTIS enrichment job for an existing title entry. At least one sync flag must be true: `sync_nmvtis`, `sync_public`, or `sync_private`.
The API returns `400` both when no sync type is requested and when the title entry is not found. It returns `409` when the entry exists but is missing `account_data`, which is required for enrichment.
# Calculate Title Fees
Source: https://docs.cardinalgray.com/api-reference/v1/title_id_fees
POST /title/{id}/fees
Trigger fee calculation for a specific title entry
This endpoint initiates fee calculation for a title entry by publishing a fee calculation event and updating the job status to "enriching".
## Path Parameters
The unique identifier of the title entry
## Response
The ID of the title entry for which fees are being calculated
Updated job status, set to "enriching" after fee calculation is initiated
```bash theme={null}
curl -X POST "https://api.cardinalgray.com/title/123e4567-e89b-12d3-a456-426614174000/fees" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```json theme={null}
{
"entryId": "123e4567-e89b-12d3-a456-426614174000",
"job_status": "enriching"
}
```
# Generate Forms
Source: https://docs.cardinalgray.com/api-reference/v1/title_id_forms
POST /title/{id}/forms
Trigger form generation or regeneration for a specific title entry
This endpoint synchronously generates or regenerates forms for a title entry and returns `{ "entryId": "..." }` when complete.
Forms are automatically generated when creating a title entry, but this endpoint allows you to manually trigger regeneration if account data has been updated or forms need to be recreated.
## Path Parameters
The unique identifier (entryId) of the title entry
## Response
The ID of the title entry whose forms were generated.
## Forms Generated
Depending on the transaction type and state, this endpoint may generate:
* **Power of Attorney (POA)** forms
* **Title Application** forms
* **Lien Addition** forms
* **Duplicate Title** forms
* **Repossession Affidavit** forms
* **Odometer Disclosure** forms
* **VIN Verification** forms
* State-specific supplementary forms
```bash theme={null}
curl -X POST "https://api.cardinalgray.com/title/123e4567-e89b-12d3-a456-426614174000/forms" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```json theme={null}
{
"entryId": "123e4567-e89b-12d3-a456-426614174000"
}
```
# Sign Title Documents
Source: https://docs.cardinalgray.com/api-reference/v1/title_id_sign
POST /title/{id}/sign
Initiate signature process for title documents
Creates a signature request for generated title documents.
You can send an explicit body with `signers` and `documents_to_sign`, or omit the body entirely to let the API build the request from the title entry. Auto-build requires signer `contact_email` values to already be present on the entry, usually via a prior `PATCH /title/{id}`.
`redirect_url` belongs in the request body, not the query string.
## Signing URLs in the response
For Notarize/Proof-based organizations, the response includes per-signer signing URLs at `signers[].transaction_access_url`. Each signer receives their own unique link to access the signing session.
```json theme={null}
{
"id": "NTRZ::ot_abc123",
"status": "SENT",
"signers": [
{
"name": "Jane Doe",
"contact_email": "jane@example.com",
"transaction_access_url": "https://app.proof.com/activate-transaction?..."
}
]
}
```
The top-level `transaction_access_url` on the `SignatureRequest` object is **not** populated on the initial create response — it is only set after an explicit activate action on a `DRAFT` request. Always read signing URLs from `signers[].transaction_access_url`.
DocuSign-based requests do not include `transaction_access_url` — signing links are delivered directly to signers via email by DocuSign.
Internal signature actions for managing existing requests are not part of the partner-facing API surface.
## Physical title return (`title_collection`)
For organizations enrolled in physical title collection, titles in states that require the original paper title to be shipped back to Cardinal Gray (e.g. Texas) additionally include a `title_collection` object in the sign response, alongside the signature-request fields. The same object is persisted on the entry and returned by subsequent [`GET /title/{id}`](/api-reference/v1/get_title) reads.
```json theme={null}
{
"id": "NTRZ::ot_abc123",
"status": "SENT",
"signers": [
{
"name": "Jane Doe",
"contact_email": "jane@example.com",
"transaction_access_url": "https://app.proof.com/activate-transaction?..."
}
],
"title_collection": {
"collection_id": "11111111-2222-4333-8444-555555555555",
"mode": "LIVE",
"carrier": "UPS",
"service": "UPS Electronic Return Label",
"tracking_number": "1Z999AA10123456784",
"label_url": "https://assets.cardinalgray.com/labels/11111111-2222-4333-8444-555555555555.pdf?X-Amz-Signature=...",
"qr_url": "https://assets.cardinalgray.com/labels/11111111-2222-4333-8444-555555555555-qr.png?X-Amz-Signature=...",
"qr_verified": true,
"assets_expires_at": "2026-08-01T12:00:00Z",
"return_address": "c/o Cardinal Gray, Inc., 8080 Westpark Drive, STE 87808, Houston, TX 77063",
"instructions": [
"Place the original vehicle title in an envelope.",
"Print the UPS return label and attach it to the envelope.",
"Drop the package off at any UPS location and keep the receipt."
],
"created_at": "2026-07-20T12:00:00Z"
}
}
```
Contract rules:
* `title_collection` is **additive and may be absent** — for organizations, groups, states, or environments where physical title collection is not enabled, the response is unchanged. Existing clients must tolerate its absence.
* `collection_id` is **stable**: repeated sign actions, bumps, activations, and retries return the same collection rather than creating additional labels.
* `tracking_number` is nullable: it is `null` until a carrier shipment exists. Clients must handle `null`.
* `label_url` is the guaranteed printable-label fallback.
* `instructions` are ready to display to the borrower as-is; render them rather than hardcoding your own copy.
* `mode` distinguishes sandbox (`STUB`) from production (`LIVE`) fulfillment; treat it as informational — the field contract is identical in both.
* `qr_url` is **optional** and only present when `qr_verified` is `true` (a verified UPS mobile drop-off barcode). Clients must not assume `qr_url` exists or that an unverified QR is accepted at UPS.
* Asset URLs are presigned and **expire** at `assets_expires_at` — do not cache them indefinitely.
* The bodyless sign request uses the ruleset-generated document path — no `signers` or `documents_to_sign` are needed when contact emails are already on the entry.
Physical-title label generation is enabled per organization during rollout. In sandbox (`STUB`) responses, `label_url` renders a UPS sample label that is not valid postage, `tracking_number` is `null`, and `qr_url` is absent — the field contract is otherwise identical to production.
```bash Bodyless sign request theme={null}
curl -X POST "https://api.cardinalgray.com/title/123e4567-e89b-12d3-a456-426614174000/sign" \
-H "Authorization: Bearer YOUR_API_KEY"
```
# Submit Title Entry
Source: https://docs.cardinalgray.com/api-reference/v1/title_id_submit
POST /title/{id}/submit
Send a title entry to the state — mail packet or online portal, driven by the entry's state
Cardinal Gray submits title work to the state on your behalf. Depending on the entry's state, submissions run in one of two modes:
* **`MAIL`** — The API synchronously builds a mail packet (cover sheet, forms, and your uploaded documents merged into a single PDF), returns a presigned `packetUrl`, and tells you the `checkAmount` and `checkPayee` to enclose. You print, sign checks, and mail.
* **`ONLINE_PORTAL`** — The API accepts the submission (`202`) and a background worker drives the state portal. Poll [`GET /title/{id}/submit/{submissionId}`](/api-reference/v1/title_id_submit_submissionId) for status and the confirmation screenshot.
## Managed submission mode
Organizations enrolled in **managed submission** use this endpoint differently: the call is a funding-complete signal, not a packet builder. Send the request with **no body** — no `attachments`, no `scenarioKey`. Cardinal Gray's title operations team selects the filing scenario, assembles the documents, and files with the state on your behalf.
* The API responds `202` with `intakeMode: "MANAGED"` and a durable `submissionId`.
* The submission starts at status `AWAITING_READINESS`; poll [`GET /title/{id}/submit/{submissionId}`](/api-reference/v1/title_id_submit_submissionId) to track it. `method` appears once Cardinal Gray selects the filing path.
* Repeated calls for the same title are **idempotent** — you get the same `submissionId` back, and no duplicate filing work is started.
* The title's `account_status` does not change at intake; it advances only when filing actually occurs (`filing_en_route` fires when Cardinal Gray performs the filing, not at intake).
* Managed intake **rejects any request body**: a non-empty JSON body (including `attachments`, `scenarioKey`, or `options`) returns `400` with `"Managed submission intake does not accept a request body"`. An empty body or `{}` is accepted.
Managed mode is enabled per organization by Cardinal Gray — contact support to enroll. The sections below apply to self-service organizations only.
## Attachments
Every attachment must include an `s3_key` and a `document_type`. Cardinal Gray issues presigned upload URLs through the dashboard's document upload flow; the returned S3 keys are what you pass here. Supported `document_type` values:
* `VEHICLE_TITLE`, `VEHICLE_TITLE_BACK`
* `DRIVER_LICENSE`
* `SIGNED_POWER_OF_ATTORNEY`
* `LIEN_RELEASE_LETTER`
* `COVER_SHEET`
* `UNKNOWN`
## Scenario key
Each supported state exposes one or more submission scenarios (e.g. `CLEAR_TITLE`, `ACTIVE_LIEN`, `LOST_OR_ILLEGIBLE`, `REFINANCE_SECURED`, `LIEN_ADD`).
* **`ONLINE_PORTAL` states — `scenarioKey` is required.** An omitted or invalid value returns `400` with an `INVALID_SCENARIO_KEY` error listing the valid keys for the state.
* **`MAIL` states — `scenarioKey` is optional.** The API resolves the recommended scenario from the entry's current account data. Pass an explicit `scenarioKey` to override that choice.
Contact Cardinal Gray for the current scenario keys supported per state.
## Response codes
Packet generated and staged. Response includes `packetUrl`, `packetS3Key`, `checkAmount`, and `checkPayee`.
Submission queued. Poll `GET /title/{id}/submit/{submissionId}` for `status`, `applicationNumber`, and `confirmationImg`. Managed-mode responses include `intakeMode: "MANAGED"` and no `method`.
Self-service: missing `attachments`, an attachment missing `s3_key` or `document_type`, an unsupported state, or an invalid `scenarioKey`. Managed intake: any non-empty request body.
```bash Online portal (KS) theme={null}
curl -X POST "https://api.cardinalgray.com/title/123e4567-e89b-12d3-a456-426614174000/submit" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"scenarioKey": "CLEAR_TITLE",
"attachments": [
{
"s3_key": "private/acme/entry-abc/title-front.pdf",
"display_name": "Title front",
"document_type": "VEHICLE_TITLE"
},
{
"s3_key": "private/acme/entry-abc/title-back.pdf",
"display_name": "Title back",
"document_type": "VEHICLE_TITLE_BACK"
},
{
"s3_key": "private/acme/entry-abc/dl.pdf",
"display_name": "Driver license",
"document_type": "DRIVER_LICENSE"
}
]
}'
```
```bash Managed intake (no body) theme={null}
curl -X POST "https://api.cardinalgray.com/title/123e4567-e89b-12d3-a456-426614174000/submit" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```bash Mail packet (FL) theme={null}
curl -X POST "https://api.cardinalgray.com/title/123e4567-e89b-12d3-a456-426614174000/submit" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"scenarioKey": "LIEN_ADD",
"attachments": [
{
"s3_key": "private/acme/entry-abc/title-front.pdf",
"document_type": "VEHICLE_TITLE"
},
{
"s3_key": "private/acme/entry-abc/spoa.pdf",
"document_type": "SIGNED_POWER_OF_ATTORNEY"
}
],
"coverSheetHints": {
"county": "Miami-Dade"
}
}'
```
```json 202 Online portal theme={null}
{
"ok": true,
"submissionId": "b8f3c1a2-8a4e-4a8b-9b2f-6e5c1b1a0f11",
"entryId": "123e4567-e89b-12d3-a456-426614174000",
"state": "KS",
"method": "ONLINE_PORTAL"
}
```
```json 202 Managed intake theme={null}
{
"ok": true,
"submissionId": "b8f3c1a2-8a4e-4a8b-9b2f-6e5c1b1a0f11",
"entryId": "123e4567-e89b-12d3-a456-426614174000",
"state": "TX",
"intakeMode": "MANAGED"
}
```
```json 400 Managed intake with body theme={null}
{
"error": "Managed submission intake does not accept a request body"
}
```
```json 200 Mail packet theme={null}
{
"ok": true,
"submissionId": "b8f3c1a2-8a4e-4a8b-9b2f-6e5c1b1a0f11",
"entryId": "123e4567-e89b-12d3-a456-426614174000",
"state": "FL",
"method": "MAIL",
"packetUrl": "https://s3.amazonaws.com/...&X-Amz-Signature=...",
"packetS3Key": "private/acme/entry-abc/submit_packet.pdf",
"checkAmount": 79.75,
"checkPayee": "Florida Department of Highway Safety and Motor Vehicles"
}
```
# Get Submission
Source: https://docs.cardinalgray.com/api-reference/v1/title_id_submit_submissionId
GET /title/{id}/submit/{submissionId}
Fetch a submission's current status, application number, and confirmation screenshot
Use this endpoint to poll the state of a queued `ONLINE_PORTAL` submission and retrieve the portal `applicationNumber` and confirmation screenshot once available. Managed-mode organizations use it to track their intake request: the response includes `intakeMode: "MANAGED"`, and `method` is absent until Cardinal Gray selects the filing path.
## Status values
Poll until `status` reaches one of the terminal states.
| Status | Meaning |
| -------------------- | --------------------------------------------------------------------------------------- |
| `AWAITING_READINESS` | Managed intake recorded — Cardinal Gray's title operations team is preparing the filing |
| `PENDING` | Queued, worker has not started |
| `SUBMITTED` | Worker submitted to the state, awaiting acknowledgement |
| `UNDER_REVIEW` | State/portal has acknowledged and is reviewing |
| `ACTION_REQUIRED` | State needs follow-up documents — see `feedbackMessage` |
| `ACCEPTED` | State accepted the submission |
| `REJECTED` | State rejected the submission — see `error` and `feedbackMessage` |
| `CANCELED` | Cardinal Gray or the user canceled before completion |
| `ERROR` | Unrecoverable error running the worker — see `error` |
When available, `confirmationImg` is a presigned URL to a PNG screenshot captured from the state portal.
```bash theme={null}
curl -X GET "https://api.cardinalgray.com/title/123e4567-e89b-12d3-a456-426614174000/submit/b8f3c1a2-8a4e-4a8b-9b2f-6e5c1b1a0f11" \
-H "Authorization: Bearer YOUR_API_KEY"
```
```json 200 Under review theme={null}
{
"submissionId": "b8f3c1a2-8a4e-4a8b-9b2f-6e5c1b1a0f11",
"status": "UNDER_REVIEW",
"method": "ONLINE_PORTAL",
"applicationNumber": "KS-2026-000123",
"confirmationImg": "https://s3.amazonaws.com/...&X-Amz-Signature=..."
}
```
```json 200 Managed intake awaiting readiness theme={null}
{
"submissionId": "b8f3c1a2-8a4e-4a8b-9b2f-6e5c1b1a0f11",
"status": "AWAITING_READINESS",
"intakeMode": "MANAGED"
}
```
```json 200 Accepted theme={null}
{
"submissionId": "b8f3c1a2-8a4e-4a8b-9b2f-6e5c1b1a0f11",
"status": "ACCEPTED",
"method": "ONLINE_PORTAL",
"applicationNumber": "KS-2026-000123",
"confirmationImg": "https://s3.amazonaws.com/...&X-Amz-Signature=..."
}
```
# Changelog
Source: https://docs.cardinalgray.com/changelog
Product updates and announcements. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## January-June, 2026
### Added
* Clerk JWT and session-cookie authentication alongside API-key authentication
* Staging API base URL: `https://api.cardinalzyn.com`
* Public enrichment fields on title entries: `public_pull`, `public_report_pdf`, `public_found_status`, `public_txn_id`, and `public_last_fetch`
* Additional title entry workflow fields including `submission_id`, `forms_status`, form delivery outputs, processing timestamps, and SFTP export timestamps
* Vehicle and title metadata fields including `VehicleTitle.prev_action_date`, lien release indicators, `txn_quick_reference`, and expanded registration/title hold enums
* Preview `account_facts` field for eligible organizations
### Changed
* `POST /title` success responses are documented as HTTP `200`, matching create and VIN-dedup update behavior
* `GET /title` list responses now use `{ items, count, nextToken, hasMore }` and support `stateMap=true` aggregate responses
* `POST /title/{id}/forms` is documented as a synchronous `{ entryId }` response
* `POST /title/{id}/sign` documents `redirect_url` in the request body and clarifies the default signature-request build behavior
* Data pull statuses now include `pending` and `skipped`
### Deprecated
* `account_discrepancies` and `account_matches` remain available but are deprecated in favor of newer account fact review data
## December, 2025
### Added
* New UI/UX for document parsing and review flow
* VIN re-submission capability across batches
* Account discrepancies detection between attachment and data sources
### Changed
* Improved title metadata with additional enums: `status`, `prev_action`, `conjunction`, `ownership_type`
* Signature collection UI/UX improvements with template suggestions
### Fixed
* Auto-rotation of documents during preprocessing
* State-specific data parsing improvements for multiple jurisdictions (WA, IN, VA, FL, AZ, AL, WI, NC, OK, LA, and others)
* Race conditions between public and private data enrichment when requested in tandem
## November, 2025
### Added
* `account_discrepancies` property to title entry
* Properties to Person entity: `name_first`, `name_middle`, `name_last`, `name_suffix`
* Properties to VehicleTitle: `status` (enum), `status_raw_text`, `prev_action` (enum), `prev_action_raw_text`, and `conjunction` (enum)
* Private data inquiries for Iowa (IA), Massachusetts (MA)
### Changed
* Title entry properties `state_pull`, `portal_pull`, and `nmvtis_pull` returns signed url to `.json` file
* Enum for data `*_found_status` (`found`, `not_found`, `error`, `unavailable`)
### Fixed
* Oregon (OR) and Utah (UT) private data inquiries
* Missouri (MO) data parsing
## October, 2025
### Added
* `form_downloads` and `form_updates` property to title entry
* Feature permissions per user group (`feature_permissions`)
* User privileging and group assignment via SSO provider
* Name OATS forms included in E-SIGN signature collection by default
### Changed
* Webhook configuration consolidated into `webhook_profile`
* Signature collection UI/UX improvements
### Fixed
* Driver license back parsing fixes (phantom owners)
* Maryland (MD), Michigan (MI) form generation
## September, 2025
### Added
* `user_email` property to title entry
* Final signed document packet export
* PDF generation for state portal data: Idaho (ID), Iowa (IA), Hawaii (HI), Connecticut (CT), Alabama (AL), Florida (FL), Wisconsin (WI)
* E-signature with ID proof of identity
* Delaware (DE) `vehicle.type: LOW_SPEED` form support
### Changed
* Frontend cleanup and improvements
* Speed up document processing
* Vehicle classification fallback logic improvements
### Fixed
* Cover sheet generation for sale transactions (`form_instructions`)
* Kentucky (KY) & Maine (ME) form generation improvements
* `form_payoff_authorization` generation
## August, 2025
### Added
* Okta SSO integration
* Self-service webhook registration
* Private data inquiries for Washington (WA)
### Fixed
* Webhook delivery reliability
* Fee calculators for states: Alabama (AL), Arizona (AZ), California (CA), Delaware (DE), Georgia (GA), Hawaii (HI), Kansas (KS), Kentucky (KY), Louisiana (LA), Oklahoma (OK), Oregon (OR), South Carolina (SC), Virginia (VA), Vermont (VT), Wyoming (WY)
## July, 2025
### Added
* Automated lien release and title mailing follow-up
* Slack integration
* Public data inquiries for states: Colorado (CO), Minnesota (MN), Oklahoma (OK)
### Added
* Query parameters `vin` & `groupId` for `GET /title`
* `PAYOFF_LETTER` document classification and parsing
* State fee calculators for: Arkansas (AR), Missouri (MO), North Carolina (NC), Nevada (NV), Utah (UT), Washington (WA)
* Granular form requirements per state for signature collection
### Changed
* Signature collection UI/UX improvements
## May, 2025
### Added
* `POST /title/{id}/fees` endpoint for fee calculation
* State fee calculators for: Arizona (AZ), Colorado (CO), Florida (FL), Iowa (IA), Ohio (OH), Tennessee (TN), Texas (TX)
* Public data inquiries for states: Arkansas (AR), Hawaii (HI), Missouri (MO), Utah (UT), Vermont (VT)
* Private data inquiries for states: Arkansas (AR), Connecticut (CT), Oregon (OR), Missouri (MO), Utah (UT), Vermont (VT)
### Changed
* Improved document processing accuracy
## April, 2025
### Added
* Support for multi-tenant organizations
* Support for servicer ELT account access
* Public data inquiries for New York (NY)
* Complete full state coverage for sales of `vehicle_type: "OHV" | "ORV" | "ATV"`
### Deprecated
* Generative AI record parsing
## March, 2025
### Added
* `PATCH /title` endpoint for updating title entries
* `DELETE /title` endpoint for removing title entries
* Support for Oklahoma Tribal Nation and P2P sales
* Document classifications for `VEHICLE_REGISTRATION` and `DRIVER_LICENSE`
* Public data inquiries for states: North Dakota (ND), Nebraska (NE), South Dakota (SD)
## February, 2025
### Added
* `forms_name_oats` property to title entry
* Dynamic field detection for signature collection
* Public data inquiries for states: Alabama (AL), Arizona (AZ), Florida (FL), Georgia (GA), Iowa (IA), Idaho (ID), Illinois (IL), Massachusetts (MA), Michigan (MI), Mississippi (MS), Ohio (OH), Wisconsin (WI)
* Private data inquiries for states: Indiana (IN), Louisiana (LA), Oklahoma (OK), Wisconsin (WI)
### Changed
* Reduced turnaround time on public data inquiries by 80%
## January, 2025
### Added
* `POST /title` and related actions
### Deprecated
* v0 Cardinal Gray endpoints
# Account Facts
Source: https://docs.cardinalgray.com/guides/account-facts
Object reference for the account_facts field — crystallized title review judgements returned on complete title entries
## Overview
`account_facts` is a top-level field on title entries (`DynamoDBEntry`) that packages four yes/no judgements an integration partner needs at a glance. Each fact includes a plain-language `summary` and `references` back into the underlying data.
Facts are derived from the **vehicle reconciliation engine** — the same projection that powers title review — not from raw `account_data` alone. They are computed **live on read** when you call `GET /title/{id}` (or the reconcile endpoint for opted-in orgs) and are only present when `job_status` is `complete`.
**Preview / not GA.** `account_facts` is currently returned only for eligible preview organizations. Other organizations receive no behavior change. Treat this surface as subject to change until generally available.
`account_discrepancies` and `account_matches` are deprecated in favor of `account_facts`. Prefer reading `account_facts` for title review automation.
***
## When it appears
| Condition | Result |
| --------------------------------------- | ------------------------------------------ |
| Organization is opted in to the preview | `account_facts` may be included |
| `job_status` is not `complete` | Field is omitted |
| Reconciliation or fact derivation fails | Field is omitted (response still succeeds) |
Borrower-related facts compare against the **originally submitted** obligor borrowers from the `POST /title` request (stored in the "Initial API Call Data" attachment), not the live `account_data.loan.borrower[]` array, which enrichment may overwrite with materialized title owners.
***
## Object shape
```typescript theme={null}
interface AccountFacts {
computed_at: string; // ISO 8601 — when facts were derived
source: "reconcile"; // Provenance of the judgements
borrower_on_title: AccountFact;
borrower_signature_only_needed_for_title: AccountFact;
has_problematic_brands: AccountFact;
are_there_active_liens: AccountFact;
}
interface AccountFact {
value: boolean;
summary: string;
references: AccountFactReference[];
}
interface AccountFactReference {
field_path: string; // JSON pointer, e.g. "/vehicle/title/owners"
value: unknown;
from?: {
kind: "submitted_borrower" | "reconciled_title";
ref_id?: string;
};
}
```
***
## The four facts
### `borrower_on_title`
**Question:** Do all submitted obligor borrowers appear on the title?
* **`true`** — Every submitted borrower fuzzy-matches at least one reconciled title owner.
* **`false`** — At least one submitted borrower does not match any owner, **or** there were no submitted borrowers / no reconciled owners to compare.
Name matching tolerates case, punctuation, `Last, First` order, middle initials, and common nickname prefixes. Non-obligor borrowers (`is_nonobligor: true`) are excluded from the submitted set.
When `false` because of a mismatch, `references` cite the **unmatched** borrower(s) plus the owner list. When `true`, references cite the matched borrower(s) plus owners.
### `borrower_signature_only_needed_for_title`
**Question:** Can the borrower sign alone (without other owners)?
* **`true`** when either:
* The title is an **OR** title (`vehicle.title.is_or === true`), or
* Every reconciled title owner is a submitted borrower (wholly borrower-owned).
* **`false`** when the title is AND/joint and not wholly borrower-owned, **or** when no reconciled owners are available to evaluate.
OR titles always yield `true` regardless of borrower ownership.
### `has_problematic_brands`
**Question:** Does the title carry a title-impairing brand?
* **`true`** — At least one problematic brand is present on `vehicle.title.brands` (or legacy `vehicle.title.brand`).
* **`false`** — No problematic brands found, or no brand data available.
**Problematic brands** include salvage-family, flood/fire, lemon/buyback, junk, and export-only brands such as `SALVAGE`, `REBUILT`, `FLOOD`, `JUNK`, `LEMON`, `EXPORT_ONLY`, and related variants.
**Excluded** (not title-blocking on their own): odometer brands (`ACTUAL_MILEAGE`, `EXEMPT_ODOMETER`, etc.) and administrative brands (`MEMORANDUM_COPY`, etc.).
### `are_there_active_liens`
**Question:** Are there unreleased liens on the title?
* **`true`** — At least one lien in `active_liens` lacks `lien_release_signed: true`, **or** `has_liens` is true with no lien detail.
* **`false`** — All liens are released, no liens exist, or no lien data is available.
When active liens exist, `references` point to each unreleased lienholder name.
***
## Interpreting `value: false`
For all four facts, **`false` is overloaded**. It can mean either:
1. A genuine **negative** answer (e.g. borrower is not on title), or
2. **Insufficient data** to decide (e.g. enrichment still running, no submitted borrowers, no reconciled owners).
There is no separate tri-state field today. Read the `summary` string to disambiguate — it explicitly states when inputs were missing (e.g. "No submitted borrower names were available…").
A future explicit `determinable` or tri-state field is tracked as a partner-facing contract change. Until then, parse `summary` or wait until `job_status` is `complete` and enrichment has populated owners/brands/liens.
***
## Example response
```json theme={null}
{
"entryId": "550e8400-e29b-41d4-a716-446655440000",
"job_status": "complete",
"account_data": { "...": "..." },
"account_facts": {
"computed_at": "2026-06-18T18:42:11.123Z",
"source": "reconcile",
"borrower_on_title": {
"value": false,
"summary": "Borrower(s) THOMAS AAA do not match any title owner (SUSAN RODRIGUEZ, KAREN ROSE ANDERSON).",
"references": [
{
"field_path": "/loan/borrower/0/name",
"value": "THOMAS AAA",
"from": { "kind": "submitted_borrower", "ref_id": "loan.borrower[0]" }
},
{
"field_path": "/vehicle/title/owners",
"value": ["SUSAN RODRIGUEZ", "KAREN ROSE ANDERSON"],
"from": { "kind": "reconciled_title" }
}
]
},
"borrower_signature_only_needed_for_title": {
"value": false,
"summary": "Title is an AND/joint title not wholly owned by the borrower; additional owner signatures are likely required.",
"references": [
{
"field_path": "/vehicle/title/is_or",
"value": false,
"from": { "kind": "reconciled_title" }
},
{
"field_path": "/vehicle/title/owners",
"value": ["SUSAN RODRIGUEZ", "KAREN ROSE ANDERSON"],
"from": { "kind": "reconciled_title" }
}
]
},
"has_problematic_brands": {
"value": false,
"summary": "No problematic title brands (e.g. salvage, flood, junk) were found.",
"references": [
{
"field_path": "/vehicle/title/brands",
"value": null,
"from": { "kind": "reconciled_title" }
}
]
},
"are_there_active_liens": {
"value": true,
"summary": "Title has 1 active (unreleased) lien(s): First National Bank.",
"references": [
{
"field_path": "/vehicle/title/active_liens/0/lienholder/name",
"value": "First National Bank",
"from": { "kind": "reconciled_title", "ref_id": "lien_date:01/20/2023" }
}
]
}
}
}
```
***
## Related endpoints
`GET /title/{id}` — primary surface where `account_facts` is returned for preview orgs
Background on `AccountData`, `Vehicle`, and reconciliation inputs
# Core Ontology
Source: https://docs.cardinalgray.com/guides/core-ontology
Understanding Cardinal Gray
# The Cardinal Gray Data Model
Cardinal Gray's API is built around an opinionated, transaction-agnostic data model that separates **what we're dealing with** (the vehicle) from **where we're taking it** (the transaction).
* **Vehicle** (descriptive): The source of truth for VIN, make, model, title information, and registration data
* **Transaction** (prescriptive): How you're working with this vehicle: loan, sale, repo, or salvage
```typescript theme={null}
interface AccountData {
vehicle: Vehicle; // Required: Always present
loan?: DirectLoan; // Optional loan origination data
repo?: Repo; // Optional when collateral enters repo status
sale?: Sale; // Optional dealer or private ownership transfer
salvage?: Salvage; // Optional for salvage/total loss processing
}
```
***
## The Vehicle Object
### Overview
The `Vehicle` object is the **descriptive core** of every title entry. It contains:
#### 1. Identity & Classification
```typescript theme={null}
{
vin: "1HGCM82633A123456", // duh
year: "2003", // Model year
make: "Honda", // Manufacturer
model: "Accord", // Model name
// enum and state literals of vehicle type, class
// body, style... and more!
}
```
#### 2. Ownership Information
```typescript theme={null}
vehicle: {
// vin, make, year, model, motorcycle engine no., etc.
title: {
number: "T1234567", // Title number
issuing_state: "CA", // State that issued the title
issuing_county: "Los Angeles", // County of issuance (if relevant)
issuance_date: "01/15/2023", // When title was issued
status: "active", // Enumeration of title status
status_raw_text: "ACTIVE - NO HOLDS", // Original DMV portal text
// Odometer information
odometer_reading: "45123",
odometer_code_translation: "ACTUAL", // ACTUAL | NOT_ACTUAL | EXEMPT
// Ownership details
owner_names_one_line: "JOHN AND JANE DOE",
owners: [{ name: "John Doe", ownership_type: "titled_owner" ... }],
conjunction: "AND", // In the case of multiple owners
// Lien information
has_liens: true,
active_liens: [{
lien_date: "01/20/2023",
lienholder: { name: "First Bank", elt_id: "FB123", ... }
}],
// Brand history
brands: [{
brand: "NONE", // Enumerated brand "translation"
nmvtis_code: "0",
state_code: "00"
}]
}
}
```
#### 3. Registration Information
Registered vs titled owners are distinguished within the `title.owners` via the `ownership_type` enum, which is ordered based
on ownership priority!
```typescript theme={null}
{
license_plate: "ABC1234",
license_plate_type: "PASSENGER",
registration_status: "ACTIVE", // ACTIVE | EXPIRED | SUSPENDED/REVOKED
registration_expiration_date: "12/31/2024",
registration_issuance_date: "12/31/2023",
// Registration/title holds
has_reg_title_holds: false,
reg_title_holds: [{
hold_type: "INSURANCE", // INSURANCE | LIEN | FEE | SUSPENSION | etc.
hold_description: "Proof of insurance required",
issue_date: "06/15/2023",
source: "DMV"
}]
}
```
**Data Enrichment:** When you POST a title with `sync_nmvtis`, `sync_public`, or `sync_private` flags, Cardinal Gray automatically populates these fields with real-time DMV data. You only need to provide the VIN and we handle the rest.
### Deep Dive
Field-by-field walkthrough of the `Vehicle` object.
#### Vehicle Fields
**Holds Block Transactions:** If `has_reg_title_holds` is `true`, you may not be able to complete certain DMV transactions until the holds are cleared. The `reg_title_holds` array provides details on what needs to be resolved.
The following fields are available on the `Vehicle` object from DMV data enrichment:
| Field | Description |
| ------------------------------ | --------------------------------- |
| `vin` | Vehicle Identification Number |
| `year` | Vehicle model year |
| `make` | Vehicle manufacturer |
| `model` | Vehicle model name |
| `body` | Body style |
| `type` | Vehicle type classification |
| `color` | Primary color |
| `color_secondary` | Secondary color |
| `fuel_type` | Fuel type |
| `use` | Vehicle use classification |
| `weight` | Vehicle weight |
| `weight_rating` | Weight rating classification |
| `weight_gross` | Gross vehicle weight |
| `weight_unladen` | Unladen weight |
| `width` | Vehicle width |
| `length` | Vehicle length |
| `bhpcc` | BHP/CC rating |
| `number_of_axles` | Number of axles |
| `number_of_cylinders` | Number of cylinders |
| `license_plate` | License plate number |
| `license_plate_type` | Plate type |
| `motorcycle_engine_number` | Motorcycle engine number |
| `registration_status` | Current registration status |
| `registration_expiration_date` | Registration expiration date |
| `registration_issuance_date` | Registration issuance date |
| `class` | Vehicle class |
| `has_reg_title_holds` | Whether holds exist |
| `reg_title_holds` | Array of registration/title holds |
| `prev_date_of_sale` | Previous sale date |
| `prev_sale_price` | Previous sale price |
| `fee_history` | Historical fee data |
| `insurance` | Insurance information |
#### VehicleTitle Fields
The following fields are available on the nested `VehicleTitle` object:
| Field | Description |
| --------------------------- | ----------------------------------------------------- |
| `number` | Title number |
| `issuing_state` | State that issued the title |
| `issuing_county` | County where title was issued |
| `issuance_date` | Title issuance date |
| `brand` | Title brand (deprecated) |
| `brand_code` | Brand code (deprecated) |
| `brands` | Array of brand details with NMVTIS codes |
| `odometer_reading` | Odometer reading at title issuance |
| `odometer_reading_unit` | MI or KM |
| `odometer_code` | Odometer status code |
| `odometer_code_translation` | Human-readable odometer status |
| `has_liens` | Whether liens exist |
| `active_liens` | Array of active liens |
| `is_electronic` | Electronic title flag |
| `is_and` | AND ownership conjunction |
| `is_or` | OR ownership conjunction |
| `is_tod` | Transfer on Death flag |
| `is_jtwros` | Joint Tenants With Right of Survivorship |
| `conjunction` | Ownership conjunction type |
| `is_leased` | Leased vehicle flag |
| `previous_title` | Previous title information |
| `owner_names_one_line` | Concatenated owner names |
| `number_of_owners` | Number of owners |
| `owners` | Array of owner Person objects |
| `status` | Title status (active, inactive, pending, etc.) |
| `status_raw_text` | Raw status text from source |
| `prev_action` | Previous action (original, transfer, duplicate, etc.) |
| `prev_action_raw_text` | Raw previous action text |
***
## Transaction Types
Each transaction type is purpose-built for specific workflows, modeling the information you collect within a loan origination system or deal flow:
Reach out to support for information on auto-populating transaction keys for your specific workflow needs
For title work involving loans or refinances.
```typescript theme={null}
{
loan: {
new_lienholder: { name, elt_id, addr },
borrower: [{ name, dob, addr }],
loan_amount: 25000,
// For refinances:
prior_lien: { lienholder, lien_date, lien_amount }
}
}
```
**Use cases:** Frontend lien perfection, refinances, lien additions.
For dealer sales, private party sales, or gifts.
```typescript theme={null}
{
sale: {
type: "NEW" | "USED" | "GIFT",
sale_price: 18500,
buyer: { name, dob, addr, insurance },
seller: { name, addr, dealer_id? },
financed?: { lienholder, lien_date }
}
}
```
**Use cases:** Dealer sales, private party transfers, gift transfers.
For vehicle repossession workflows.
```typescript theme={null}
{
repo: {
reposession_date: "03/15/2024",
reposession_address: { street1, city, state, zip },
reposession_agency: "Recovery Services Inc",
reposession_agency_address: { ... }
}
}
```
**Use cases:** Repo title acquisition, affidavit generation, post-repo sales.
For insurance total loss or salvage title work.
```typescript theme={null}
{
salvage: {
type: "THEFT" | "DAMAGE",
date_of_loss: "04/10/2024",
total_loss_value: 15000,
insurance_company: "Progressive"
}
}
```
**Use cases:** Salvage title applications, total loss settlements, rebuilt titles.
***
## Title entry projections
Beyond `account_data`, complete title entries may expose derived fields for review automation:
The `account_facts` object — four crystallized judgements (`borrower_on_title`, signature sufficiency, problematic brands, active liens) with summaries and references. Preview / not GA.
***
## Next Steps
Learn how NMVTIS, state DMV, and portal data flows work
Jump into the endpoint documentation
# Data Enrichment
Source: https://docs.cardinalgray.com/guides/data-enrichment
Explore which Vehicle and VehicleTitle fields are populated from public and private DMV data
## About `dppa_exemption`
The `dppa_exemption` parameter identifies which permissible use exemption under the Federal Driver's Privacy Protection Act (DPPA) applies to your request for motor vehicle records. Access to personal information in motor vehicle records requires compliance with DPPA regulations.
### What is the Driver's Privacy Protection Act (DPPA)?
The Driver's Privacy Protection Act is a United States federal law enacted in 1994 that safeguards personal information contained in motor vehicle records maintained by state Departments of Motor Vehicles (DMVs). The DPPA restricts the disclosure of this information and establishes legal consequences for unauthorized access or use.
This information is provided for informational purposes only and should not be considered legal advice. Consult with an attorney or relevant authority for guidance specific to your situation.
### Permissible Uses Under DPPA
The DPPA allows access to personal information for specific purposes, including:
1. **Government agency functions** - Use by any government agency, including courts or law enforcement, in carrying out official functions
2. **Motor vehicle safety and theft** - Matters related to vehicle safety, theft prevention, emissions, product recalls, and market research
3. **Legitimate business operations** - Verification of personal information submitted to businesses, but only to prevent fraud, pursue legal remedies, or recover debts
4. **Legal proceedings** - Use in connection with civil, criminal, administrative, or arbitral proceedings
5. **Research and statistics** - Research activities and statistical reports, provided the information is not published, redisclosed, or used to contact individuals
6. **Insurance activities** - Use by insurers or insurance support organizations for claims investigation, antifraud activities, rating, or underwriting
7. **Towed/impounded vehicles** - Providing notice to owners of towed or impounded vehicles
8. **Private investigation** - Use by licensed private investigative agencies or security services for permitted purposes
9. **Commercial driver licensing** - Employment verification related to commercial driver's license holders
10. **Toll facilities** - Operations of private toll transportation facilities
11. **Individual consent** - Any use when the State has obtained express consent from the individual
12. **Bulk distribution** - Surveys, marketing, or solicitations when express consent has been obtained
13. **Written consent** - Any use when the requester demonstrates written consent from the individual
14. **State-authorized uses** - Other uses specifically authorized by state law related to motor vehicle operation or public safety
### Access Requirements
Before obtaining access to personal information in motor vehicle records, users must complete:
* An Account Agreement
* A User Registration Form identifying the DPPA exemption under which they qualify
Motor vehicle and driver license records are accessed on a per-transaction basis (one inquiry per record), with a fee charged for each transaction.
### Source
The DPPA is codified at [18 U.S.C. § 2721 et seq.](https://www.law.cornell.edu/uscode/text/18/2721)
## Coverage Map
Cardinal Gray enriches `AccountData::Vehicle` objects with real-time data from state DMV systems. This guide outlines field availability across all 50 states for both `sync_public` and `sync_private` data sources.
Field availability varies based on state DMV system capabilities and data freshness.
**Hover over and click any state to view the Vehicle data returned from that state's DMV.**
***
## Integration pattern: POST + ping
Two common ways to run enrichment:
* **Create + enrich in one shot**: `POST /title` (optionally requesting NMVTIS and/or state enrichment immediately)
* **Enrich an existing entry**: `POST /title/{id}/data`
### Option A: Directly via `/title`
You can query nmvtis, public, or private data directly via the `POST /title` endpoint.
This is beneficial when you need to need to initialize an account using just a VIN or cross-validate collected information (e.g., structured originations/deal data or unstructured title/reg documents) against DMV data.
```ts init-via-title.ts theme={null} theme={null}
type InitTitleResponse = {
entryId: string;
job_status: "enriching" | "complete" | "error";
nmvtis_pull?: unknown;
state_pull?: unknown;
account_data?: { vehicle?: { title?: { issuing_state?: string; odometer_reading?: string } } };
};
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
export async function postTitleAndWait({
apiBaseUrl,
secretKey,
body,
}: {
apiBaseUrl: string;
secretKey: string;
body: unknown;
}): Promise {
let res = await fetch(`${apiBaseUrl}/title`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${secretKey}`,
},
body: JSON.stringify(body),
});
if (!res.ok) {
throw new Error(`POST /title failed: ${res.status} ${await res.text()}`);
}
let json: InitTitleResponse = await res.json();
while (json.job_status !== "complete" && json.job_status !== "error") {
await sleep(1500);
res = await fetch(`${apiBaseUrl}/title/${json.entryId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${secretKey}`,
},
});
json = await res.json();
}
return json;
}
// Example usage:
await postTitleAndWait({
apiBaseUrl: process.env.API_BASE_URL!,
secretKey: process.env.CG_SECRET_KEY!,
body: {
account_data: {
vehicle: {
vin: "19UUA56602A000960",
title: {
odometer_reading: "100000",
issuing_state: "FL",
},
},
},
sync_nmvtis: true,
sync_private: true,
},
});
```
```bash init-via-title.sh theme={null} theme={null}
# POST /title (request enrichment) then poll GET /title/{id} until complete
API_BASE_URL="https://YOUR_API_BASE_URL"
SECRET_KEY="YOUR_SECRET_KEY"
INIT_RES="$(curl -sS -X POST "${API_BASE_URL}/title" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${SECRET_KEY}" \
-d '{
"account_data": {
"vehicle": {
"vin": "19UUA56602A000960",
"title": { "odometer_reading": "100000", "issuing_state": "FL" }
}
},
"sync_nmvtis": true,
"sync_private": true
}')"
ENTRY_ID="$(node -p 'JSON.parse(process.argv[1]).entryId' "${INIT_RES}")"
while true; do
RES="$(curl -sS -X GET "${API_BASE_URL}/title/${ENTRY_ID}" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${SECRET_KEY}")"
STATUS="$(node -p 'JSON.parse(process.argv[1]).job_status' "${RES}")"
echo "job_status=${STATUS}"
if [ "${STATUS}" = "complete" ] || [ "${STATUS}" = "error" ]; then
echo "${RES}"
break
fi
sleep 1.5
done
```
AccountData parameter `vehicle.title.issuing_state` tells us where to make a motor vehicle inquiry; if uncertain which state the vehicle is titled in, just set `sync_nmvtis: true`
### Option B: Via `/title/{id}/data`
For first-time or follow-up data inquiries on existing title entries, the `POST /title/{id}/data` endpoint is available.
```ts post-data-via-existing-entry.ts theme={null} theme={null}
type GetTitleResponse = {
entryId: string;
job_status: "enriching" | "complete" | "error";
nmvtis_last_fetch?: string;
state_last_fetch?: string;
};
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
export async function postDataRequest({
apiBaseUrl,
entryId,
secretKey,
sync_nmvtis,
sync_public,
sync_private,
dppa_exemption,
}: {
apiBaseUrl: string;
entryId: string;
secretKey: string;
sync_nmvtis: boolean;
sync_public: boolean;
sync_private: boolean;
dppa_exemption: string;
}) {
const res = await fetch(`${apiBaseUrl}/title/${entryId}/data`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${secretKey}`,
},
body: JSON.stringify({ sync_nmvtis, sync_public, sync_private, dppa_exemption }),
});
if (!res.ok) {
throw new Error(`POST /title/${entryId}/data failed: ${res.status} ${await res.text()}`);
}
return res.json() as Promise;
}
export async function waitForTitle({
apiBaseUrl,
entryId,
secretKey,
}: {
apiBaseUrl: string;
entryId: string;
secretKey: string;
}) {
while (true) {
const res = await fetch(`${apiBaseUrl}/title/${entryId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${secretKey}`,
},
});
const json = (await res.json()) as GetTitleResponse;
if (json.job_status === "complete" || json.job_status === "error") return json;
await sleep(1500);
}
}
// Example usage:
await postDataRequest({
apiBaseUrl: process.env.API_BASE_URL!,
entryId: "YOUR_ENTRY_ID",
secretKey: process.env.CG_SECRET_KEY!,
sync_nmvtis: true,
sync_public: true,
sync_private: true,
dppa_exemption: "legitimate_business_need",
});
await waitForTitle({
apiBaseUrl: process.env.API_BASE_URL!,
entryId: "YOUR_ENTRY_ID",
secretKey: process.env.CG_SECRET_KEY!,
});
```
```bash post-data-via-existing-entry.sh theme={null} theme={null}
# POST /title/{id}/data then poll GET /title/{id} until complete
API_BASE_URL="https://YOUR_API_BASE_URL"
SECRET_KEY="YOUR_SECRET_KEY"
ENTRY_ID="YOUR_ENTRY_ID"
curl -sS -X POST "${API_BASE_URL}/title/${ENTRY_ID}/data" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${SECRET_KEY}" \
-d '{
"sync_nmvtis": true,
"sync_public": true,
"sync_private": true,
"dppa_exemption": "legitimate_business_need"
}' >/dev/null
while true; do
RES="$(curl -sS -X GET "${API_BASE_URL}/title/${ENTRY_ID}" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${SECRET_KEY}")"
STATUS="$(node -p 'JSON.parse(process.argv[1]).job_status' "${RES}")"
echo "job_status=${STATUS}"
if [ "${STATUS}" = "complete" ] || [ "${STATUS}" = "error" ]; then
echo "${RES}"
break
fi
sleep 1.5
done
```
# Webhooks
Source: https://docs.cardinalgray.com/guides/webhooks
Receive real-time notifications when title entry state changes occur
## Overview
Cardinal Gray dispatches webhook notifications to your endpoint whenever a title entry undergoes a state change that matches your subscribed events. Webhooks are sent as `POST` requests with the full title entry as the JSON body.
To configure a webhook endpoint, contact the Cardinal Gray team. You'll need to provide your endpoint URL and choose an authentication scheme.
## Authentication
Cardinal Gray supports two outbound authentication schemes for webhook delivery:
### Basic Auth
```
Authorization: Basic base64(username:password)
```
The `Authorization` header contains a Base64-encoded `username:password` pair. This is the recommended scheme for new integrations.
### Bearer Token (Legacy)
```
Authorization: Bearer YOUR_TOKEN
x-partner-key: YOUR_PARTNER_KEY
```
Legacy integrations may receive a bearer token in the `Authorization` header and/or an `x-partner-key` header. New integrations should use Basic Auth.
***
## Subscribable Events
| Event | Fires When |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `title.job_status.complete` | Entry processing finishes successfully (`job_status` transitions to `complete`) |
| `title.job_status.error` | Entry processing fails terminally (`job_status` transitions to `error`) |
| `title.data.found` | New enrichment data is discovered (NMVTIS, public portal, or private state pull transitions from `not_found` → `found`) |
| `title.data.not_found` | Reserved for future use |
| `title.account_status.*` | Any `account_status` field change (wildcard — fires for all transitions) |
### Account Status Lifecycle
The `title.account_status.*` event fires with the specific new status appended. Possible values:
```
title.account_status.originated
title.account_status.poa_sent
title.account_status.poa_signed
title.account_status.filing_en_route
title.account_status.filing_received
title.account_status.lien_delay
title.account_status.lien_error
title.account_status.lien_filed
title.account_status.lien_perfected
title.account_status.account_paid
title.account_status.lien_release_pending
title.account_status.lien_release_complete
```
### Physical Title Milestones
Physical-title milestone automation is enabled per organization during rollout.
For titles whose original paper document is shipped to Cardinal Gray (e.g. Texas), a coarse `physical_title_status` field on the title entry tracks the return, with matching webhook events:
| Event | Fires When |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `title.physical_title_status.in_transit` | The carrier has physically accepted and scanned the package containing the original title (label creation alone does not fire this) |
| `title.physical_title_status.received` | Cardinal Gray's receiving facility has logged and matched the physical title (carrier "Delivered" alone does not fire this) |
| `title.physical_title_status.unavailable` | The original title is lost or unavailable — Cardinal Gray will pursue a duplicate-title path instead of waiting for it. Recoverable: the status may later advance to in-transit or received if the title appears |
| `title.physical_title_status.skipped` | No physical original is required for this transaction (e.g. an electronic title, or a filing scenario that does not require collecting the original) |
These milestones are independent of `account_status`; the title lifecycle statuses above are unaffected.
***
## Payload Shape
The webhook payload is a `POST` request with `Content-Type: application/json`. The body is the **full title entry** (identical to `GET /title/{id}` response) plus a `webhook_events` array indicating which subscribed events triggered this dispatch.
```json Example Webhook Payload theme={null}
{
"entryId": "ffe3f543-cd92-4cb7-a78f-68a58b1fe8d3",
"website": "yourorg.com",
"VIN": "19UUA56602A000960",
"state": "FL",
"Borrower": "Jane Doe",
"job_status": "complete",
"account_status": "poa_signed",
"account_data": {
"vehicle": {
"vin": "19UUA56602A000960",
"year": "2002",
"make": "ACURA",
"model": "TL",
"body_style": "SEDAN",
"title": {
"title_number": "123456789",
"issuing_state": "FL",
"issue_date": "2023-01-15",
"odometer_reading": "98500",
"odometer_status": "ACTUAL"
},
"owners": [
{
"name": { "first": "Jane", "middle": "M", "last": "Doe" },
"address": {
"street": "123 Main St",
"city": "Miami",
"state": "FL",
"zip": "33101"
}
}
],
"liens": [
{
"position": 1,
"lienholder_name": "First National Bank",
"date_filed": "2023-01-15"
}
]
},
"loan": {
"loan_amount": "25000",
"borrower": [
{
"name": { "first": "Jane", "middle": "M", "last": "Doe" },
"address": {
"street": "123 Main St",
"city": "Miami",
"state": "FL",
"zip": "33101"
}
}
],
"lender": {
"name": "Your Lending Co",
"address": {
"street": "456 Finance Ave",
"city": "New York",
"state": "NY",
"zip": "10001"
}
}
},
"txn_quick_reference": "refi"
},
"nmvtis_found_status": "found",
"nmvtis_pull": "https://s3-presigned-url...",
"state_found_status": "found",
"state_pull": "https://s3-presigned-url...",
"signature_requests": [
{
"id": "NTRZ-abc123",
"type": "RON",
"status": "COMPLETED",
"created_at": "2026-06-20T10:00:00Z",
"updated_at": "2026-06-23T22:00:00Z",
"signers": [
{ "name": { "first": "Jane", "last": "Doe" } }
],
"documents_to_sign": [
{ "filename": "poa.pdf", "id": "doc-1", "signer_id": ["signer-1"] }
]
}
],
"fee_estimate": {
"tax": { "taxable_amount": 25000, "total": 1750, "items": [] },
"registration": { "total": 225, "items": [] },
"title": { "total": 75, "items": [] },
"submission_instructions": {
"payable_to": "FL DMV",
"address": { "street": "...", "city": "Tallahassee", "state": "FL", "zip": "32301" }
}
},
"webhook_events": ["title.account_status.poa_signed"]
}
```
### Key Fields
| Field | Type | Description |
| --------------------- | ---------- | --------------------------------------------------------------------------------------------------- |
| `entryId` | `string` | Unique identifier for the title entry |
| `website` | `string` | Organization identifier |
| `VIN` | `string` | Vehicle Identification Number |
| `state` | `string` | Target state for title processing |
| `job_status` | `enum` | Processing status: `pending`, `input_parsing`, `enriching`, `generating_forms`, `complete`, `error` |
| `account_status` | `enum` | Lifecycle status (see Account Status Lifecycle above) |
| `account_data` | `object` | Structured vehicle, loan/sale, and owner data (see [Core Ontology](/guides/core-ontology)) |
| `nmvtis_found_status` | `enum` | NMVTIS enrichment result: `pending`, `found`, `not_found`, `error`, `unavailable`, `skipped` |
| `state_found_status` | `enum` | State/private enrichment result (same enum as above) |
| `signature_requests` | `array` | Signing ceremony status (DocuSign, Notarize, etc.) |
| `fee_estimate` | `object` | Calculated DMV fees (tax, registration, title) |
| `webhook_events` | `string[]` | Which subscribed events triggered this webhook delivery |
S3 document paths (`nmvtis_pull`, `state_pull`, `form_*` fields) are delivered as **presigned URLs** valid for 1 hour. Download or process them promptly.
***
## Delivery Semantics
* **Method**: `POST`
* **Content-Type**: `application/json`
* **Retry**: Exponential backoff on 5xx responses (up to 3 retries)
* **Timeout**: 10 second response timeout per attempt
* **Idempotency**: The same event may be delivered more than once. Use `entryId` + `webhook_events` to deduplicate.
### Success / Failure
| Your Response | Our Behavior |
| ------------- | ------------------------------------------------- |
| `2xx` | Delivery confirmed, no retry |
| `4xx` | Client error logged, no retry (fix your endpoint) |
| `5xx` | Retried with exponential backoff |
| Timeout | Treated as 5xx, retried |
***
## Testing
To trigger a test webhook:
1. Create a title entry via `POST /title` with enrichment enabled (`sync_nmvtis: true`)
2. Wait for `job_status` to reach `complete` — this fires `title.job_status.complete` and potentially `title.data.found`
3. Update `account_status` via the dashboard or signing flow to trigger `title.account_status.*` events
You can verify your endpoint received the payload and responded with `2xx`. Contact the Cardinal Gray team if you need help debugging delivery issues.
# Introduction
Source: https://docs.cardinalgray.com/introduction
Welcome to Cardinal Gray's API documentation, where you can learn how to use our platform to streamline DMV transaction workflows.
If you do not already have an API key, please reach out to our team via email or Slack.
Cardinal Gray's API suite enables auto lenders, dealers, insurance companies, and wholesalers to automate their title backoffice through a unified API.
## Authentication
Cardinal Gray supports API-key and Clerk session authentication.
For server-to-server API access, send your API key in the `Authorization` header:
```bash theme={null}
Authorization: Bearer YOUR_API_KEY
```
Legacy clients may also send an API key with `Authorization: Basic YOUR_API_KEY`.
For Clerk-authenticated sessions, send either a Clerk JWT as a bearer token or the Clerk `__session` cookie:
```bash theme={null}
Authorization: Bearer YOUR_CLERK_JWT
```
## Base URLs
* Production: `https://api.cardinalgray.com`
* Staging: `https://api.cardinalzyn.com`