W W.A.L.T.Developers Main site
Integration guide · API v1

WALT Partner API — Integration Guide for CRM Connectors

API version: v1 · Document version: 1.0 (2026-10-28)
Audience: technical teams at lenders who connect their CRM (Salesforce, Microsoft Dynamics, HubSpot, a bespoke system, or an integration platform) to WALT.

Status — available on request. The v1 Partner API is available in the sandbox on request; production access is by arrangement. The base URLs in §3 are the intended public hosts: your onboarding email gives the base URLs and credentials to use. Within /v1 changes are additive (§3), and every change appears in the changelog (§16).


1. What you can build

With the Partner API your CRM can:

  • Submit a loan application — borrowers, guarantor companies, the security property or properties, and loan details — straight from the CRM record. Nobody re-keys anything into WALT.
  • Attach documents, such as the RICS valuation report.
  • Start a WALT assessment. WALT runs identity, AML/sanctions/PEP, credit, company, valuation and property checks in parallel and returns an audited recommendation, usually within 2 minutes.
  • Receive the result back in the CRM: recommendation (Approve / Approve with conditions / Refer / Decline), risk and confidence scores, step-by-step outcomes, conditions precedent and subsequent, and the signed PDF report.
  • Track borrower actions. WALT tells your CRM when a borrower still has to complete biometric ID verification, and again when the assessment updates after they do.

Your underwriters keep working in WALT if they want to. Every case created through the API also appears in the WALT webapp, marked "Created via CRM", and every underwriter in your organisation can see it.

Human decision. WALT produces an advisory recommendation and evidence. A qualified underwriter at your firm must make and record the lending decision. Do not configure your CRM to approve or decline a loan automatically on the WALT recommendation alone (see §14).


2. Core concepts

