The integration guide
Verification.
Connected.
Verification of assets, employment and income, ordered over one API and delivered as a signed result — plus, for asset orders, a sealed report your counterparty can check without an account.
https://connect.homeequal.aiAbout the sandbox and this guide
Base URL https://connect.homeequal.ai. Everything below is copy-pasteable against it.
Start in the sandbox with a heq_test_ key: it mirrors the production API contract using synthetic data, needs no
borrower, and costs nothing.
The tables on this page are read from the running service at request time, not transcribed. If it is on this page, it is what the service is doing.
Your first connection
Start with a read-only identity check. Confirm your key, environment, and available products before you create an order.
- Get your sandbox key.Set up your institution, then use an issued
heq_test_key. Keys belong on your server. - Make the first call.Set
FINTEGRAL_API_KEYin your shell and run this request. The example placeholder is not a credential. - Confirm test mode.The response must show
"mode": "test"and"livemode": false. Theproductslist identifies what your key may order.
export FINTEGRAL_API_KEY='heq_test_YOUR_ISSUED_KEY'
curl --fail-with-body --silent --show-error \
https://connect.homeequal.ai/api/v1/partner/whoami \
-H "X-API-Key: $FINTEGRAL_API_KEY"$env:FINTEGRAL_API_KEY = 'heq_test_YOUR_ISSUED_KEY'
if (-not $env:FINTEGRAL_API_KEY.StartsWith('heq_test_')) {
throw 'Use your issued sandbox key.'
}
Invoke-RestMethod `
-Uri 'https://connect.homeequal.ai/api/v1/partner/whoami' `
-Headers @{ 'X-API-Key' = $env:FINTEGRAL_API_KEY }A sandbox key is required even though source credentials are not. Sandbox access and live borrower-data access have separate requirements.
Working code. Standard HTTP.
Use the HTTP client built into your runtime. These read-only examples check your issued test key and confirm sandbox mode. They do not create an order or print your key.
// Node.js 20+. Save as connection.mjs, then run: node connection.mjs
try {
const key = process.env.FINTEGRAL_API_KEY || '';
if (!key.startsWith('heq_test_')) {
throw new Error('Set FINTEGRAL_API_KEY to your issued sandbox key.');
}
const response = await fetch(
'https://connect.homeequal.ai/api/v1/partner/whoami',
{ headers: { 'X-API-Key': key }, signal: AbortSignal.timeout(15000) }
);
if (!response.ok) {
throw new Error('Connection refused: HTTP ' + response.status);
}
const identity = await response.json();
if (identity.livemode !== false || identity.mode !== 'test') {
throw new Error('Expected sandbox mode. Check your key and account.');
}
console.log(JSON.stringify({
mode: identity.mode,
products: identity.products || []
}, null, 2));
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}# Python 3. Save as connection.py, then run: python connection.py
import json
import os
import sys
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError
try:
key = os.environ.get('FINTEGRAL_API_KEY', '')
if not key.startswith('heq_test_'):
raise ValueError('Set FINTEGRAL_API_KEY to your issued sandbox key.')
request = Request(
'https://connect.homeequal.ai/api/v1/partner/whoami',
headers={'X-API-Key': key},
)
with urlopen(request, timeout=15) as response:
identity = json.load(response)
if identity.get('livemode') is not False or identity.get('mode') != 'test':
raise ValueError('Expected sandbox mode. Check your key and account.')
print(json.dumps({
'mode': identity['mode'],
'products': identity.get('products', []),
}, indent=2))
except HTTPError as error:
print('Connection refused: HTTP ' + str(error.code), file=sys.stderr)
sys.exit(1)
except URLError:
print('Connection unavailable. Check the network and try again.', file=sys.stderr)
sys.exit(1)
except (ValueError, TimeoutError) as error:
print(str(error), file=sys.stderr)
sys.exit(1)For a verification order, first capture the required consent, then use the returned token with an Idempotency-Key. See ordering for the request and retry handling for ambiguous responses.
Authentication
Every call carries your key in the X-API-Key header. There is no OAuth
dance, no bearer exchange and no session — one header, on every request, over TLS.
curl https://connect.homeequal.ai/api/v1/partner/whoami \
-H "X-API-Key: heq_live_YOUR_API_KEY"whoami is the first call worth making: it answers which tenant the key
belongs to, which products it may order, whether it is live or test, and — usefully — the source IP
we saw you from, which is the number an allowlist is written against.
Two key literals, two worlds
| Prefix | Mode | What it touches |
|---|---|---|
heq_live_ | live | Real borrowers, real source pulls, real money. Counts against the live ceiling and appears on the invoice. |
heq_test_ | test | The sandbox. Synthetic sources only, never billed, and invisible to live keys. |
Use the key issued for your environment. Keys are displayed once at issue. If a key is lost, rotate it and update your integration.
Network restriction
A key may be pinned to your egress addresses (IPv4, CIDR, or exact IPv6). Used from
anywhere else it fails with 403 API_KEY_IP_FORBIDDEN —
so a leaked key is worth nothing outside your network. Confirm what we see you as with the
seenFrom field above before you ask for one.
Scopes
| Scope | Grants |
|---|---|
fraud:analyze | Submit a loan file to the fraud screen (POST /api/v1/fraud/analyze). Billed per analyzed file. |
verify:create | Issue borrower consent and order verifications (POST /api/v1/verify/consent, POST /api/v1/verify). |
verify:read | Read a verification, its report, and this tenant’s usage (GET /api/v1/verify/:id and friends). |
verify:voa | Order and read Verification of Assets (VOA) specifically. See the entitlement rule below. |
verify:voe | Order and read Verification of Employment (VOE) specifically. See the entitlement rule below. |
verify:voi | Order and read Verification of Income (VOI) specifically. See the entitlement rule below. |
verify:income:exact | Receive the exact income figure (income.amount) on reads of verifications whose borrower grant authorized exact-figure disclosure. Without it, results carry the band only. Every exact disclosure is recorded in the verification’s tamper-evident event log. |
keys:rotate | Rotate this tenant’s API keys and webhook secrets. NOT granted by default — issue it to a separate management key. |
Request verify:create and the scopes for your contracted products (verify:voa, verify:voe, verify:voi). Your key may access only authorized products and records. Use whoami to confirm your entitlements before creating an order.
keys:rotate is credential-grade: it can replace your keys and repoint your
webhook. Issue it to a deliberately separate management key, never to the integration key — otherwise a
leaked integration key can mint itself a fresh secret and start the clock on locking you out.
Manage all of this yourself in the developer console: inventory, ages, last use, zero-downtime rotation, webhook configuration and live usage.
Test the outcomes your workflow needs.
Use an issued heq_test_ key for synthetic asset, employment, and income verifications. Test orders are isolated from live orders. Do not use real borrower information.
| Synthetic borrower email | Outcome | Delay |
|---|---|---|
complete@example.com | COMPLETED | 15s |
deny@example.com | DENIED | 15s |
fail@example.com | FAILED | 10s |
expire@example.com | EXPIRED | 120s |
flag@example.com | COMPLETED · manual review flagged | 20s |
Set borrower.emailOrPhone to the chosen test address when ordering. These are synthetic scenarios for the verification API. IRS transcript ordering uses its dedicated workspace and authorization workflow.
Ordering a verification
Three products, ordered the same way and sold separately. The last column says what each product's result carries: a VOE order confirms employment and never returns an income figure. The full reference lists the response fields behind each.
| Product | Name | Scope | Meter | What the result carries |
|---|---|---|---|---|
voa | Verification of Assets | verify:voa | verify_voa | Assets, Verification result |
voe | Verification of Employment | verify:voe | verify_voe | Employment, Verification result |
voi | Verification of Income | verify:voi | verify_voi | Assets, Employment, Income, Verification result |
Step 1 — consent, always first
No verification exists without a permissible-purpose record. Capture the borrower's consent in your own UI, then exchange it here for a single-use, purpose-limited token. The token is minted in the calling key's mode and can only be spent in that mode, so a sandbox rehearsal can never produce the consent record for a real borrower's pull.
curl -X POST https://connect.homeequal.ai/api/v1/verify/consent \
-H "X-API-Key: heq_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"borrower":{"id":"borrower-8891"},"purpose":"mortgage_application"}'{
"token": "CONSENT_TOKEN_SHOWN_ONCE",
"consentId": "vc_0123456789abcdef",
"purpose": "mortgage_application",
"disclosure": "band",
"expiresAt": "2026-08-07T16:04:05Z"
}Exact income — a contracted tier, doubly authorized
By default an income result carries a band (income.band,
exactDisclosed: false). Underwriting runs on figures, so keys contracted for it hold
verify:income:exact and receive income.amount — but only when
both authorities align: your key's scope, and a borrower grant issued with
"disclosure": "exact", which requires the consent text you showed to have named
exact-figure disclosure. Either alone yields the band. Two keys of the same tenant can read the same
verification and see different tiers — that is the design, not a bug.
Exact income requires both borrower authorization and the appropriate tenant entitlement. Exact disclosures are auditable. Retrieve authorized exact values through the result endpoint; webhooks carry the band response. The band remains present in both response tiers.
Employed-as-of re-check — the verbal VOE, replaced
Closing re-verifies employment inside the final days before funding. Order it as a
first-class re-check instead of a phone log: POST /api/v1/verify/:id/recheck (no body,
Idempotency-Key required) creates a fresh employment-only verification chained to the
original. Its authority is the window the borrower granted up front — issue the
original consent with "recheckDays": 45 (max 180) and the consent text you show must name
re-verification through that bound; inside the window no fresh token is needed, outside it the order
is refused. A re-check is always product voe whatever the original was —
it answers continuance, so it can never widen disclosure (an exact-tier VOI original still yields an
income-free re-check), and it needs voe entitlement. The completed re-check carries a
tri-state recheck.continuity block — unchanged is true only when
the employer affirmatively matches and status is affirmatively active; incomplete evidence reads
null, never reassurance — and the original verification's tamper-evident event log
records every re-check ordered and resolved against it.
Step 2 — order it
Idempotency-Key is required on this endpoint: 8–128
characters of [A-Za-z0-9._:-], unique per intended order. A network timeout followed by a
retry is otherwise a second real verification on the same borrower, billed twice. Replaying the same key
with the same body returns the original response with X-Idempotent-Replay: 1; reusing it
with a different body is a 409 IDEMPOTENCY_KEY_CONFLICT, because that is a bug on
your side, not a retry.
curl -X POST https://connect.homeequal.ai/api/v1/verify \
-H "X-API-Key: heq_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: app-2026-000412-voa-01" \
-d @order.json{
"applicationId": "APP-2026-000412",
"product": "voa",
"borrower": {
"id": "borrower-8891",
"fullName": "Jordan Rivera",
"emailOrPhone": "complete@example.com"
},
"consent": {
"token": "CONSENT_TOKEN_FROM_STEP_1",
"capturedAt": "2026-08-07T15:04:05Z",
"purpose": "mortgage_application"
},
"claimedFacts": {
"employer_name": "Northwind Logistics",
"stated_income": 128000
},
"options": {
"policy": "observe"
},
"delivery": {
"email": true
}
}{
"verificationId": "ver_0000000000",
"status": "AWAITING_BORROWER_CONNECT",
"product": "voa",
"borrowerLink": {
"url": "https://homeequal.ai/verify-income/BORROWER_LINK_TOKEN",
"token": "BORROWER_LINK_TOKEN",
"expiresAt": "2026-08-14T15:04:05Z"
}
}202, not 200. The borrower has not done anything yet. Hand them
borrowerLink.url (or set delivery.email and we will), then wait for the
webhook. Always use the returned link; do not construct a borrower URL or infer a
connection flow from the product name. The response example shows the fields
needed to complete this integration.
Two refusals worth handling explicitly, because neither is a bad request:
409 SOURCE_UNAVAILABLE means verification is currently unavailable for that request and we refused
it up front rather than accepting a verification that could never complete — nothing was created, and
your consent token is unspent. 403 PRODUCT_NOT_ENTITLED means the key is product-scoped and
this is not one of its products.
Retry without creating another order.
Create one idempotency key per intended order and keep the original request body. The order and employment re-check endpoints require this header.
| Situation | What to do |
|---|---|
| Timeout or uncertain order response | Retry the same request with the same Idempotency-Key. Keep the original consent token and body. |
| Same key and same body | Within the 24-hour idempotency period, the recorded response is returned with X-Idempotent-Replay: 1. |
| Same key, changed body | 409 IDEMPOTENCY_KEY_CONFLICT. Reconcile the original request before submitting a distinct order. |
| 429 or retryable 503 | Respect Retry-After when provided; use bounded exponential backoff with jitter. |
| 400, 401, 403, or monthly 402 limit | Resolve the request, credentials, permissions, or account limit before retrying. |
Idempotency is endpoint-specific. Consent issuance, key rotation, and webhook configuration do not share the order endpoint's replay contract. Consult the relevant operation before automatically retrying a write.
Getting the result
Webhooks — the path to build on
Every terminal state is delivered to your receiver as a signed envelope. Configure the
URL yourself (PUT /api/v1/partner/webhook, keys:rotate scope) or in the
console.
{
"id": "evt_00000000000000000000",
"type": "verification.completed",
"tenantId": "your-tenant-id",
"livemode": true,
"createdAt": "2026-08-07T15:19:41.204Z",
"data": {
"applicationId": "APP-2026-000412",
"verificationId": "ver_0000000000",
"status": "COMPLETED",
"product": "voa"
}
}Events emitted: verification.completed, verification.denied, verification.expired, verification.failed.
livemode is on the envelope for a reason — read it and route on it.
It is absent-means-live: only an explicit false is a sandbox event. Each mode has its own
receiver URL and its own signing secret, so a test event can never arrive signed with your production
secret, and a sandbox rehearsal can never be booked as a real one by a receiver that was not
paying attention.
| Header | Meaning |
|---|---|
X-HomeEqual-Signature | t=<unix seconds>,v1=<hex hmac sha256> — see below. |
X-HomeEqual-Event-Id | Stable event id. The same id on a retry. |
X-HomeEqual-Event-Type | Same value as type in the body, for routing before parse. |
Idempotency-Key | The event id again — dedupe on it. Retries are guaranteed; duplicates are not a defect. |
Verifying the signature
The HMAC is computed over <timestamp>.<raw body>, so a signature
captured yesterday cannot be replayed onto today's body and vice versa. Reject anything outside a
five-minute window, and compare in constant time.
const crypto = require('node:crypto');
// Verify against the RAW request bytes. Re-serializing the parsed JSON
// changes key order and whitespace, and the signature will never match.
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((s) => s.split('=', 2)));
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(parts.t));
if (!(age <= 300)) return false; // 5-minute replay window
const expected = crypto.createHmac('sha256', secret)
.update(parts.t + '.' + rawBody).digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(String(parts.v1 || ''), 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Delivery retries use exponential backoff on server or network errors. Receivers must be idempotent. Answer 2xx quickly and do the
work afterwards: a receiver that verifies and enqueues never needs a replay. Setting a webhook URL always
issues a fresh signing secret, and secret rotation has no overlap window — a webhook
carries one signature per event, so the very next delivery uses the new secret. Deploy it before you
rotate, not after.
Polling — the fallback, not the design
curl https://connect.homeequal.ai/api/v1/verify/ver_0000000000 \
-H "X-API-Key: heq_live_YOUR_API_KEY"Tenant-scoped, mode-scoped and product-scoped: a key sees only its own tenant's
verifications, only its own mode's, and only the products it is entitled to. Everything else answers
404 — deliberately not 403, so the endpoint cannot be used to discover what
exists.
Results, delivered to your system.
Configure a receiver for each environment. Verify the signature over the raw request bytes, store the event durably, and acknowledge it with a 2xx response.
| Delivery behavior | Integration requirement |
|---|---|
| HMAC-SHA256 signatures | Verify X-HomeEqual-Signature; reject signatures outside the five-minute timestamp tolerance. |
| Duplicate deliveries | Deduplicate on X-HomeEqual-Event-Id, which remains stable across retries. |
| Network failures and 5xx responses | Delivery uses four attempts with jittered backoff. Receiver 4xx responses are not retried. |
| Failed deliveries | Retained failed events can be inspected and replayed by an authorized administrator. Event retention has no fixed time guarantee. |
| Separate environments | Use separate receiver settings and signing secrets for test and live events. Read livemode from every envelope. |
See the event envelope and signature-verification example. Retrieve results with your own authorized key; a webhook does not expand your access.
The API at a glance.
Base URL: https://connect.homeequal.ai. Send your issued key in X-API-Key. Product entitlement, environment, and institution permissions are enforced by the service.
| Operation | Endpoint | Purpose |
|---|---|---|
| GET | /api/v1/partner/whoami | Confirm key mode, scopes, and orderable products |
| POST | /api/v1/verify/consent | Exchange captured consent for a single-use token |
| POST | /api/v1/verify | Create a verification; Idempotency-Key required |
| GET | /api/v1/verify/{verificationId} | Read authorized status and result |
| POST | /api/v1/verify/{verificationId}/recheck | Re-check employment within an authorized consent window; Idempotency-Key required |
| GET | /api/v1/verify/usage | Read usage for the key’s environment |
Request fields and ordering · Results and events · Error codes
The report and its seal
The artifact a lender files and forwards is a PDF, guarded identically to the JSON read
— same tenant scope, same product entitlement, same 404-not-403, so the PDF is
never a way around the shaping applied to the JSON.
Availability is product-scoped: today only voa renders a PDF.
A voe or voi verification returns 409 REPORT_UNAVAILABLE here, and
the JSON read is its deliverable. This is a real limit, not a transient one — build against the JSON and
treat the PDF as additive for asset orders. The seal below likewise exists only where a report does.
curl https://connect.homeequal.ai/api/v1/verify/ver_0000000000/report.pdf \
-H "X-API-Key: heq_live_YOUR_API_KEY" \
-o report.pdfThe seal
Every report carries a sixteen-character seal in four groups. Anyone holding the document — a counterparty, an investor, an auditor with no account here — can check it, unauthenticated, with nothing but the code on the page.
curl https://homeequal.ai/verify/ABCD-EFGH-JKLM-NPQR \
-H "Accept: application/json"Read this part carefully, because the distinction is the whole value of the seal.
| Answer | What it proves | What it does NOT prove |
|---|---|---|
| VALID | This seal was issued by Fintegral, for the report described, and has not been revoked. | That the copy you are holding is unaltered. The check reads our registry and never sees your document — anyone can retype a genuine seal onto a doctored PDF. |
| CONFIRMED | The document’s own content digest matches the one recorded at issue: no figure, name or timestamp has been altered. | Reached only when the caller supplies the digest. It is the answer that is actually about the document. |
| MISMATCH | Issued by us, but the supplied digest does not match. Treat the document as altered. | — |
| REVOKED | Issued by us and since withdrawn — usually corrected or superseded. Do not rely on it. Revocation outranks content: a revoked report answers REVOKED even when its digest matches. | — |
| NOT_FOUND | Nothing matches. Unknown and malformed codes answer identically and at the same cost, so the endpoint cannot be used to enumerate which reports exist. | — |
To get the content-bound answer, pass the report's digest — a sha256 over its canonical facts:
curl "https://homeequal.ai/verify/ABCD-EFGH-JKLM-NPQR?digest=REPORT_CONTENT_DIGEST" \
-H "Accept: application/json"The seal check never discloses the subject's name, an account, or a balance — only facts already printed on the face of the document the asker is holding. Someone who guessed a valid seal learns nothing they could not have learned by holding the report.
The full reference
Two sections are served to key holders only: the sandbox — the outcome
personas and when each one lands — and quotas — the ceilings by tier behind the
402 QUOTA_EXCEEDED response and the X-Quota-* headers. Paste any
heq_test_ or heq_live_ key to open it here, or fetch it directly:
curl -H 'X-API-Key: heq_test_YOUR_API_KEY' https://connect.homeequal.ai/api/v1/partner/docs/referenceNo key yet? The developer console issues sandbox keys, and fintegral@homeequal.ai reaches a person.
Error codes
Every error is { "error": { "code", "message", … } }. Branch on
code for request errors; handle all HTTP 503 responses as temporary failures and retry with backoff. Never branch on the message — messages are written for humans and will be improved.
| Status | Code | Meaning |
|---|---|---|
| 401 | API_KEY_MISSING | No X-API-Key header. |
| 401 | API_KEY_INVALID | Key invalid, revoked, expired, or its literal disagrees with its stored mode. |
| 403 | API_KEY_IP_FORBIDDEN | Key is network-restricted and you are calling from outside its allowlist. |
| 403 | INSUFFICIENT_SCOPE | The key lacks a scope this endpoint requires. The response names the missing one. |
| 403 | PRODUCT_NOT_ENTITLED | A product-scoped key ordered a product it is not scoped for. |
| 400 | INVALID_REQUEST | Body failed validation. validationErrors names the fields. |
| 400 | CONSENT_REQUIRED | No consent token. Nothing is verified without a permissible-purpose record. |
| 400 | CONSENT_INVALID | Token unusable — already spent, expired, or minted in the other mode. |
| 400 | UNKNOWN_PRODUCT | The product field is not one we sell. |
| 400 | PRODUCT_SOURCE_MISMATCH | The named source cannot serve the ordered product. |
| 400 | IDEMPOTENCY_KEY_REQUIRED | This endpoint requires an Idempotency-Key. |
| 400 | IDEMPOTENCY_KEY_INVALID | Key must be 8–128 chars of [A-Za-z0-9._:-]. |
| 404 | NOT_FOUND | No such verification for this key — wrong tenant, wrong mode, or outside its product entitlement. Not 403, deliberately. |
| 409 | IDEMPOTENCY_KEY_CONFLICT | The key was reused with a different body. |
| 409 | SOURCE_UNAVAILABLE | Verification is unavailable for this request. Nothing was created; your consent token is unspent. |
| 409 | LIFECYCLE_MISMATCH | Use the borrower link returned for this verification. Contact support if it is no longer available. |
| 409 | REPORT_UNAVAILABLE | No PDF is produced for that product — voa only. The JSON read is the deliverable for voe/voi. |
| 403 | EXACT_NOT_ENTITLED | This key tried to issue exact-disclosure consent without holding verify:income:exact. A grant no key could honor is a promise to the borrower nobody can keep. |
| 409 | RECHECK_NOT_AUTHORIZED | The original consent granted no re-check window (recheckDays). A re-check the borrower never authorized does not exist. |
| 409 | RECHECK_WINDOW_CLOSED | The granted re-check window has ended. Issue fresh consent and order a new verification. |
| 409 | RECHECK_ORIGINAL_NOT_COMPLETED | Only a COMPLETED verification can be re-checked. |
| 409 | RECHECK_OF_RECHECK | Re-checks chain off the ORIGINAL verification — re-check it again, not the re-check. |
| 409 | RECHECK_PRODUCT_UNSUPPORTED | Asset verifications carry no employment to re-check. |
| 409 | RECHECK_CONSENT_REVOKED | The borrower revoked the grant that carried the re-check window. The standing authorization dies with its grant, whatever the window date says. |
| 403 | KEY_QUARANTINED | Suspicious credentials may be automatically quarantined. Check GET /api/v1/partner/restrictions and contact support to resolve access. |
| 403 | KEY_SUSPENDED | A human suspended this key. Every route refuses until a human lifts it. |
| 429 | RATE_LIMIT_EXCEEDED | Per-minute limit for the tenant. Back off and retry — this one does clear. |
| 429 | KEY_RATE_LIMIT_EXCEEDED | Per-minute limit for the individual API key. Back off and retry. |
| 429 | TOO_MANY_AUTH_FAILURES | Repeated bad keys from one address. Fix the credential before retrying. |
| 402 | QUOTA_EXCEEDED | The monthly ceiling. Does not clear on retry — see above. |
| 503 | VERIFY_UNAVAILABLE | Temporary. Retry. |
Integration support
Keys, rotation, webhooks and live usage are self-service in the developer console. Everything else reaches a person at fintegral@homeequal.ai.