FIN∫EGRALA productDeveloper documentation
FintegralDevelopersIntegration guide

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.

Base URLhttps://connect.homeequal.ai
About 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.

  1. Get your sandbox key.Set up your institution, then use an issued heq_test_ key. Keys belong on your server.
  2. Make the first call.Set FINTEGRAL_API_KEY in your shell and run this request. The example placeholder is not a credential.
  3. Confirm test mode.The response must show "mode": "test" and "livemode": false. The products list identifies what your key may order.
cURL · Bash
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"
PowerShell
$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.

JavaScript · Node.js 20+
// 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 · standard library
# 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.

bash
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

PrefixModeWhat it touches
heq_live_liveReal borrowers, real source pulls, real money. Counts against the live ceiling and appears on the invoice.
heq_test_testThe 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

ScopeGrants
fraud:analyzeSubmit a loan file to the fraud screen (POST /api/v1/fraud/analyze). Billed per analyzed file.
verify:createIssue borrower consent and order verifications (POST /api/v1/verify/consent, POST /api/v1/verify).
verify:readRead a verification, its report, and this tenant’s usage (GET /api/v1/verify/:id and friends).
verify:voaOrder and read Verification of Assets (VOA) specifically. See the entitlement rule below.
verify:voeOrder and read Verification of Employment (VOE) specifically. See the entitlement rule below.
verify:voiOrder and read Verification of Income (VOI) specifically. See the entitlement rule below.
verify:income:exactReceive 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:rotateRotate 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 emailOutcomeDelay
complete@example.comCOMPLETED15s
deny@example.comDENIED15s
fail@example.comFAILED10s
expire@example.comEXPIRED120s
flag@example.comCOMPLETED · manual review flagged20s

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.

ProductNameScopeMeterWhat the result carries
voaVerification of Assetsverify:voaverify_voaAssets, Verification result
voeVerification of Employmentverify:voeverify_voeEmployment, Verification result
voiVerification of Incomeverify:voiverify_voiAssets, 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.