Diagram
Case ──┬── Individuals (borrowers, guarantors)          ┐
       ├── Companies (borrowing / guarantor entities)    ├─ "subjects"
       ├── Properties (security)                         ┘
       ├── Ownerships (individual ↔ company links)
       ├── Documents (e.g. valuation report)
       └── Assessments (one per run of WALT's checks)
               ├── Checks (one per check × subject)
               ├── Decision (recommendation, scores, conditions)
               └── Report (PDF)
ConceptMeaning
CaseOne loan application. Holds the loan details and the subjects. Your CRM's opportunity or application record maps to a Case through external_ref.
SubjectAn individual, company or property the checks run against.
Case typeWhich WALT suite assesses the case: credit_identity, property_lending, property_buy_to_let, property_development, property_real_estate or full. Your contract must include the matching module (§7.1).
AssessmentOne execution of all applicable checks for the case's current data. You can run several over time, for example after a correction. A re-assessment reuses every earlier result whose input data has not changed and that succeeded. Only changed, failed or missing checks are searched again, and you pay only for the subjects re-searched (§6, Step 6). Sanctions, PEP and adverse-media screens are never reused: they are searched again every time.
CheckA single evaluation of one subject, for example equifax on an individual or land_registry on a property. Checks are grouped into steps: borrower_profile, kyc_and_aml, credit_assessment, property_valuation, property_due_diligence, borrower_call.
DecisionThe typed result of an assessment (§8).
TokenUsage unit. One assessment costs 1 token for the first individual, company and property, plus 1 per additional subject. A multi-unit building counts one token per unit. Call POST …/assessments/quote to see the cost first.

Assessment lifecycle

Diagram
            POST /v1/cases/{id}/assessments
                         │
                     ┌───▼───┐
                     │queued │
                     └───┬───┘
                     ┌───▼────┐   failed / cancelled
                     │running ├──────────────────────────┐
                     └───┬────┘                          │
           ┌─────────────┴─────────────┐                 │
  ┌────────▼─────────┐        ┌────────▼────────┐  ┌─────▼────┐
  │ awaiting_action  │───────►│   completed     │  │ failed / │
  │ (borrower must   │ borrower│ decision final │  │ cancelled│
  │  act; provisional│ acts or │                │  └──────────┘
  │  decision exists)│ expires └────────────────┘
  └──────────────────┘
  • awaiting_action: a provisional decision is already available (decision.is_final = false). WALT emits assessment.action_required, then assessment.updated when the borrower completes the step or it expires.
  • completed: decision.is_final = true.
  • If you edit case data after an assessment, that assessment becomes stale: true. Start a new one to take the change into account. By default it is incremental: checks unaffected by your edit are carried forward, not searched again.

3. Environments and versioning

EnvironmentBase URLDataBilling
Sandboxhttps://dev-api.underwritinguk.ai/v1 (WALT's dev environment; your onboarding email gives the base URL to use)Synthetic. Never send real personal data. Your sandbox organisation's providers are simulated (§11).Free
Productionhttps://api.underwritinguk.ai/v1 (by arrangement; the base URL is confirmed when production access is issued)Live checks against credit bureaus and KYC providersTokens from your plan
  • Sandbox and production credentials are separate. A sandbox credential never works in production, and vice versa.
  • Versioning. The major version is in the path (/v1).
    • Within a major version WALT only makes additive changes: new endpoints, new optional fields, new enum values, new event types. Your code must ignore unknown fields and tolerate unknown enum values.
    • Breaking changes ship as /v2. /v1 stays supported for at least 12 months after /v2 is released.
    • Deprecations are announced by email to your technical contact and through the Deprecation / Sunset response headers.
  • OpenAPI. The machine-readable spec is at GET /v1/openapi.json (no authentication needed). Import it into Postman, Power Automate, MuleSoft, Boomi, or your code generator.

4. Authentication

WALT uses OAuth 2.0 client credentials (RFC 6749 §4.4). Each integration gets its own client ID and client secret from WALT during onboarding.

  • A credential belongs to exactly one of your WALT organisations and one environment.
  • Each credential carries a set of scopes.
  • It can optionally be restricted to an IP allow-list. WALT checks the address that reached its Front Door edge (X-Azure-ClientIP, trusted only on requests carrying WALT's own Front Door id in X-Azure-FDID); an X-Forwarded-For value you send is never trusted.

4.1 Get an access token

HTTP
POST /v1/oauth/token
Host: api.underwritinguk.ai
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&scope=cases:write assessments:write assessments:read reports:read
JSON
200 OK
{
  "access_token": "walt_at_3Jm9…",
  "token_type": "Bearer",
  "expires_in": 900,
  "scope": "cases:write assessments:write assessments:read reports:read"
}
  • The token is valid for 15 minutes. Cache it and reuse it until about 60 seconds before it expires, then request a new one. Do not request a token for every API call: the token endpoint is rate-limited (§16).
  • client_secret_post (credentials in the form body) is also accepted.
  • scope is optional. If you leave it out, the token gets every scope granted to the credential. Asking for a scope the credential does not hold returns 400 with "error": "invalid_scope" (an OAuth error body, not problem+json).
  • Send the token on every call: Authorization: Bearer walt_at_….
  • Never put tokens or secrets in URLs, query strings, logs, or browser code.

4.2 Scopes

ScopeAllows
cases:readRead cases, subjects and documents (personal data minimised, see pii:read)
cases:writeCreate and update cases, subjects, ownerships; close cases
documents:writeUpload documents
assessments:writeRequest quotes, start and cancel assessments
assessments:readRead assessments, decisions and check results
reports:readDownload PDF reports (together with pii:read)
account:readRead entitlements, quota and usage
lookups:readAddress and UPRN lookup
webhooks:manageManage your webhook endpoints
pii:readRead back write-only sensitive fields (passport number, bank details). Also required to download PDF reports, which contain unminimised personal data. Not granted by default.
act_asAttribute actions to a named underwriter (§4.3). Not granted by default.

Least privilege: a connector that only pushes applications and reads decisions needs cases:write assessments:write assessments:read. Add reports:read pii:read only if it downloads the PDF report (otherwise 403 insufficient_scope).

Which cases a credential sees: every credential of your organisation can read every case of your organisation, whichever integration created it, including cases your underwriters created in the WALT webapp. Use created_via=api|webapp and external_ref to filter. Scopes still limit what a credential can do, and personal data stays hidden without pii:read. Any credential with cases:write may edit, close or assess any case of your organisation, including cases created in the webapp; there is no per-integration owner check. Another organisation's case always returns 404.

4.3 Acting on behalf of an underwriter (optional)

By default, actions are attributed to your integration (for example "Salesforce Connector") in WALT's audit trail. If you have the act_as scope, add:

HTTP
WALT-Acting-User: jane.doe@yourlender.co.uk
  • The user must already exist and be active in your WALT organisation; WALT never creates users from this header.
  • The audit trail then records both the integration and the user.
  • New cases are assigned to that user.

4.4 Credential custody (mandatory)

  • Store the client secret only in your platform's secret store: Salesforce External/Named Credentials, Azure Key Vault, AWS Secrets Manager, HashiCorp Vault, HubSpot secrets, or the iPaaS connection vault. Never in code, custom fields, spreadsheets or tickets.
  • Use a separate credential for each environment and each integration. Sandbox credentials must never be copied into production orgs, and production secrets must not reach CRM sandbox refreshes.
  • Rotate at least every 12 months. Rotation gives you a new secret while the old one keeps working for a grace period (default 7 days).
  • If you suspect a secret has leaked, contact WALT support (§16). Revocation takes effect immediately.

5. API conventions

TopicConvention
FormatJSON (UTF-8) for requests and responses, except document uploads (multipart/form-data). Field names are snake_case. A JSON body must be sent with Content-Type: application/json; without it the request is refused (422 validation_failed) and the body is not parsed.
IDsWALT IDs are UUIDs. Treat them as opaque strings.
Your referencesEvery case, individual, company, property and document accepts an external_ref (≤ 128 chars), normally your CRM record ID. external_ref is unique per case in your organisation. Look a case up with GET /v1/cases?external_ref=….
Dates and timesDates are YYYY-MM-DD. Timestamps are ISO 8601 in UTC with Z (2026-10-09T14:03:22Z).
MoneyInteger pounds sterling (GBP), no pence, no separators (392000). Range 0–999,999,999,999 (rent, service charge and maintenance costs: 0–2,147,483,647).
CountriesISO 3166-1 alpha-2 (GB, IE, AE). Nationality uses the same codes.
PhoneE.164 (+447700900123).
Nulls and partial updatesPATCH changes only the fields you send. Send null to clear an optional field.
IdempotencyRequired on every POST that creates something: Idempotency-Key: <uuid-v4> (missing → 400 idempotency_key_required). Same key and same request → the original response is returned (Idempotent-Replayed: true), nothing is created twice. The request includes the body, its media type, the acting user (WALT-Acting-User) and the credential's scopes: the same key with any of these changed → 409 idempotency_conflict. First request still running → 409 idempotency_in_progress (retry shortly). Processed, but the response cannot be returned (it could not be recorded, or can no longer be decrypted) → 409 idempotency_result_unavailable: nothing runs twice; read the current state with a GET, and use a new key only for a genuinely new operation. Refusals you are told to retry (409, 429, any response with Retry-After) and 5xx are never stored, so a retry with the same key runs again. Keys are kept 24 h. Generate one key per logical operation and reuse it on retries. A quote ignores the key.
ConcurrencyGET responses for cases and subjects include an ETag. Send If-Match: <etag> on PATCH to avoid overwriting someone else's change; a mismatch returns 412 precondition_failed.
PaginationCursor-based: ?limit=50&cursor=… (max 100). Responses contain data[] and next_cursor (null on the last page). An invalid cursor → 400 bad_request.
FilteringGET /v1/cases?external_ref=&status=&updated_since=&created_via=.
Request IDsSend X-Request-ID (UUID) if you have one, otherwise WALT generates it. It is echoed in every response. Log it and quote it to support.
Errorsapplication/problem+json (RFC 9457) with a stable code (§15).
Rate limitsRateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers on every response. A 429 includes Retry-After (seconds).
CompatibilityIgnore unknown fields. Tolerate new enum values: treat an unknown recommendation as refer, and an unknown check outcome as refer.

6. End-to-end integration flow

The recommended flow:

  1. Authenticate.
  2. Check entitlements (once a day is enough).
  3. Create the case with all its subjects in one call.
  4. Upload documents.
  5. Get a quote.
  6. Start the assessment.
  7. Wait for the webhook, or poll.
  8. Fetch the decision and report.
  9. Write the results back to the CRM.

Step 1 — Token

See §4.1.

Step 2 — Entitlements (cache for 24 h)

HTTP
GET /v1/account
JSON
{
  "organisation": {"id": "cda78dc2-…", "name": "Example Bridging Ltd"},
  "modules": ["credit_identity_profile", "property_profile", "underwriting_suite"],
  "allowed_case_types": ["credit_identity", "property_lending", "property_buy_to_let", "property_development", "property_real_estate", "full"],
  "quota": [
    {"module": "underwriting_suite", "period_start": "2026-10-01", "included": 200, "used": 37, "remaining": 163}
  ],
  "rate_limits": {"requests_per_minute": 600, "assessments_per_hour": 60}
}

included: -1 means unlimited.

Step 3 — Create the case (one transactional call)

HTTP
POST /v1/cases
Idempotency-Key: 6f1c9a52-6a2e-4c71-9a43-5b0d0f0e9d11
Content-Type: application/json
JSON
{
  "external_ref": "006Qy00000AbCdEIAV",
  "case_type": "full",
  "loan": {
    "facility_amount_gbp": 392000,
    "term_months": 12,
    "loan_type": "bridge",
    "interest_type": "retained",
    "exit_strategy": "sale",
    "exit_strategy_detail": "Sale of the property after refurbishment"
  },
  "individuals": [
    {
      "external_ref": "003Qy00000JdoeAAA",
      "is_primary": true,
      "role": "borrower",
      "title": "mr",
      "first_name": "John",
      "middle_name": null,
      "surname": "Doe",
      "date_of_birth": "1980-04-12",
      "gender": "male",
      "nationality": "GB",
      "country_of_birth": "GB",
      "residence_country": "GB",
      "email": "john.doe@example.com",
      "phone": "+447700900123",
      "address": {"line1": "10 Example Street", "line2": null, "city": "London", "postcode": "SW1A 1AA", "country": "GB"}
    }
  ],
  "companies": [
    {
      "external_ref": "001Qy00000AcmeAAA",
      "role": "borrower",
      "company_name": "Example Holdings Ltd",
      "company_number": "12345678",
      "address": {"line1": "1 Example Place", "city": "London", "postcode": "EC1A 1BB", "country": "GB"}
    }
  ],
  "ownerships": [
    {"individual_external_ref": "003Qy00000JdoeAAA", "company_external_ref": "001Qy00000AcmeAAA", "relationship": "director_shareholder", "ownership_percent": 100}
  ],
  "properties": [
    {
      "external_ref": "a0BQy000001PropAAA",
      "address": {"line1": "22 Sample Road", "city": "Manchester", "postcode": "M1 1AE", "country": "GB"},
      "uprn": "100012345678",
      "asset_type": "residential",
      "property_type": "terraced",
      "tenure": "freehold",
      "num_rooms": 3,
      "charge_type": "first",
      "purchase_price_gbp": 520000,
      "units_in_building": 1
    }
  ]
}
JSON
201 Created
ETag: "c-7"
{
  "id": "5b8d…",
  "external_ref": "006Qy00000AbCdEIAV",
  "reference": "WALT-2026-004213",
  "status": "open",
  "case_type": "full",
  "created_via": "api",
  "individuals": [{"id": "9a1e…", "external_ref": "003Qy00000JdoeAAA", "...": "..."}],
  "companies": [{"id": "…", "external_ref": "001Qy00000AcmeAAA"}],
  "properties": [{"id": "…", "external_ref": "a0BQy000001PropAAA"}],
  "links": {"self": "/v1/cases/5b8d…", "webapp": "https://app.underwritinguk.ai/cases/5b8d…"}
}
  • Store id (WALT case ID) and links.webapp on your CRM record, along with the subject ids next to their own CRM records.
  • If a case already exists with that external_ref, you get 409 case_exists, with the existing case's id in the error body. Fetch it with GET /v1/cases?external_ref=… and update it rather than creating another.
  • Validation is strict. A 422 lists every invalid field (errors[].pointer, e.g. /individuals/0/date_of_birth).

Step 4 — Upload documents (before the assessment)

HTTP
POST /v1/cases/{case_id}/documents
Idempotency-Key: 0d0b…
Content-Type: multipart/form-data; boundary=…

file=@rics_valuation.pdf ; document_type=valuation_report ; external_ref=069Qy000001DocAAA
JSON
202 Accepted
{"id": "d71…", "status": "scanning", "document_type": "valuation_report", "file_name": "rics_valuation.pdf", "size_bytes": 2483021}
  • Accepted formats: PDF, JPEG, PNG. Maximum 25 MB.
  • Every file is malware-scanned. status moves to available or rejected, usually within 30 seconds.
  • A rejected document carries a rejection_reason, for example pdf_encrypted (password-protected PDF; owner-password-only PDFs are accepted), pdf_too_complex, scan_failed or scan_incomplete. Rejected documents do not count toward the 20 per case.
  • An assessment started before a valuation report becomes available will not use it.
HTTP
POST /v1/cases/{case_id}/assessments/quote
JSON
{
  "module": "underwriting_suite",
  "mode": "incremental",
  "token_cost": 1,
  "quota_remaining": 163,
  "subjects": [
    {"subject_type": "individual", "subject_id": "9a1e…", "external_ref": "003Qy00000JdoeAAA", "action": "search",
     "checks_to_run": ["equifax", "transunion", "smartsearch", "worldcheck_one"], "checks_reused": [],
     "reason": "address changed"},
    {"subject_type": "property", "subject_id": "77c2…", "external_ref": "a0BQy000001PropAAA", "action": "reuse",
     "checks_to_run": [], "checks_reused": ["chimnie", "land_registry", "rightmove"], "reason": "unchanged"}
  ],
  "missing_data": [
    {"subject_type": "individual", "subject_id": "9a1e…", "field": "passport_number", "affects": ["identity_documents"], "severity": "optional"}
  ],
  "warnings": []
}
  • A quote is free and changes nothing.
  • For a case's first assessment every subject is search. After that, subjects[] shows exactly what will be reused and what will be searched again, and token_cost is what you will be charged.
  • missing_data with severity: "required" means that check will be skipped. Fix the data first if the check matters to you. A row may carry one_of (supplying any one of those fields unblocks the check) or derived: true (WALT derives the value from the address or UPRN; correct the address instead).
  • warnings[] lists the error a start would return now, for example quota_exhausted or case_closed.

Step 6 — Start the assessment

HTTP
POST /v1/cases/{case_id}/assessments
Idempotency-Key: 3c2e…
Content-Type: application/json

{"mode": "incremental", "checks": {"exclude": []}}
JSON
202 Accepted
Location: /v1/assessments/a4f0…
{"id": "a4f0…", "case_id": "5b8d…", "status": "queued", "token_cost": 2, "created_at": "2026-10-09T14:05:00Z"}
StatusCodeMeaning
409assessment_in_progressAn assessment is already running for this case
402quota_exhaustedNot enough tokens; detail gives the cost and what remains
403module_not_enabledThe case type is not in your plan
422case_incompleteThe minimum data is missing (at least one subject for the case type)
409assessment_cooldownThe previous assessment on this case failed less than 5 minutes ago. Retry after Retry-After seconds. The failed assessment's cooldown_until says when (set only on the case's latest failed assessment, while a cooldown applies).
409case_closedThe case is closed. Reopen it first; a closed case is never assessed or charged.
422validation_failedFor example checks.exclude named a check that is not a provider search (errors[].code not_excludable; see excludable in /v1/check-catalog)
429rate_limitedHourly start limit (§16), or your organisation's concurrent-assessment cap where starts cannot be queued. Retry after Retry-After. The concurrency 429 is only returned once no 404 or 409 applies.

mode is incremental (default) or full. full searches every check again and costs the same as a first assessment.

Charging rules:

  • A completed assessment costs its quoted tokens.
  • A failed assessment costs nothing. An assessment is failed only when no provider check returned a usable result (an ID-verification link that was only sent does not count) or no decision could be made; a single failed check leaves the assessment completed with that check failed. An assessment can also fail without running when the case changed while it was queued and your quota no longer covers the new cost: nothing is searched or charged, and no cooldown applies.
  • A cancelled assessment costs its tokens only if at least one provider check had already been called.
  • A re-assessment (incremental) costs the token formula applied to the subjects with at least one check searched again. Unchanged subjects whose checks all succeeded cost nothing. One re-searched subject costs 1 token. A re-assessment never costs more than a first assessment. If nothing changed and nothing failed, it costs 0 for subjects without screens and only the recommendation is recomputed.
  • A check is searched again when its own input data changed, when it previously failed or was skipped for missing data, or when the earlier result is older than your organisation's reuse window (default 30 days). Sanctions, PEP and adverse-media screens are never reused, and neither is borrower ID verification: they are searched again on every assessment, so a subject that has one always costs its token.

Step 7 — Wait for the result

Preferred: webhooks (§9). You receive assessment.completed, assessment.action_required or assessment.failed.

Fallback: polling if you cannot receive webhooks:

HTTP
GET /v1/assessments/{assessment_id}
JSON
{
  "id": "a4f0…",
  "case_id": "5b8d…",
  "status": "running",
  "progress": {"total_checks": 24, "finished_checks": 15},
  "pending_actions": [],
  "stale": false,
  "started_at": "2026-10-09T14:05:02Z",
  "completed_at": null
}
  • Poll no more often than every 10 seconds, and back off to 60 seconds after 5 minutes.
  • Most assessments finish within 3 minutes. Stop polling after 30 minutes and raise an alert on your side.
  • Even if you rely on webhooks, run a reconciliation job (for example hourly): any CRM record waiting for more than 1 hour should be re-checked with a GET.

Step 8 — Fetch the decision, check results and report

HTTP
GET /v1/assessments/{assessment_id}/decision           → Decision (§8)
GET /v1/assessments/{assessment_id}/checks?limit=100   → derived per-check results (§7.6)
GET /v1/assessments/{assessment_id}/report             → 200 application/pdf (streamed)

The report needs both reports:read and pii:read. Report renders are limited per server: when busy, the call answers 429 rate_limited with Retry-After. The PDF is streamed in the response body; WALT never hands out a storage link. Store the PDF file itself, under the retention rules in §14.

Step 9 — Write back to the CRM

Map the Decision onto your CRM fields (Appendix B):

  • Set the CRM application status, for example WALT: Approve with conditions.
  • Create tasks for each conditions_precedent[] item.
  • Attach the PDF.
  • Store assessment_id and decision.version.

Leave the final lending decision to the underwriter.


7. Resource reference

7.1 Case types

case_typeModule requiredSubjects assessed
credit_identityCredit & Identity ProfileIndividuals, companies
property_lending · property_buy_to_let · property_development · property_real_estateProperty ProfileProperties
fullWALT Underwriting SuiteIndividuals, companies, properties

case_type is required and cannot be changed after the first assessment.

7.2 Endpoints

MethodPathScopePurpose
POST/v1/oauth/token—Get an access token
GET/v1/accountaccount:readModules, quota, usage, limits
GET/v1/check-cataloganyChecks, their step, module and required data
POST/v1/casescases:writeCreate a case (with nested subjects)
GET/v1/casescases:readList and filter cases
GET · PATCH/v1/cases/{case_id}cases:read · cases:writeRead or update a case and its loan details
POST/v1/cases/{case_id}/closecases:writeClose a case (reason: completed, withdrawn, declined, duplicate)
POST · PATCH · DELETE/v1/cases/{case_id}/individuals[/{id}]cases:writeManage individuals
POST · PATCH · DELETE/v1/cases/{case_id}/companies[/{id}]cases:writeManage companies
POST · PATCH · DELETE/v1/cases/{case_id}/properties[/{id}]cases:writeManage properties
POST · DELETE/v1/cases/{case_id}/ownershipscases:writeLink or unlink individual ↔ company
POST/v1/cases/{case_id}/documentsdocuments:writeUpload a document
GET/v1/cases/{case_id}/documentscases:readList documents
GET/v1/documents/{document_id}/contentcases:readThe file, streamed (200; documents you uploaded, once available)
POST/v1/cases/{case_id}/assessments/quoteassessments:writeCost, planned checks, missing data
POST/v1/cases/{case_id}/assessmentsassessments:writeStart an assessment
GET/v1/cases/{case_id}/assessmentsassessments:readList a case's assessments
GET/v1/assessments/{assessment_id}assessments:readStatus, progress, pending actions
POST/v1/assessments/{assessment_id}/cancelassessments:writeCancel a queued or running assessment
GET/v1/assessments/{assessment_id}/decisionassessments:readDecision object
GET/v1/assessments/{assessment_id}/checksassessments:readDerived per-check results
GET/v1/assessments/{assessment_id}/reportreports:read + pii:readPDF report, streamed (200 application/pdf)
GET/v1/lookups/addresses?postcode=&line1=lookups:readAddress and UPRN candidates (rate-limited)
GET · POST · PATCH · DELETE/v1/webhook-endpoints[/{id}]webhooks:manageManage webhook endpoints
POST/v1/webhook-endpoints/{id}/testwebhooks:manageSend a ping event
GET/v1/events?since=assessments:readEvent log (last 7 days), for reconciliation

Subject changes are refused while an assessment is running (409 case_locked). A subject external_ref already used by another subject of the same kind in the case → 409 conflict (the body names the existing <kind>_id). On a credit case, adding or deleting an individual so that no individual is primary → 422 validation_failed with errors[].code primary_required: to delete the primary, move the flag first (PATCH is_primary: false on it, then true on the other). A case reopened in the WALT webapp reads status: open with closed_at and close_reason null.

7.3 Case fields

FieldTypeReq.Notes
external_refstring ≤128recommendedYour record ID; unique per case
referencestring ≤64noYour human-readable reference; WALT generates one if omitted
case_typeenumyes§7.1
loan.facility_amount_gbpintegeryes for full/property_*Gross facility requested
loan.term_monthsinteger 1–60yes for full
loan.loan_typeenumyes for fullbridge, development, refurbishment
loan.interest_typeenumnoretained, serviced, rolled_up
loan.exit_strategyenumyes for fullsale, refinance, other
loan.exit_strategy_detailstring ≤1000if otherTreated as untrusted text (§14)
loan.ltv_basisenumnoday_180 (default unless your organisation changed it) or market_value. Decides which LTV drives the recommendation. Both are always returned (§8). Changing it re-decides the case at no cost.
call_transcriptobjectnov1.1: { "text": "…", "recorded_at": "…", "participants": ["broker","borrower"] }, pushed with POST /v1/cases/{case_id}/call-transcripts. 422 validation_failed with errors[].code step_not_enabled when the case's assessment would not use it (borrower call step not in your plan or case type). Stored encrypted and never returned; it is pseudonymised when an assessment uses it.
read-onlyid, status (open, closed), created_via (api, webapp), created_at, updated_at, latest_assessment (summary), links

7.4 Individual fields

FieldTypeReq.Used by
external_refstringno
is_primaryboolone per caseExactly one primary individual on credit cases
roleenumyesborrower, guarantor, director, shareholder
titleenumyesmr, mrs, ms, miss, dr, prof, mx, other
first_name, middle_name, surnamestring ≤100first/surname yesAll checks
date_of_birthdateyesCredit, KYC, sanctions. Must be in the past; age 18+
genderenumnomale, female, other, undisclosed
nationality · country_of_birth · residence_countryISO alpha-2nationality recommendedSanctions/PEP
emailemailfor IDVBorrower ID-verification invitation
phoneE.164for IDVSMS ID-verification invitation
address.line1/line2/city/postcode/countrystringsline1, city, postcode yesCredit, KYC
previous_addresses[]address + from/toif < 3 years at current addressCredit match quality
passport_numberstringnoWrite-only (returned only with pii:read)
source_of_fundsobjectno{ "mode": "invite" | "declared", "origin": enum, "deposit_amount_gbp": int }. The source-of-funds check is not available yet: the field is accepted and stored, but no check uses it
bank_accountobjectno{ "sort_code": "112233", "account_number": "12345678", "bank_name": "…" }. Write-only

Consent and notice. By submitting an individual you confirm that the person has received your privacy notice. That notice must cover credit reference and fraud-prevention searches, ID verification (including biometric face matching) and AI-assisted assessment, and you must have a lawful basis and permissible purpose for every search (§14).

7.5 Company and property fields

Company: external_ref, role (borrower, guarantor, corporate_shareholder), company_name (required), company_number (required for UK entities; Companies House format ^(\d{8}|[A-Z]{2}\d{6}|R\d{7}|[A-Z]{2}\d{5}R)$; shorter all-digit numbers are zero-padded to 8), jurisdiction (ISO alpha-2, default GB), address.

Property:

FieldTypeReq.Notes
address.line1/city/postcodestringsyes
uprndigits ≤12strongly recommendedAvoids address-matching errors. Use /v1/lookups/addresses if you don't hold it
asset_typeenumyesresidential, commercial, mixed_use, multi_unit_block, hmo, new_build_residential
property_typeenumyes if residentialflat, terraced, semi_detached, detached, bungalow, block_of_flats
tenureenumyesfreehold, leasehold, share_of_freehold
tenure_unexpired_yearsintif leasehold
num_roomsintrecommendedBedrooms; used by AVMs
floor_area_sqftint 1–1,000,000recommended for new_build_residentialGross internal area (GIA) in sq ft. A new build is valued against local £/sq ft comparables
charge_typeenumyesfirst, second
first_charge_amount_gbp, first_charge_lenderint, stringif second charge
purchase_price_gbp, asking_price_gbpintrecommended
estimated_value_gbpintnoYour own/broker estimate. Shown alongside WALT's figures, never used as the valuation
units_in_buildingint 0–1000if multi-unitEach unit costs one token
units[]arrayif multi-unit{ "label", "beds", "area_sqm", "epc", "rent_pcm_gbp", "value_gbp" }
BTL fieldsintsproperty_buy_to_lettarget_rent_pcm_gbp (required), service_charge_ground_rent_gbp, maintenance_costs_gbp
Development fieldsproperty_developmentdevelopment_type (enum), proposed_units (required), planning_status (none, pending, approved, refused), estimated_build_costs_gbp
year_built, condition (excellent, good, fair, poor)no

Document: document_type enum: valuation_report (RICS valuation; used for LTV), title_register, lease, bank_statement, proof_of_address, other. Plus file and external_ref.

7.6 Check result (derived)

JSON
{
  "check": "equifax",
  "step": "credit_assessment",
  "subject": {"type": "individual", "id": "9a1e…", "external_ref": "003Qy00000JdoeAAA"},
  "status": "completed",
  "outcome": "refer",
  "risk_level": "medium",
  "risk_score": 55,
  "confidence_score": 80,
  "reason_codes": ["CREDIT_ADVERSE_CCJ_SATISFIED", "CREDIT_SEARCHES_HIGH_6M"],
  "metrics": {"ccj_count_6y": 1, "defaults_count_6y": 0},
  "summary": "One satisfied CCJ (2021). Elevated recent search activity.",
  "reused": false,
  "source_assessment_id": null,
  "searched_at": "2026-10-09T14:06:11Z",
  "completed_at": "2026-10-09T14:06:11Z"
}
  • status is one of pending, running, completed, skipped, awaiting_action, failed, cancelled.
  • outcome is one of pass, refer, fail, not_run, error.
  • Scores run 0–100, higher means riskier. risk_level is low below 40, medium from 40 to 69, high from 70.
  • reused: true means the result was carried forward from source_assessment_id, without a new search. searched_at is when the data was actually obtained.
  • metrics contains only the derived fields WALT is licensed to share. The set per check is listed in /v1/check-catalog.
  • Raw bureau, sanctions or valuation-provider data is never returned through the API. Your underwriters see the full evidence in the WALT webapp and the PDF.

8. The Decision object

JSON
{
  "assessment_id": "a4f0…",
  "case_id": "5b8d…",
  "version": 1,
  "is_final": true,
  "recommendation": "approve_with_conditions",
  "overall_risk_score": 38,
  "overall_risk_level": "low",
  "confidence_score": 81,
  "decision_source": "walt_synthesis",
  "human_review_required": true,
  "executive_summary": "Experienced borrower; clean KYC/AML; credit profile acceptable with one satisfied CCJ; 180-day LTV 85% (market-value LTV 68%) on a Walt estimated value of £575,000.",
  "steps": [
    {"step": "kyc_and_aml", "status": "completed", "outcome": "pass", "risk_level": "low", "summary": "Identity verified; no sanctions or PEP matches."},
    {"step": "credit_assessment", "status": "completed", "outcome": "refer", "risk_level": "medium", "summary": "…"},
    {"step": "property_valuation", "status": "completed", "outcome": "pass", "risk_level": "low", "summary": "…",
     "metrics": {"walt_estimated_value_gbp": 575000, "valuation_basis": "avm_consensus",
                 "ltv_basis": "day_180", "ltv_day_180_percent": 85.2, "ltv_market_value_percent": 68.2}}
  ],
  "key_risk_factors": ["Satisfied CCJ within 6 years"],
  "mitigating_factors": ["LTV below 70%", "Clear exit via sale"],
  "conditions_precedent": ["Satisfactory RICS valuation confirming value ≥ £560,000"],
  "conditions_subsequent": ["Evidence of refurbishment completion within 6 months"],
  "pending_actions": [],
  "generated_at": "2026-10-09T14:06:40Z"
}
FieldMeaning
recommendationapprove, approve_with_conditions, refer or decline. Advisory.
is_finalfalse while pending_actions is not empty; the decision may still change.
versionGoes up each time the decision for this assessment changes, for example after the borrower completes ID verification. Store it, and ignore a webhook whose version is lower than or equal to the one you hold.
decision_sourcewalt_synthesis (normal) or rules_fallback (the AI synthesis was unavailable; the decision is from rule-based scoring and is usually refer).
human_review_requiredAlways true in v1.
steps[]One entry per step in the case type. Steps that do not apply are omitted.
overall_risk_levelFollows the same bands as §7.6.
Guard-railsSome findings cap the recommendation at refer whatever the synthesis says: a failed or incomplete sanctions/PEP screen (sanctions_pep_fail, sanctions_pep_incomplete), a failed or incomplete ID verification (idv_fail, idv_incomplete), a borrower step that expired unanswered (borrower_action_expired, §10) and an LTV above policy (ltv_above_policy_limit). Incomplete means it failed, timed out, was cancelled or paused, or is still awaiting the borrower. The codes are recorded with the decision in WALT; /v1 returns the capped recommendation.
LTV metricsThe property_valuation step always carries both ltv_day_180_percent (facility ÷ 80% of the WALT estimated value) and ltv_market_value_percent. ltv_basis names the one used for the recommendation.

9. Webhooks

9.1 Registering an endpoint

You give WALT an HTTPS URL during onboarding, or register it yourself with webhooks:manage. WALT returns a signing secret (whsec_…) once. Store it the same way as the client secret.

  • The endpoint must use TLS 1.2+ with a publicly trusted certificate.
  • It must respond 2xx within 10 seconds. Acknowledge first, process asynchronously.
  • WALT does not follow redirects.
  • Private, loopback and link-local addresses are refused.

9.2 Events

EventWhen
assessment.completedAn assessment reached completed (final decision)
assessment.action_requiredThe assessment produced a provisional decision; a borrower or third party must act (§10)
assessment.updatedThe decision changed (new version), e.g. ID verification completed or expired
assessment.failedThe assessment failed (no tokens charged; the case can be re-assessed after 5 minutes) or was cancelled (tokens charged only if a provider check had already been called). data.status tells you which.
report.readyThe PDF report for the latest decision version is ready (fetching it needs reports:read and pii:read)
case.erasedThe case was erased in WALT (retention or data-subject request). Delete your copies of WALT outputs according to your own policy
quota.threshold_reached80% and 100% of a module's monthly quota used
pingTest event

9.3 Payload (thin by design — no personal data)

HTTP
POST https://crm-gateway.yourlender.co.uk/walt/webhooks
Content-Type: application/json
WALT-Event-Id: evt_01JA8Y5W8R6M5T0N3P2Q1R0S9T
WALT-Event-Type: assessment.completed
WALT-Signature: t=1760018800,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
JSON
{
  "id": "evt_01JA8Y5W8R6M5T0N3P2Q1R0S9T",
  "type": "assessment.completed",
  "created_at": "2026-10-09T14:06:41Z",
  "environment": "production",
  "data": {
    "case_id": "5b8d…",
    "case_external_ref": "006Qy00000AbCdEIAV",
    "assessment_id": "a4f0…",
    "status": "completed",
    "recommendation": "approve_with_conditions",
    "decision_version": 1,
    "is_final": true
  }
}

After you receive an event, call the API (§6 step 8) for the details. Treat the webhook as a notification, not as the source of truth.

9.4 Verifying the signature (mandatory)

v1 = hex(HMAC_SHA256(signing_secret, t + "." + raw_request_body))

  1. Parse t and every v1 value. There can be two during a secret rotation; accept if either matches.
  2. Reject the request if |now − t| > 300 seconds (replay protection).
  3. Compute the expected signature over the raw body bytes, before any JSON parsing.
  4. Compare in constant time.
  5. De-duplicate on WALT-Event-Id: delivery is at least once.

Python

Python
import hmac, hashlib, time

def verify(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    sigs = [v for k, v in (p.split("=", 1) for p in header.split(",")) if k == "v1"]
    t = int(parts.get("t", 0))
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, s) for s in sigs)

