API Reference

Base URL https://api.apiclx.com. Every endpoint takes JSON and returns JSON. Every billable call reports exactly what it charged in the charged object of its response, and every charge is auditable on the usage ledger.

POST/v1/onboardfree

Instant self-serve provisioning. Returns an account id and an API key immediately (the key is shown exactly once). Request upstream integrations by name; they start locked until you submit credentials and an operator approves. If card billing is enabled and you pass amount_usd (min 20), the response includes a Stripe checkout_url to load your first credits.

Request
{ "name": "Jordan Avery", "email": "jordan@company.com", "company": "Company LLC", "services": ["loansifter", "arive"], // optional: loansifter | arive | uwm "amount_usd": 50 // optional: mints a Stripe checkout link }
Response
{ "account_id": "acct_906d2fbbf5", "api_key": "apiclx_live_...", // SHOWN ONCE - store it now "services": { "loansifter": { "label": "LoanSifter / Optimal Blue", "status": "awaiting_credentials", "submit": "POST /v1/services/loansifter/credentials" } }, "checkout_url": "https://checkout.stripe.com/...", // null if card billing not yet enabled "balance": 0 }

Authentication

Pass your API key as a Bearer token on every request. Keys are scoped to one account and carry that account's balance and rate card. Rotate keys anytime; old keys die instantly.

Authorization: Bearer apiclx_live_xxxxxxxxxxxxxxxx Content-Type: application/json

Metering & billing

Accounts are prepaid. The gateway checks your balance before execution - if it can't cover the call you get 402 payment_required and nothing runs, nothing is charged. The debit posts atomically at execution start; if the platform fails mid-run (our side, not a payload problem), the charge auto-credits back and the ledger shows both entries.

EndpointRateCharged when
POST /v1/arive/run$1.40run accepted → job starts
POST /v1/uwm/run$2.00run accepted → job starts
POST /v1/loansifter/price$1.50every price execution (each payload send)
POST /v1/mail/lookup$0.50every lookup, hit or miss
POST /v1/avm/value$2.50per address resolved
POST /v1/fees/escrow$1.00per quote generated
POST /v1/tax/lookup$0.75per parcel resolved
GET /v1/jobs/:id · /v1/usage · /v1/balancefree-

Services & credentials

Endpoints split into two classes. Data services (Home Value, Escrow Fees, Property Tax, Mail Lookup) run on our infrastructure and are live the moment your account has credits. Upstream integrations (LOS, PPE/Optimal Blue, lender) run against your accounts, so they require your credentials and an operator approval before the endpoint unlocks. Credentials are encrypted at rest (AES-256-GCM) and never returned by the API.

Service lifecycle
not_requested → awaiting_credentials → pending_approval → approved (request at onboard) (you POST creds) (operator OK) endpoint unlocks
GET /v1/services - where each integration stands
{ "services": { "arive": { "label": "LOS (Arive)", "status": "awaiting_credentials" }, "loansifter": { "label": "LoanSifter / Optimal Blue", "status": "approved" }, "uwm": { "label": "UWM EASE", "status": "not_requested" } }, "data_services": { "avm": "enabled", "fees": "enabled", "tax": "enabled", "mail": "enabled" } }
POST /v1/services/{service}/credentials

Submit the login your automation uses for that upstream. The exact fields mirror how each integration authenticates. On submit, the service moves to pending_approval and an operator is alerted. Resubmit anytime to rotate.

# LoanSifter / Optimal Blue POST /v1/services/loansifter/credentials { "credentials": { "username": "...", "password": "..." } } # LOS (Arive) - includes the TOTP secret so runs re-auth headlessly POST /v1/services/arive/credentials { "credentials": { "email": "...", "password": "...", "totp_secret": "..." } } # UWM EASE POST /v1/services/uwm/credentials { "credentials": { "username": "...", "password": "..." } }
Calling a locked endpoint returns 403
{ "error": { "code": "service_pending_approval", "service": "loansifter", "message": "LoanSifter / Optimal Blue credentials are under review. This endpoint unlocks once approved." } }

Codes: service_not_enabled (never requested), service_awaiting_credentials (requested, no creds yet), service_pending_approval (creds in, awaiting operator). None of these charge, the call is refused before the meter runs.

Idempotency

Send an Idempotency-Key header (any unique string, e.g. a UUID) on billable POSTs. Retrying with the same key within 24h returns the original response and is never charged twice. Without the header, every send is a fresh, billed execution.

Idempotency-Key: 4f1c9a2e-77aa-4b1f-9d0c-2f5e8b6a1c3d

Errors

Errors are JSON with a stable machine code. Validation failures are never charged.

HTTPcodeMeaning
400invalid_payloadSchema validation failed, details lists each field. Not charged.
401invalid_keyMissing, revoked, or malformed API key.
402payment_requiredBalance can't cover this call. Fund the account; nothing ran.
409duplicate_runAn identical run is already in flight for this file (idempotency window).
422upstream_rejectedThe upstream system rejected the file (their validation). Charged, the run executed. Details included.
429rate_limitedPer-key rate limit hit. Retry-After header included.
500platform_errorOur fault. Auto-credited; safe to retry with the same Idempotency-Key.
Error shape
{ "error": { "code": "invalid_payload", "message": "borrower.fico must be an integer between 300 and 850", "details": [{ "field": "borrower.fico", "problem": "out_of_range" }] }, "request_id": "req_1b8d33f7" }

POST/v1/arive/run$1.40 / run

Creates and populates a complete LOS loan file from one payload. Asynchronous: the response returns a job immediately; poll job status or register a webhook for completion. Typical completion: 2 to 4 minutes.

Request
{ "borrower": { "first_name": "Jordan", "last_name": "Avery", "phone": "+15125550143", "email": "jordan@example.com", "dob": "1984-03-19", "ssn": "***-**-1234", // tokenized or full; encrypted at rest, never logged "marital_status": "married" }, "co_borrower": null, // same shape as borrower, or null "property": { "address": "402 Franklin Ave N", "city": "Fayetteville", "state": "TN", "zip": "37334", "occupancy": "primary", "estimated_value": 385000 }, "loan": { "purpose": "cashout_refinance", // purchase | rate_term_refinance | cashout_refinance "type": "VA", // VA | FHA | Conventional "amount": 312000, "current_balance": 268400, "current_rate": 7.125 }, "income": [{ "source": "employment", "monthly": 8450 }], "webhook_url": "https://yourapp.com/hooks/apiclx" // optional }
Response, 202 Accepted
{ "request_id": "req_77aa02c4", "charged": { "amount": 1.40, "currency": "USD" }, "job": { "id": "job_ar_5c2f91", "status": "queued", "poll": "/v1/jobs/job_ar_5c2f91" } }
Completed job result (via /v1/jobs or webhook)
{ "job_id": "job_ar_5c2f91", "status": "succeeded", "result": { "file_number": "17424584", "deep_link": "https://los.example/app/loans/17424584", "created_fields": 64, "warnings": [] } }
Troubleshooting
SymptomCauseFix
job stuck in queuedruns are serialized per upstream; a long run is ahead of yourspoll /v1/jobs/:id; queued longer than 10 min auto-fails and auto-credits
422 upstream_rejectedthe LOS rejected a field (bad state code, missing SSN format)read result.warnings and details, correct the payload, resend with a NEW Idempotency-Key
duplicate file createdtwo sends without an Idempotency-Keyalways send Idempotency-Key; retries then return the original job

POST/v1/uwm/run$2.00 / run

Full lender-side cycle: imports the loan into UWM EASE, triggers the soft-credit pull, reads liabilities back, and returns a MISMO 3.4 export. Asynchronous. Requires borrower consent attestation in the payload, runs without it are rejected with 400 (not charged).

Request
{ "source": { "mismo34_url": "https://yourapp.com/files/loan.xml" }, // or inline "mismo34_b64" "credit": { "pull": "soft", "consent": { "attested_by": "lo@yourcompany.com", "method": "verbal", "at": "2026-08-12T15:04:00Z" } }, "outputs": ["liabilities", "mismo34"], "webhook_url": "https://yourapp.com/hooks/apiclx" }
Completed job result
{ "job_id": "job_uw_b81e42", "status": "succeeded", "result": { "lender_loan_id": "1073741812", "credit": { "status": "completed", "type": "soft" }, "liabilities": [ { "creditor": "CHASE CARD", "category": "credit_card", "balance": 5000.00, "monthly": 150.00 } ], "mismo34_b64": "PD94bWwgdmVyc2lvbj0iMS4wIj8+..." } }
Troubleshooting
SymptomCauseFix
400 on submitmissing credit.consent attestationinclude consent.attested_by, method, and timestamp; runs never start without it
job failed: upstream_maintenancethe lender portal has a recurring outage windowsafe to retry with the same Idempotency-Key; the charge auto-credited
liabilities array emptythin credit file or credit still processing upstreamre-run outputs ["liabilities"] after a few minutes; only the run charge applied, re-reads of a completed job are free

GET/v1/jobs/:idfree

Status of an asynchronous run. States: queued → running → succeeded | failed. A failed job caused by the platform is auto-credited and shows "credited": true.

{ "job_id": "job_ar_5c2f91", "status": "running", "started_at": "2026-08-12T17:02:11Z", "eta_seconds": 90 }

POST/v1/loansifter/price$1.50 / price

A complete PPE pricing scenario, synchronous. You are charged each time you press price, i.e., every payload send is one metered execution, whether one product or forty comes back.

Request
{ "loan": { "purpose": "rate_term_refinance", "type": "VA", "amount": 366760, "term_months": 360 }, "property": { "state": "TX", "county": "Travis", "value": 495000, "occupancy": "primary", "type": "sfr" }, "borrower": { "fico": 742, "self_employed": false }, "options": { "lock_days": 30, "comp": "lender_paid" } }
Response, 200
{ "request_id": "req_9f2ce1b0", "charged": { "amount": 1.50, "currency": "USD" }, "scenario": { "ltv": 74.1, "dti_hint": null }, "products": [ { "lender": "WHOLESALE-A", "name": "VA 30-Yr Fixed", "rate": 6.250, "price": 100.125, "pi_monthly": 2258.44 }, { "lender": "WHOLESALE-B", "name": "VA 30-Yr Fixed", "rate": 6.500, "price": 101.480, "pi_monthly": 2318.71 } ] }
Troubleshooting
SymptomCauseFix
products: []no eligible products for the scenario (LTV/FICO/state caps)check scenario.ltv in the response; loosen the scenario. The call is still charged, it executed
422 upstream_rejectedpurpose/state combination the PPE refuses (e.g. TX cash-out rules)price it as rate_term or a HELOC product set instead
slow responses (10s+)cold PPE session being re-establishednormal on the first call of the hour; subsequent calls return in 2 to 4s

POST/v1/mail/lookup$0.50 / lookup

Direct-mail response resolution. Your call center takes an inbound mailer call, sends the mailer ID (and anything else the caller gives you) as JSON, the API returns the matched prospect record as JSON. Charged per lookup, hit or miss (match is null on a miss).

Request, the payload you send
{ "mailer_id": "AMS-4471820", // required: the code printed on the mail piece "caller": { // optional: sharpens the match "phone": "+19282577299", "name": "Pablo Gomez", "zip": "85305" }, "campaign_hint": "va_refi_q3" // optional: your campaign tag, echoed back }
Response, the payload we send back
{ "request_id": "req_3d91aa07", "charged": { "amount": 0.50, "currency": "USD" }, "match": { "confidence": 0.97, // 0..1; how sure the resolver is "prospect": { "first_name": "Pablo", "last_name": "Gomez", "phone": "+19282577299", "property": { "address": "7119 Odonnell Ln", "city": "Glendale", "state": "AZ", "zip": "85305" }, "loan_hints": { "est_balance": 243000, "loan_type": "VA", "est_rate": 6.875 } }, "mail_piece": { "campaign": "va_refi_q3", "drop_date": "2026-07-28", "offer": "rate_review" } }, "alternates": [] // lower-confidence candidates, same prospect shape }
Miss
{ "request_id": "req_44b0c9e2", "charged": { "amount": 0.50 }, "match": null, "alternates": [] }
Troubleshooting
SymptomCauseFix
match: null on a good IDthe mail piece is from a drop not yet in the indexdrops load within 24h of mailing; retry the next day, or pass caller.phone for a phone-first match
low confidence (< 0.6)several prospects share the ID prefixsend caller.zip or caller.name; alternates[] lists the runners-up
429 rate_limitedburst of lookups from a call-center spikerespect Retry-After (seconds); the limiter refills continuously

POST/v1/avm/value$2.50 / address

Institutional-grade automated valuation (HouseCanary-class data). Synchronous.

Request
{ "address": "5307 Ithaca Ln", "city": "Sarasota", "state": "FL", "zip": "34243" }
Response, 200
{ "request_id": "req_be7712d0", "charged": { "amount": 2.50, "currency": "USD" }, "valuation": { "value": 418200, "range": { "low": 397300, "high": 439100 }, "confidence": 0.91, "fsd": 0.05, "comparables_used": 14, "as_of": "2026-08-11" } }
Troubleshooting
SymptomCauseFix
422 address_not_foundthe address doesn't normalize to a residential parcelsend unit numbers separately, drop suite/apt text from address, verify the zip
low confidence (< 0.7)thin comparables in the areathe range object is the honest answer; treat value as a midpoint, not gospel

POST/v1/fees/escrow$1.00 / quote

County-accurate escrow, title, and recording fees for a purchase or refinance, itemized for your Loan Estimate. Synchronous.

Request
{ "transaction": "refinance", // purchase | refinance "state": "CA", "county": "San Diego", "loan_amount": 312000, "property_value": 495000 }
Response, 200
{ "request_id": "req_0acc41f8", "charged": { "amount": 1.00, "currency": "USD" }, "quote": { "total": 2381.00, "items": [ { "le_section": "B", "name": "Lender's Title Insurance", "amount": 1180.00 }, { "le_section": "B", "name": "Settlement / Escrow Fee", "amount": 890.00 }, { "le_section": "E", "name": "Recording Fees", "amount": 311.00 } ] } }
Troubleshooting
SymptomCauseFix
400 invalid_payload: countycounty name misspelled or not in the stateuse the county exactly as the USPS/city lookup returns it; no 'County' suffix
quote seems high/lowpurchase vs refinance rates differ by state programconfirm the transaction field; refinance reissue rates apply only when transaction=refinance

POST/v1/tax/lookup$0.75 / parcel

The property's current county tax bill, the number underwriting actually uses, not an estimate. Synchronous.

Request
{ "address": "328 Bear Country Dr", "city": "Aledo", "state": "TX", "zip": "76008" }
Response, 200
{ "request_id": "req_51e0b3aa", "charged": { "amount": 0.75, "currency": "USD" }, "tax": { "annual": 6841.22, "parcel": "R000123456", "year": 2026, "authorities": ["Parker County", "Aledo ISD"] } }
Troubleshooting
SymptomCauseFix
422 parcel_not_foundnew construction or a recent split not yet on the county rollretry next quarter or use /v1/avm/value's tax estimate field as a stopgap
annual looks stalecounty publishes annually; mid-year increases lagthe year field tells you the roll year the figure is from

GET/v1/usage?from=2026-08-01&to=2026-08-12free

Your metering ledger: one row per charge or credit, traceable to the request that caused it. GET /v1/balance returns just the current balance.

{ "balance": 184.30, "entries": [ { "at": "2026-08-12T17:02:09Z", "endpoint": "/v1/uwm/run", "amount": -2.00, "request_id": "req_77aa02c4" }, { "at": "2026-08-12T16:48:51Z", "endpoint": "/v1/mail/lookup", "amount": -0.50, "request_id": "req_3d91aa07" }, { "at": "2026-08-12T09:00:00Z", "type": "deposit", "amount": 200.00 } ] }

Webhooks

Async runs POST their final state to your webhook_url, signed with your webhook secret (HMAC-SHA256 of the raw body in the X-Apiclx-Signature header). Retries with backoff for 24h until you return 2xx.

POST https://yourapp.com/hooks/apiclx X-Apiclx-Signature: sha256=8b1f4e... { "event": "job.succeeded", "job_id": "job_ar_5c2f91", "result": { /* same shape as GET /v1/jobs */ } }