bash
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"}'
201 Created
{
  "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.

bash
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
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
  }
}
202 Accepted
{
  "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.

SituationWhat to do
Timeout or uncertain order responseRetry the same request with the same Idempotency-Key. Keep the original consent token and body.
Same key and same bodyWithin the 24-hour idempotency period, the recorded response is returned with X-Idempotent-Replay: 1.
Same key, changed body409 IDEMPOTENCY_KEY_CONFLICT. Reconcile the original request before submitting a distinct order.
429 or retryable 503Respect Retry-After when provided; use bounded exponential backoff with jitter.
400, 401, 403, or monthly 402 limitResolve 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.

event envelope
{
  "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.

HeaderMeaning
X-HomeEqual-Signaturet=<unix seconds>,v1=<hex hmac sha256> — see below.
X-HomeEqual-Event-IdStable event id. The same id on a retry.
X-HomeEqual-Event-TypeSame value as type in the body, for routing before parse.
Idempotency-KeyThe 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.

node
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

bash
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 behaviorIntegration requirement
HMAC-SHA256 signaturesVerify X-HomeEqual-Signature; reject signatures outside the five-minute timestamp tolerance.
Duplicate deliveriesDeduplicate on X-HomeEqual-Event-Id, which remains stable across retries.
Network failures and 5xx responsesDelivery uses four attempts with jittered backoff. Receiver 4xx responses are not retried.
Failed deliveriesRetained failed events can be inspected and replayed by an authorized administrator. Event retention has no fixed time guarantee.
Separate environmentsUse 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.

OperationEndpointPurpose
GET/api/v1/partner/whoamiConfirm key mode, scopes, and orderable products
POST/api/v1/verify/consentExchange captured consent for a single-use token
POST/api/v1/verifyCreate a verification; Idempotency-Key required
GET/api/v1/verify/{verificationId}Read authorized status and result
POST/api/v1/verify/{verificationId}/recheckRe-check employment within an authorized consent window; Idempotency-Key required
GET/api/v1/verify/usageRead usage for the key’s environment

Request fields and ordering · Results and events · Error codes

Working with IRS tax transcripts?Open the dedicated IRS IVES workspace

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.

bash
curl https://connect.homeequal.ai/api/v1/verify/ver_0000000000/report.pdf \
  -H "X-API-Key: heq_live_YOUR_API_KEY" \
  -o report.pdf

The 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.

bash
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.

AnswerWhat it provesWhat it does NOT prove
VALIDThis 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.
CONFIRMEDThe 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.
MISMATCHIssued by us, but the supplied digest does not match. Treat the document as altered.—
REVOKEDIssued 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_FOUNDNothing 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:

bash
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:

bash
curl -H 'X-API-Key: heq_test_YOUR_API_KEY' https://connect.homeequal.ai/api/v1/partner/docs/reference

No 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.

StatusCodeMeaning
401API_KEY_MISSINGNo X-API-Key header.
401API_KEY_INVALIDKey invalid, revoked, expired, or its literal disagrees with its stored mode.
403API_KEY_IP_FORBIDDENKey is network-restricted and you are calling from outside its allowlist.
403INSUFFICIENT_SCOPEThe key lacks a scope this endpoint requires. The response names the missing one.
403PRODUCT_NOT_ENTITLEDA product-scoped key ordered a product it is not scoped for.
400INVALID_REQUESTBody failed validation. validationErrors names the fields.
400CONSENT_REQUIREDNo consent token. Nothing is verified without a permissible-purpose record.
400CONSENT_INVALIDToken unusable — already spent, expired, or minted in the other mode.
400UNKNOWN_PRODUCTThe product field is not one we sell.
400PRODUCT_SOURCE_MISMATCHThe named source cannot serve the ordered product.
400IDEMPOTENCY_KEY_REQUIREDThis endpoint requires an Idempotency-Key.
400IDEMPOTENCY_KEY_INVALIDKey must be 8–128 chars of [A-Za-z0-9._:-].
404NOT_FOUNDNo such verification for this key — wrong tenant, wrong mode, or outside its product entitlement. Not 403, deliberately.
409IDEMPOTENCY_KEY_CONFLICTThe key was reused with a different body.
409SOURCE_UNAVAILABLEVerification is unavailable for this request. Nothing was created; your consent token is unspent.
409LIFECYCLE_MISMATCHUse the borrower link returned for this verification. Contact support if it is no longer available.
409REPORT_UNAVAILABLENo PDF is produced for that product — voa only. The JSON read is the deliverable for voe/voi.
403EXACT_NOT_ENTITLEDThis 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.
409RECHECK_NOT_AUTHORIZEDThe original consent granted no re-check window (recheckDays). A re-check the borrower never authorized does not exist.
409RECHECK_WINDOW_CLOSEDThe granted re-check window has ended. Issue fresh consent and order a new verification.
409RECHECK_ORIGINAL_NOT_COMPLETEDOnly a COMPLETED verification can be re-checked.
409RECHECK_OF_RECHECKRe-checks chain off the ORIGINAL verification — re-check it again, not the re-check.
409RECHECK_PRODUCT_UNSUPPORTEDAsset verifications carry no employment to re-check.
409RECHECK_CONSENT_REVOKEDThe borrower revoked the grant that carried the re-check window. The standing authorization dies with its grant, whatever the window date says.
403KEY_QUARANTINEDSuspicious credentials may be automatically quarantined. Check GET /api/v1/partner/restrictions and contact support to resolve access.
403KEY_SUSPENDEDA human suspended this key. Every route refuses until a human lifts it.
429RATE_LIMIT_EXCEEDEDPer-minute limit for the tenant. Back off and retry — this one does clear.
429KEY_RATE_LIMIT_EXCEEDEDPer-minute limit for the individual API key. Back off and retry.
429TOO_MANY_AUTH_FAILURESRepeated bad keys from one address. Fix the credential before retrying.
402QUOTA_EXCEEDEDThe monthly ceiling. Does not clear on retry — see above.
503VERIFY_UNAVAILABLETemporary. 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.