Node.js

JavaScript
const crypto = require("crypto");
function verify(rawBody, header, secret, tolerance = 300) {
  const items = header.split(",").map(p => p.split("="));
  const t = Number((items.find(([k]) => k === "t") || [])[1]);
  if (!t || Math.abs(Date.now() / 1000 - t) > tolerance) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest("hex");
  return items.filter(([k]) => k === "v1").some(([, s]) =>
    s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}

Apex (Salesforce)

Java
Blob mac = Crypto.generateMac('HmacSHA256', Blob.valueOf(t + '.' + rawBody), Blob.valueOf(secret));
String expected = EncodingUtil.convertToHex(mac);
// compare expected with each v1 value in constant time (loop over all chars; do not short-circuit)

9.5 Retries and ordering

  • Non-2xx responses and timeouts are retried with exponential backoff: about 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, then every 24 h until 3 days have passed.
  • After 3 days of continuous failure the endpoint is disabled automatically. Your technical contact is emailed, and you can re-enable it in the admin console or through the API.
  • Events can arrive out of order. Use decision_version and the GET endpoints to decide what is current.
  • Missed something? GET /v1/events?since=2026-10-09T00:00:00Z lists the events of the last 7 days, in commit order. An event appears there about 5 seconds after it is emitted (settle window), so a cursor never skips one. An invalid cursor → 400 bad_request.

9.6 Signing-secret rotation

When you rotate the secret, WALT signs with both the old and new secrets for 24 hours, so a header can carry two v1 values. Deploy the new secret within that window. Rotating again inside the window keeps the original old secret signing until its original expiry; the intermediate secret stops signing at once.


10. Borrower actions and other pending steps

Some checks need a person to act. WALT starts these automatically and tracks them for you.

pending_actions[].typeWhat happensCompletion
borrower_identity_verificationWALT's ID-verification partner sends the borrower an SMS (or email) link for document + selfie verificationDetected automatically within about 5 minutes of the borrower finishing. The link expires after 14 days.
borrower_source_of_fundsReserved: the borrower source-of-funds step is not available yet, so this type is not returned today. Tolerate it, as any unknown value—
valuation_report_requiredLTV could only be estimated from AVMs; the policy requires a RICS valuationUpload a valuation_report and start a new assessment

Example:

JSON
"pending_actions": [
  {"type": "borrower_identity_verification", "subject": {"type": "individual", "id": "9a1e…", "external_ref": "003Qy00000JdoeAAA"},
   "channel": "sms", "sent_at": "2026-10-09T14:05:30Z", "expires_at": "2026-10-23T14:05:30Z", "status": "waiting"}
]
  • A borrower step still unanswered after 14 days expires: its check reads cancelled, the decision becomes final (assessment.updated) and is at best refer (guard-rail borrower_action_expired, §8).
  • Do not start a new assessment just to "check" whether the borrower has finished. That re-runs paid searches and sends the borrower another link. Wait for assessment.updated.
  • Make sure email and phone are correct before you start the assessment: the borrower receives the invitation from WALT's ID-verification partner. Tell your borrowers to expect it.

11. Sandbox and test personas

The sandbox runs the real WALT API and workflow in WALT's dev environment, but for sandbox organisations every provider is simulated. Outcomes are chosen by test personas, selected through the individual's surname or the property postcode. They work only in the sandbox.

Persona triggerBehaviour
surname ApproveClean KYC/AML, good credit → approve
surname ConditionsSatisfied CCJ → approve_with_conditions
surname ReferThin credit file, address mismatch → refer
surname DeclineRecent default + IVA → decline
surname SanctionsSanctions match → KYC fail → decline
surname PepPEP match → refer
surname Idvpendingawaiting_action for 2 minutes, then ID verification passes → assessment.updated
surname IdvfailID verification fails after 2 minutes → assessment.updated with refer
surname OutageCredit provider unavailable → check error, decision_source: rules_fallback
surname SlowAssessment takes 6 minutes (tests your polling and timeouts)
property postcode ZZ1 1HLHigh LTV (85%) → valuation refer
property postcode ZZ1 1LOLow LTV (45%)
property postcode ZZ1 1MU + units_in_buildingMulti-unit block valuation
company number 00000000Dissolved company → fail
anything elseRandomised but internally consistent results

Notes:

  • Sandbox webhooks are sent exactly as in production. POST /v1/webhook-endpoints/{id}/test sends a ping.
  • Sandbox data is deleted after 30 days.
  • Sandbox quota defaults to 500 tokens/month; it costs nothing and exists to exercise the quota logic.

12. Connector methodology — how to build a CRM connector

This section is CRM-agnostic. It describes the architecture, data model and behaviours that a robust connector must have, whatever platform you build it on.

12.1 Choose an architecture

PatternWhenStrengthsWatch out
A. Integration service (recommended) — a small service you host (Azure Function, AWS Lambda, container) sits between your CRM and WALTYou have an IT/dev team; several CRMs or systems; strict securitySecrets outside the CRM; easy webhook endpoint; retries, queueing, logging in one place; CRM-agnosticOne more component to run
B. CRM-native code — Apex, Dynamics plug-ins/Azure Functions, HubSpot custom-coded actions/serverless functionsSingle CRM, platform-savvy teamFewer moving partsCRM governor/time limits; the webhook endpoint must be exposed from the CRM platform
C. iPaaS / low-code — Power Automate, MuleSoft, Boomi, Workato, Make, Zapier, n8nSmall team, standard flowFast to build; import openapi.jsonMake sure the platform supports HMAC verification and secret storage; check per-run cost and limits

Never call WALT from the user's browser (Lightning Web Components, client-side scripts, HubSpot UI extensions calling WALT directly). Secrets would be exposed. Browser components should read from CRM records only.

Diagram
 ┌───────────── Your environment ─────────────┐            ┌────── WALT ──────┐
 │  CRM ──(trigger: "Send to WALT")──► Connector ──OAuth2 + HTTPS──► /v1 API  │
 │   ▲                                   │  ▲                       │         │
 │   └──── write-back (status, PDF) ─────┘  └──── signed webhooks ──┘         │
 └─────────────────────────────────────────────┘            └──────────────────┘

12.2 Model WALT in your CRM

Add these fields to the record that represents the application (Opportunity, Deal, Application, Case):

CRM fieldSource
WALT Case ID (text, unique, external ID)case.id
WALT Link (URL)case.links.webapp
WALT Status (picklist)Connector state (§12.3)
WALT Latest Assessment IDassessment.id
WALT Recommendation (picklist)decision.recommendation
WALT Decision Final (bool), WALT Decision Version (number)decision.is_final, decision.version
WALT Risk Score, WALT Confidence (number)decision.overall_risk_score, decision.confidence_score
WALT Pending Actions (text)decision.pending_actions[].type
WALT Last Error, WALT Last Request IDError code / X-Request-ID
WALT Assessed At (datetime)decision.generated_at

Also add WALT Subject ID to the person/contact and company/account records, and WALT Property ID to the property/collateral record, to map subjects one-to-one.

Child records (recommended):

  • WALT Condition — one record per conditions_precedent / conditions_subsequent item, with an owner and a status.
  • WALT Step Result — step, outcome, risk level, summary.

12.3 The connector state machine

Diagram
Not sent ──(user clicks "Send to WALT" / stage change)──► Submitting
Submitting ──201──► Submitted ──quote ok──► Assessing ──webhook/poll──┬─► Awaiting borrower ──updated──► Decision received
          └─4xx (validation)──► Needs data (show errors to user)       ├─► Decision received
          └─5xx/timeout──► Retry (same Idempotency-Key)                 └─► Failed (alert)
Decision received ──CRM data changed──► Stale (offer "Re-assess")

The rules that make a connector robust:

  1. Explicit trigger. Send on a deliberate action ("Send to WALT" button, or entering a defined stage), not on every record save. Every assessment costs tokens and runs real credit searches.
  2. Pre-flight validation in the CRM. Required fields per case type (§7, Appendix B), so users fix data before submitting. The API is still authoritative: show its 422 errors[] to the user against the right fields, using pointer.
  3. One idempotency key per logical operation, stored with the request (e.g. on the record), reused on every retry.
  4. Upsert by external_ref.
    • On first send, create the case.
    • On a later send: if no assessment has started, PATCH the changed fields and add or delete subjects. If an assessment has run, the case becomes stale; let the user decide whether to re-assess.
    • Re-assess in the default incremental mode. Only what changed or failed is searched again and charged. Never delete and recreate a case to "start fresh": that throws away the reusable results and costs a full assessment.
  5. Documents before assessment. Upload, wait until each is available (or the document.available state shows up in GET …/documents), then quote, then assess.
  6. Quote, then confirm. Before starting, show the user the token cost, which subjects will be searched again and which reused, and any missing-data warnings.
  7. Asynchronous by default.
    • Return control to the user immediately, with the status Assessing.
    • Update the record from the webhook.
    • Keep polling only as a fallback.
    • Run an hourly reconciliation job: list records in Assessing or Awaiting borrower older than 1 h and GET their assessment.
  8. Write-back is idempotent. Apply a decision only if decision.version is greater than the stored one.
  9. Fetch the PDF once per decision version. Store it as a file on the record, under your retention policy (§14).
  10. Surface pending actions. For borrower_identity_verification, create a task for the case owner ("Borrower has been sent an ID-verification link"). Do not resend; WALT handles reminders.

12.4 Error handling and retries

ResponseWhat the connector does
400 / 422Do not retry. Show errors[] to the user; set Needs data.
401 invalid_tokenGet a new token once and retry once. If it fails again, alert (the credential may be revoked).
402 quota_exhaustedDo not retry. Notify the WALT account owner at your firm.
403Do not retry. Configuration/entitlement problem: alert the integration owner.
404The record may have been erased or the ID is wrong. Re-sync with GET /v1/cases?external_ref=.
409 case_existsSwitch to update mode using the returned case ID.
409 assessment_in_progressWait for the webhook; do not retry the start.
409 assessment_cooldownA previous assessment failed. Wait Retry-After seconds, then offer the user a retry. Never auto-loop.
409 idempotency_conflictBug: the same key was used for a different request (body, acting user or scopes). Generate a new key only if the operation is genuinely new.
409 idempotency_result_unavailableThe operation was processed but its response is lost. Do not retry with the same key; re-read the state with a GET.
409 case_closedThe case is closed in WALT. Reopen it there before assessing; do not auto-retry.
412 precondition_failedRe-read (GET), merge, retry the PATCH with the new ETag.
429 rate_limitedWait Retry-After seconds, then retry.
5xx, timeouts, network errorsRetry with exponential backoff and jitter (e.g. 2 s, 4 s, 8 s … max 5 min, 8 attempts) with the same Idempotency-Key. Then dead-letter and alert.

Always log X-Request-ID, the HTTP status and the error code, and never log tokens, secrets or personal data.

12.5 Security checklist for your connector

  • Secrets live only in a vault or credential store; separate credentials per environment.
  • TLS 1.2+ certificate validation is on (never disable it); outbound calls are pinned to *.underwritinguk.ai.
  • Webhook signatures are verified and replays rejected (timestamp tolerance, event-ID dedupe).
  • Minimum scopes; pii:read only if a documented process needs it.
  • Optional: static egress IPs, if you want WALT to accept your credential only from those addresses.
  • Logs contain no personal data, tokens or secrets.
  • Personal data is sent only for applicants who have received your privacy notice (§14).
  • CRM permissions: only authorised roles can trigger "Send to WALT" or see WALT results; field-level security is set on the WALT fields.
  • Free-text fields (exit_strategy_detail, notes) contain only application facts, never instructions or unrelated personal data.
  • Sandbox refreshes of your CRM do not carry production WALT credentials.

12.6 Testing your connector

  1. Run the sandbox persona matrix (§11) end to end, including Idvpending, Outage and Slow.
  2. Run negative tests:
    • invalid postcode or DOB → 422;
    • a duplicate send → idempotent, with the same case.id;
    • an expired token → refresh;
    • a forced 429 → backoff;
    • webhook endpoint down for 1 h → retries, then catch-up reconciliation;
    • out-of-order events.
  3. Run security tests:
    • a webhook with a tampered body or a stale timestamp → rejected;
    • search the logs for personal data.
  4. Go through the go-live checklist (Appendix C) before you switch to production credentials.

13. Platform notes

These are patterns, not product endorsements. Always follow your platform's current documentation.

Salesforce

  • Store credentials in an External Credential (OAuth 2.0, Client Credentials flow) plus a Named Credential pointing at the WALT base URL. Callouts then carry no secrets.
  • Trigger from a Flow or button and run the callout in Queueable Apex (callouts are not allowed directly from triggers).
  • Expose the webhook endpoint as an Apex REST resource on a Salesforce Site with a dedicated, minimal-permission guest or integration user. Verify the HMAC before any DML. Alternatively, receive webhooks in middleware and publish a Platform Event.
  • Use WALT Case ID as an External ID field for upserts. Mind callout limits and timeouts (stay asynchronous).

Microsoft Dynamics 365 / Power Platform

  • Use an Azure Function (pattern A), with secrets in Azure Key Vault, triggered by a Dataverse webhook or a Power Automate flow.
  • Alternatively, build a custom connector from openapi.json (OAuth 2.0 generic, client credentials) and receive WALT webhooks with an HTTP-triggered flow plus HMAC verification in an Azure Function.

HubSpot

  • Use a private app for HubSpot API access, and keep the WALT client secret in HubSpot secrets (for custom-coded workflow actions) or in your middleware.
  • Webhooks should land in middleware or a serverless function that verifies the signature, then update the deal through the HubSpot API.

iPaaS (MuleSoft, Boomi, Workato, Make, Zapier, n8n)

  • Import openapi.json and configure OAuth 2.0 client credentials.
  • Use the platform's HMAC function to verify webhooks; if it has none, put a small verification function in front.
  • Persist idempotency keys between retries.

Bespoke CRM / loan origination system

  • Pattern A in your own stack. Generate a client from openapi.json (openapi-generator, NSwag, oapi-codegen).

14. Your data-handling obligations

These are summarised here. The binding terms are in the WALT Partner API Terms and your Data Processing Agreement.

  1. Lawful basis, permissible purpose and notice.
    • You submit personal data only for genuine loan applications.
    • The applicant has received your privacy notice. It names credit reference agencies and fraud-prevention searches (CRAIN), ID verification (including biometric face matching), and AI-assisted assessment with human review.
    • Each assessment is a new search that may be recorded on the applicant's credit file.
  2. No automated decisions solely on WALT output. A qualified person reviews the evidence and makes the decision (UK GDPR Art. 22). Record who decided.
  3. Third-party data.
    • The results you receive are derived from licensed sources (credit bureaus, sanctions and PEP databases, valuation models, HM Land Registry). Use them only to assess and manage that loan application.
    • Do not resell, share with third parties (including brokers), or reuse them for marketing or model training.
    • Keep them only as long as your retention policy for loan files allows.
  4. Data minimisation. Send only the fields WALT needs. Do not put unrelated personal data in free-text fields.
  5. Security. Follow §12.5. Report suspected credential compromise or misuse to WALT within 24 hours.
  6. Data-subject requests.
    • Access requests: you handle them. WALT provides an export of WALT-held data for a case on request.
    • Erasure: request it through support. WALT sends case.erased; delete WALT outputs from your systems according to your policy.
  7. Environments. Never send real personal data to the sandbox.

15. Error catalogue

All errors are application/problem+json:

JSON
{
  "type": "https://www.underwritinguk.ai/developers/errors/validation_failed",
  "title": "Request validation failed",
  "status": 422,
  "code": "validation_failed",
  "detail": "2 fields are invalid.",
  "request_id": "7b0b1e5c-…",
  "errors": [
    {"pointer": "/individuals/0/date_of_birth", "code": "must_be_past", "message": "Date of birth must be in the past."},
    {"pointer": "/properties/0/address/postcode", "code": "invalid_uk_postcode", "message": "Not a valid UK postcode."}
  ]
}
HTTPcodeRetry?
400bad_request, invalid_json, idempotency_key_requiredNo
401invalid_client, invalid_token, token_expiredRefresh the token once
403insufficient_scope, module_not_enabled, ip_not_allowed, organisation_suspended, acting_user_not_allowedNo
404not_foundNo
409case_exists, assessment_in_progress, case_locked, idempotency_conflict, idempotency_result_unavailable, case_type_locked, case_closed, conflictNo (handle)
409idempotency_in_progressYes, shortly, with the same key
409assessment_cooldownAfter Retry-After (≤ 5 min)
412precondition_failedRe-read, then retry
413payload_too_largeNo
415unsupported_media_typeNo
402quota_exhaustedNo
422validation_failed, case_incomplete, acting_user_not_foundNo
429rate_limitedAfter Retry-After
500internal_errorYes, with backoff
503service_unavailable, maintenanceYes, after Retry-After
  • 403 organisation_suspended is also returned when your organisation is closed.
  • 409 idempotency_result_unavailable: the request was processed but its response cannot be returned (§5).
  • 422 validation_failed lists field-level codes in errors[].code, for example not_excludable (§6 Step 6), primary_required (§7.2) and step_not_enabled (§7.3).
  • The OAuth token endpoint answers OAuth errors (invalid_client, invalid_scope, …), not problem+json (§4.1).

16. Limits, support, SLA and change policy

Default limits (per credential unless stated; higher limits on request):

LimitValue
API requests600 / minute
Token requests30 / minute (authenticated requests; unauthenticated attempts are limited per client ID and IP)
Assessment starts60 / hour per organisation (accepted starts only; refused starts and 429s are not counted)
Concurrent assessments5 per organisation (extra starts are queued, or refused with 429 rate_limited and Retry-After where queueing is not enabled)
Address lookups30 / minute
Request body1 MB for every request body, whatever its content type or transfer encoding (chunked bodies are counted). Document uploads have their own cap (next row) and must send Content-Length
Document upload25 MB per file, 20 files per case
Subjects per case20 individuals, 20 companies, 20 properties
Polling≥ 10 s interval

Limits use a sliding window, so after a burst Retry-After can be longer than the seconds left in the current minute or hour.

Support:

  • Contact: api-support@underwritinguk.ai (to be created). Business hours 09:00–18:00 UK time, Monday to Friday.
  • Always include the X-Request-ID.
  • Production incidents (P1) are acknowledged within 1 business hour.
  • Status page: status.underwritinguk.ai (to be created).

Service level (production /v1):

  • 99.5% monthly availability target, excluding announced maintenance (at least 5 business days' notice, outside 08:00–19:00 UK).
  • Assessments are completed or failed within 30 minutes; most finish in under 3.

Change policy:

  • Additive changes may ship at any time and are listed in the changelog (www.underwritinguk.ai/developers/changelog).
  • Breaking changes only in a new major version, with 12 months' notice.
  • Security fixes may be applied immediately.

Appendix A — Enumerations

NameValues
case_typecredit_identity, property_lending, property_buy_to_let, property_development, property_real_estate, full
case.statusopen, closed
assessment.statusqueued, running, awaiting_action, completed, failed, cancelled
assessment.modeincremental, full
ltv_basisday_180, market_value
recommendationapprove, approve_with_conditions, refer, decline
check.statuspending, running, completed, skipped, awaiting_action, failed, cancelled
check.outcomepass, refer, fail, not_run, error
risk_levellow, medium, high
stepborrower_profile, kyc_and_aml, credit_assessment, borrower_call, property_valuation, property_due_diligence
loan_typebridge, development, refurbishment
interest_typeretained, serviced, rolled_up
exit_strategysale, refinance, other
individual.roleborrower, guarantor, director, shareholder
company.roleborrower, guarantor, corporate_shareholder
asset_typeresidential, commercial, mixed_use, multi_unit_block, hmo, new_build_residential
property_typeflat, terraced, semi_detached, detached, bungalow, block_of_flats
tenurefreehold, leasehold, share_of_freehold
charge_typefirst, second
planning_statusnone, pending, approved, refused
document_typevaluation_report, title_register, lease, bank_statement, proof_of_address, other
pending_action.typeborrower_identity_verification, valuation_report_required (borrower_source_of_funds is reserved, not available yet)

Appendix B — Field-mapping template

Fill this in with your CRM object and field names to plan the mapping. WALT support can review it on request.

WALT fieldRequired forYour CRM object.fieldTransformNotes
external_refallOpportunity.Id—
case_typeallOpportunity.Product__cmap → enum
loan.facility_amount_gbpfull, propertyOpportunity.Amountround to £
loan.term_monthsfullOpportunity.Term__c—
loan.loan_typefull…map → enum
loan.exit_strategyfull…map → enum
individuals[].first_name/surnamecredit, fullContact.FirstName/LastName—
individuals[].date_of_birthcredit, fullContact.BirthdateISO date
individuals[].address.*credit, fullContact.MailingAddresssplit
individuals[].email/phoneIDVContact.Email / MobilePhoneE.164
individuals[].nationalitysanctions…ISO alpha-2
companies[].company_numbercredit, fullAccount.CompanyNumber__cpad to 8
properties[].address.*property, fullCollateral__c.*split
properties[].uprnrecommendedCollateral__c.UPRN__cdigits
properties[].asset_type/property_type/tenureproperty, full…map → enum
write-back: decision.recommendationOpportunity.WALT_Recommendation__c—
write-back: report PDFFiles on Opportunity—retention policy

Appendix C — Go-live checklist

This is a self-check for your team before you switch to production. WALT does not review it. Your contract makes you responsible for how your connector uses your credits.

  • Credentials stored in a vault or credential store; none in code or config files.
  • Tokens cached and refreshed before expiry; no per-call token requests.
  • Idempotency-Key on every create; retries reuse the same key.
  • external_ref set on the case and subjects; duplicate send → no duplicate case.
  • Pre-flight validation in the CRM; 422 errors displayed to users per field.
  • Documents uploaded and available before quote and assessment.
  • Quote shown to the user before the assessment starts.
  • Webhook endpoint over HTTPS; signature verified; replay window enforced; events de-duplicated; 2xx within 10 s.
  • Reconciliation job for records stuck in Assessing or Awaiting borrower.
  • decision.version respected (no overwrite by an older event).
  • awaiting_action handled without starting new assessments.
  • 402/403/409/412/429/5xx handled as in §12.4.
  • PDF fetched from the redirect and stored; links never stored.
  • No personal data, tokens or secrets in logs.
  • CRM permissions restrict who can trigger assessments and see results.
  • Applicant privacy notice updated (§14).
  • (Optional) Production egress IPs supplied, if you want an IP allow-list.
  • Named technical contact and escalation path supplied.
  • All sandbox personas in §11 tested; results attached.