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
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)| Concept | Meaning |
|---|---|
| Case | One loan application. Holds the loan details and the subjects. Your CRM's opportunity or application record maps to a Case through external_ref. |
| Subject | An individual, company or property the checks run against. |
| Case type | Which 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). |
| Assessment | One 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. |
| Check | A 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. |
| Decision | The typed result of an assessment (§8). |
| Token | Usage 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
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 emitsassessment.action_required, thenassessment.updatedwhen 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
| Environment | Base URL | Data | Billing |
|---|---|---|---|
| Sandbox | https://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 |
| Production | https://api.underwritinguk.ai/v1 (by arrangement; the base URL is confirmed when production access is issued) | Live checks against credit bureaus and KYC providers | Tokens 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./v1stays supported for at least 12 months after/v2is released. - Deprecations are announced by email to your technical contact and through the
Deprecation/Sunsetresponse 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 inX-Azure-FDID); anX-Forwarded-Forvalue you send is never trusted.
4.1 Get an access token
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:read200 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.scopeis optional. If you leave it out, the token gets every scope granted to the credential. Asking for a scope the credential does not hold returns400with"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
| Scope | Allows |
|---|---|
cases:read | Read cases, subjects and documents (personal data minimised, see pii:read) |
cases:write | Create and update cases, subjects, ownerships; close cases |
documents:write | Upload documents |
assessments:write | Request quotes, start and cancel assessments |
assessments:read | Read assessments, decisions and check results |
reports:read | Download PDF reports (together with pii:read) |
account:read | Read entitlements, quota and usage |
lookups:read | Address and UPRN lookup |
webhooks:manage | Manage your webhook endpoints |
pii:read | Read 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_as | Attribute 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:
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
| Topic | Convention |
|---|---|
| Format | JSON (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. |
| IDs | WALT IDs are UUIDs. Treat them as opaque strings. |
| Your references | Every 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 times | Dates are YYYY-MM-DD. Timestamps are ISO 8601 in UTC with Z (2026-10-09T14:03:22Z). |
| Money | Integer 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). |
| Countries | ISO 3166-1 alpha-2 (GB, IE, AE). Nationality uses the same codes. |
| Phone | E.164 (+447700900123). |
| Nulls and partial updates | PATCH changes only the fields you send. Send null to clear an optional field. |
| Idempotency | Required 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. |
| Concurrency | GET 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. |
| Pagination | Cursor-based: ?limit=50&cursor=… (max 100). Responses contain data[] and next_cursor (null on the last page). An invalid cursor → 400 bad_request. |
| Filtering | GET /v1/cases?external_ref=&status=&updated_since=&created_via=. |
| Request IDs | Send 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. |
| Errors | application/problem+json (RFC 9457) with a stable code (§15). |
| Rate limits | RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers on every response. A 429 includes Retry-After (seconds). |
| Compatibility | Ignore 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:
- Authenticate.
- Check entitlements (once a day is enough).
- Create the case with all its subjects in one call.
- Upload documents.
- Get a quote.
- Start the assessment.
- Wait for the webhook, or poll.
- Fetch the decision and report.
- Write the results back to the CRM.
Step 1 — Token
See §4.1.
Step 2 — Entitlements (cache for 24 h)
GET /v1/account{
"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)
POST /v1/cases
Idempotency-Key: 6f1c9a52-6a2e-4c71-9a43-5b0d0f0e9d11
Content-Type: application/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
}
]
}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) andlinks.webappon your CRM record, along with the subjectids next to their own CRM records. - If a case already exists with that
external_ref, you get409 case_exists, with the existing case'sidin the error body. Fetch it withGET /v1/cases?external_ref=…and update it rather than creating another. - Validation is strict. A
422lists every invalid field (errors[].pointer, e.g./individuals/0/date_of_birth).
Step 4 — Upload documents (before the assessment)
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=069Qy000001DocAAA202 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.
statusmoves toavailableorrejected, usually within 30 seconds. - A
rejecteddocument carries arejection_reason, for examplepdf_encrypted(password-protected PDF; owner-password-only PDFs are accepted),pdf_too_complex,scan_failedorscan_incomplete. Rejected documents do not count toward the 20 per case. - An assessment started before a valuation report becomes
availablewill not use it.
Step 5 — Quote (recommended)
POST /v1/cases/{case_id}/assessments/quote{
"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, andtoken_costis what you will be charged. missing_datawithseverity: "required"means that check will be skipped. Fix the data first if the check matters to you. A row may carryone_of(supplying any one of those fields unblocks the check) orderived: true(WALT derives the value from the address or UPRN; correct the address instead).warnings[]lists the error a start would return now, for examplequota_exhaustedorcase_closed.
Step 6 — Start the assessment
POST /v1/cases/{case_id}/assessments
Idempotency-Key: 3c2e…
Content-Type: application/json
{"mode": "incremental", "checks": {"exclude": []}}202 Accepted
Location: /v1/assessments/a4f0…
{"id": "a4f0…", "case_id": "5b8d…", "status": "queued", "token_cost": 2, "created_at": "2026-10-09T14:05:00Z"}| Status | Code | Meaning |
|---|---|---|
409 | assessment_in_progress | An assessment is already running for this case |
402 | quota_exhausted | Not enough tokens; detail gives the cost and what remains |
403 | module_not_enabled | The case type is not in your plan |
422 | case_incomplete | The minimum data is missing (at least one subject for the case type) |
409 | assessment_cooldown | The 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). |
409 | case_closed | The case is closed. Reopen it first; a closed case is never assessed or charged. |
422 | validation_failed | For example checks.exclude named a check that is not a provider search (errors[].code not_excludable; see excludable in /v1/check-catalog) |
429 | rate_limited | Hourly 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
failedonly 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 assessmentcompletedwith that checkfailed. 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:
GET /v1/assessments/{assessment_id}{
"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
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_idanddecision.version.
Leave the final lending decision to the underwriter.
7. Resource reference
7.1 Case types
case_type | Module required | Subjects assessed |
|---|---|---|
credit_identity | Credit & Identity Profile | Individuals, companies |
property_lending · property_buy_to_let · property_development · property_real_estate | Property Profile | Properties |
full | WALT Underwriting Suite | Individuals, companies, properties |
case_type is required and cannot be changed after the first assessment.
7.2 Endpoints
| Method | Path | Scope | Purpose |
|---|---|---|---|
| POST | /v1/oauth/token | — | Get an access token |
| GET | /v1/account | account:read | Modules, quota, usage, limits |
| GET | /v1/check-catalog | any | Checks, their step, module and required data |
| POST | /v1/cases | cases:write | Create a case (with nested subjects) |
| GET | /v1/cases | cases:read | List and filter cases |
| GET · PATCH | /v1/cases/{case_id} | cases:read · cases:write | Read or update a case and its loan details |
| POST | /v1/cases/{case_id}/close | cases:write | Close a case (reason: completed, withdrawn, declined, duplicate) |
| POST · PATCH · DELETE | /v1/cases/{case_id}/individuals[/{id}] | cases:write | Manage individuals |
| POST · PATCH · DELETE | /v1/cases/{case_id}/companies[/{id}] | cases:write | Manage companies |
| POST · PATCH · DELETE | /v1/cases/{case_id}/properties[/{id}] | cases:write | Manage properties |
| POST · DELETE | /v1/cases/{case_id}/ownerships | cases:write | Link or unlink individual ↔ company |
| POST | /v1/cases/{case_id}/documents | documents:write | Upload a document |
| GET | /v1/cases/{case_id}/documents | cases:read | List documents |
| GET | /v1/documents/{document_id}/content | cases:read | The file, streamed (200; documents you uploaded, once available) |
| POST | /v1/cases/{case_id}/assessments/quote | assessments:write | Cost, planned checks, missing data |
| POST | /v1/cases/{case_id}/assessments | assessments:write | Start an assessment |
| GET | /v1/cases/{case_id}/assessments | assessments:read | List a case's assessments |
| GET | /v1/assessments/{assessment_id} | assessments:read | Status, progress, pending actions |
| POST | /v1/assessments/{assessment_id}/cancel | assessments:write | Cancel a queued or running assessment |
| GET | /v1/assessments/{assessment_id}/decision | assessments:read | Decision object |
| GET | /v1/assessments/{assessment_id}/checks | assessments:read | Derived per-check results |
| GET | /v1/assessments/{assessment_id}/report | reports:read + pii:read | PDF report, streamed (200 application/pdf) |
| GET | /v1/lookups/addresses?postcode=&line1= | lookups:read | Address and UPRN candidates (rate-limited) |
| GET · POST · PATCH · DELETE | /v1/webhook-endpoints[/{id}] | webhooks:manage | Manage webhook endpoints |
| POST | /v1/webhook-endpoints/{id}/test | webhooks:manage | Send a ping event |
| GET | /v1/events?since= | assessments:read | Event 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
| Field | Type | Req. | Notes |
|---|---|---|---|
external_ref | string ≤128 | recommended | Your record ID; unique per case |
reference | string ≤64 | no | Your human-readable reference; WALT generates one if omitted |
case_type | enum | yes | §7.1 |
loan.facility_amount_gbp | integer | yes for full/property_* | Gross facility requested |
loan.term_months | integer 1–60 | yes for full | |
loan.loan_type | enum | yes for full | bridge, development, refurbishment |
loan.interest_type | enum | no | retained, serviced, rolled_up |
loan.exit_strategy | enum | yes for full | sale, refinance, other |
loan.exit_strategy_detail | string ≤1000 | if other | Treated as untrusted text (§14) |
loan.ltv_basis | enum | no | day_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_transcript | object | no | v1.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-only | id, status (open, closed), created_via (api, webapp), created_at, updated_at, latest_assessment (summary), links |
7.4 Individual fields
| Field | Type | Req. | Used by |
|---|---|---|---|
external_ref | string | no | |
is_primary | bool | one per case | Exactly one primary individual on credit cases |
role | enum | yes | borrower, guarantor, director, shareholder |
title | enum | yes | mr, mrs, ms, miss, dr, prof, mx, other |
first_name, middle_name, surname | string ≤100 | first/surname yes | All checks |
date_of_birth | date | yes | Credit, KYC, sanctions. Must be in the past; age 18+ |
gender | enum | no | male, female, other, undisclosed |
nationality · country_of_birth · residence_country | ISO alpha-2 | nationality recommended | Sanctions/PEP |
email | for IDV | Borrower ID-verification invitation | |
phone | E.164 | for IDV | SMS ID-verification invitation |
address.line1/line2/city/postcode/country | strings | line1, city, postcode yes | Credit, KYC |
previous_addresses[] | address + from/to | if < 3 years at current address | Credit match quality |
passport_number | string | no | Write-only (returned only with pii:read) |
source_of_funds | object | no | { "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_account | object | no | { "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:
| Field | Type | Req. | Notes |
|---|---|---|---|
address.line1/city/postcode | strings | yes | |
uprn | digits ≤12 | strongly recommended | Avoids address-matching errors. Use /v1/lookups/addresses if you don't hold it |
asset_type | enum | yes | residential, commercial, mixed_use, multi_unit_block, hmo, new_build_residential |
property_type | enum | yes if residential | flat, terraced, semi_detached, detached, bungalow, block_of_flats |
tenure | enum | yes | freehold, leasehold, share_of_freehold |
tenure_unexpired_years | int | if leasehold | |
num_rooms | int | recommended | Bedrooms; used by AVMs |
floor_area_sqft | int 1–1,000,000 | recommended for new_build_residential | Gross internal area (GIA) in sq ft. A new build is valued against local £/sq ft comparables |
charge_type | enum | yes | first, second |
first_charge_amount_gbp, first_charge_lender | int, string | if second charge | |
purchase_price_gbp, asking_price_gbp | int | recommended | |
estimated_value_gbp | int | no | Your own/broker estimate. Shown alongside WALT's figures, never used as the valuation |
units_in_building | int 0–1000 | if multi-unit | Each unit costs one token |
units[] | array | if multi-unit | { "label", "beds", "area_sqm", "epc", "rent_pcm_gbp", "value_gbp" } |
| BTL fields | ints | property_buy_to_let | target_rent_pcm_gbp (required), service_charge_ground_rent_gbp, maintenance_costs_gbp |
| Development fields | property_development | development_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)
{
"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"
}statusis one ofpending,running,completed,skipped,awaiting_action,failed,cancelled.outcomeis one ofpass,refer,fail,not_run,error.- Scores run 0–100, higher means riskier.
risk_levelislowbelow 40,mediumfrom 40 to 69,highfrom 70. reused: truemeans the result was carried forward fromsource_assessment_id, without a new search.searched_atis when the data was actually obtained.metricscontains 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
{
"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"
}| Field | Meaning |
|---|---|
recommendation | approve, approve_with_conditions, refer or decline. Advisory. |
is_final | false while pending_actions is not empty; the decision may still change. |
version | Goes 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_source | walt_synthesis (normal) or rules_fallback (the AI synthesis was unavailable; the decision is from rule-based scoring and is usually refer). |
human_review_required | Always true in v1. |
steps[] | One entry per step in the case type. Steps that do not apply are omitted. |
overall_risk_level | Follows the same bands as §7.6. |
| Guard-rails | Some 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 metrics | The 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
| Event | When |
|---|---|
assessment.completed | An assessment reached completed (final decision) |
assessment.action_required | The assessment produced a provisional decision; a borrower or third party must act (§10) |
assessment.updated | The decision changed (new version), e.g. ID verification completed or expired |
assessment.failed | The 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.ready | The PDF report for the latest decision version is ready (fetching it needs reports:read and pii:read) |
case.erased | The case was erased in WALT (retention or data-subject request). Delete your copies of WALT outputs according to your own policy |
quota.threshold_reached | 80% and 100% of a module's monthly quota used |
ping | Test event |
9.3 Payload (thin by design — no personal data)
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{
"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))
- Parse
tand everyv1value. There can be two during a secret rotation; accept if either matches. - Reject the request if
|now − t| > 300seconds (replay protection). - Compute the expected signature over the raw body bytes, before any JSON parsing.
- Compare in constant time.
- De-duplicate on
WALT-Event-Id: delivery is at least once.
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
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)
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_versionand theGETendpoints to decide what is current. - Missed something?
GET /v1/events?since=2026-10-09T00:00:00Zlists 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 invalidcursor→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[].type | What happens | Completion |
|---|---|---|
borrower_identity_verification | WALT's ID-verification partner sends the borrower an SMS (or email) link for document + selfie verification | Detected automatically within about 5 minutes of the borrower finishing. The link expires after 14 days. |
borrower_source_of_funds | Reserved: 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_required | LTV could only be estimated from AVMs; the policy requires a RICS valuation | Upload a valuation_report and start a new assessment |
Example:
"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 bestrefer(guard-railborrower_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
emailandphoneare 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 trigger | Behaviour |
|---|---|
surname Approve | Clean KYC/AML, good credit → approve |
surname Conditions | Satisfied CCJ → approve_with_conditions |
surname Refer | Thin credit file, address mismatch → refer |
surname Decline | Recent default + IVA → decline |
surname Sanctions | Sanctions match → KYC fail → decline |
surname Pep | PEP match → refer |
surname Idvpending | awaiting_action for 2 minutes, then ID verification passes → assessment.updated |
surname Idvfail | ID verification fails after 2 minutes → assessment.updated with refer |
surname Outage | Credit provider unavailable → check error, decision_source: rules_fallback |
surname Slow | Assessment takes 6 minutes (tests your polling and timeouts) |
property postcode ZZ1 1HL | High LTV (85%) → valuation refer |
property postcode ZZ1 1LO | Low LTV (45%) |
property postcode ZZ1 1MU + units_in_building | Multi-unit block valuation |
company number 00000000 | Dissolved company → fail |
| anything else | Randomised but internally consistent results |
Notes:
- Sandbox webhooks are sent exactly as in production.
POST /v1/webhook-endpoints/{id}/testsends aping. - 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
| Pattern | When | Strengths | Watch out |
|---|---|---|---|
| A. Integration service (recommended) — a small service you host (Azure Function, AWS Lambda, container) sits between your CRM and WALT | You have an IT/dev team; several CRMs or systems; strict security | Secrets outside the CRM; easy webhook endpoint; retries, queueing, logging in one place; CRM-agnostic | One more component to run |
| B. CRM-native code — Apex, Dynamics plug-ins/Azure Functions, HubSpot custom-coded actions/serverless functions | Single CRM, platform-savvy team | Fewer moving parts | CRM governor/time limits; the webhook endpoint must be exposed from the CRM platform |
| C. iPaaS / low-code — Power Automate, MuleSoft, Boomi, Workato, Make, Zapier, n8n | Small team, standard flow | Fast to build; import openapi.json | Make 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.
┌───────────── 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 field | Source |
|---|---|
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 ID | assessment.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 ID | Error 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_subsequentitem, with an owner and a status. - WALT Step Result — step, outcome, risk level, summary.
12.3 The connector state machine
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:
- 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.
- 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
422errors[]to the user against the right fields, usingpointer. - One idempotency key per logical operation, stored with the request (e.g. on the record), reused on every retry.
- Upsert by
external_ref.- On first send, create the case.
- On a later send: if no assessment has started,
PATCHthe changed fields and add or delete subjects. If an assessment has run, the case becomesstale; let the user decide whether to re-assess. - Re-assess in the default
incrementalmode. 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.
- Documents before assessment. Upload, wait until each is
available(or thedocument.availablestate shows up inGET …/documents), then quote, then assess. - 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.
- 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
GETtheir assessment.
- Write-back is idempotent. Apply a decision only if
decision.versionis greater than the stored one. - Fetch the PDF once per decision version. Store it as a file on the record, under your retention policy (§14).
- 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
| Response | What the connector does |
|---|---|
400 / 422 | Do not retry. Show errors[] to the user; set Needs data. |
401 invalid_token | Get a new token once and retry once. If it fails again, alert (the credential may be revoked). |
402 quota_exhausted | Do not retry. Notify the WALT account owner at your firm. |
403 | Do not retry. Configuration/entitlement problem: alert the integration owner. |
404 | The record may have been erased or the ID is wrong. Re-sync with GET /v1/cases?external_ref=. |
409 case_exists | Switch to update mode using the returned case ID. |
409 assessment_in_progress | Wait for the webhook; do not retry the start. |
409 assessment_cooldown | A previous assessment failed. Wait Retry-After seconds, then offer the user a retry. Never auto-loop. |
409 idempotency_conflict | Bug: 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_unavailable | The operation was processed but its response is lost. Do not retry with the same key; re-read the state with a GET. |
409 case_closed | The case is closed in WALT. Reopen it there before assessing; do not auto-retry. |
412 precondition_failed | Re-read (GET), merge, retry the PATCH with the new ETag. |
429 rate_limited | Wait Retry-After seconds, then retry. |
5xx, timeouts, network errors | Retry 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:readonly 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
- Run the sandbox persona matrix (§11) end to end, including
Idvpending,OutageandSlow. - 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.
- invalid postcode or DOB →
- Run security tests:
- a webhook with a tampered body or a stale timestamp → rejected;
- search the logs for personal data.
- 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 IDas 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.jsonand 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.
- 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.
- No automated decisions solely on WALT output. A qualified person reviews the evidence and makes the decision (UK GDPR Art. 22). Record who decided.
- 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.
- Data minimisation. Send only the fields WALT needs. Do not put unrelated personal data in free-text fields.
- Security. Follow §12.5. Report suspected credential compromise or misuse to WALT within 24 hours.
- 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.
- Environments. Never send real personal data to the sandbox.
15. Error catalogue
All errors are application/problem+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."}
]
}| HTTP | code | Retry? |
|---|---|---|
| 400 | bad_request, invalid_json, idempotency_key_required | No |
| 401 | invalid_client, invalid_token, token_expired | Refresh the token once |
| 403 | insufficient_scope, module_not_enabled, ip_not_allowed, organisation_suspended, acting_user_not_allowed | No |
| 404 | not_found | No |
| 409 | case_exists, assessment_in_progress, case_locked, idempotency_conflict, idempotency_result_unavailable, case_type_locked, case_closed, conflict | No (handle) |
| 409 | idempotency_in_progress | Yes, shortly, with the same key |
| 409 | assessment_cooldown | After Retry-After (≤ 5 min) |
| 412 | precondition_failed | Re-read, then retry |
| 413 | payload_too_large | No |
| 415 | unsupported_media_type | No |
| 402 | quota_exhausted | No |
| 422 | validation_failed, case_incomplete, acting_user_not_found | No |
| 429 | rate_limited | After Retry-After |
| 500 | internal_error | Yes, with backoff |
| 503 | service_unavailable, maintenance | Yes, after Retry-After |
403 organisation_suspendedis also returned when your organisation is closed.409 idempotency_result_unavailable: the request was processed but its response cannot be returned (§5).422 validation_failedlists field-level codes inerrors[].code, for examplenot_excludable(§6 Step 6),primary_required(§7.2) andstep_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):
| Limit | Value |
|---|---|
| API requests | 600 / minute |
| Token requests | 30 / minute (authenticated requests; unauthenticated attempts are limited per client ID and IP) |
| Assessment starts | 60 / hour per organisation (accepted starts only; refused starts and 429s are not counted) |
| Concurrent assessments | 5 per organisation (extra starts are queued, or refused with 429 rate_limited and Retry-After where queueing is not enabled) |
| Address lookups | 30 / minute |
| Request body | 1 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 upload | 25 MB per file, 20 files per case |
| Subjects per case | 20 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
| Name | Values |
|---|---|
case_type | credit_identity, property_lending, property_buy_to_let, property_development, property_real_estate, full |
case.status | open, closed |
assessment.status | queued, running, awaiting_action, completed, failed, cancelled |
assessment.mode | incremental, full |
ltv_basis | day_180, market_value |
recommendation | approve, approve_with_conditions, refer, decline |
check.status | pending, running, completed, skipped, awaiting_action, failed, cancelled |
check.outcome | pass, refer, fail, not_run, error |
risk_level | low, medium, high |
step | borrower_profile, kyc_and_aml, credit_assessment, borrower_call, property_valuation, property_due_diligence |
loan_type | bridge, development, refurbishment |
interest_type | retained, serviced, rolled_up |
exit_strategy | sale, refinance, other |
individual.role | borrower, guarantor, director, shareholder |
company.role | borrower, guarantor, corporate_shareholder |
asset_type | residential, commercial, mixed_use, multi_unit_block, hmo, new_build_residential |
property_type | flat, terraced, semi_detached, detached, bungalow, block_of_flats |
tenure | freehold, leasehold, share_of_freehold |
charge_type | first, second |
planning_status | none, pending, approved, refused |
document_type | valuation_report, title_register, lease, bank_statement, proof_of_address, other |
pending_action.type | borrower_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 field | Required for | Your CRM object.field | Transform | Notes |
|---|---|---|---|---|
external_ref | all | Opportunity.Id | — | |
case_type | all | Opportunity.Product__c | map → enum | |
loan.facility_amount_gbp | full, property | Opportunity.Amount | round to £ | |
loan.term_months | full | Opportunity.Term__c | — | |
loan.loan_type | full | … | map → enum | |
loan.exit_strategy | full | … | map → enum | |
individuals[].first_name/surname | credit, full | Contact.FirstName/LastName | — | |
individuals[].date_of_birth | credit, full | Contact.Birthdate | ISO date | |
individuals[].address.* | credit, full | Contact.MailingAddress | split | |
individuals[].email/phone | IDV | Contact.Email / MobilePhone | E.164 | |
individuals[].nationality | sanctions | … | ISO alpha-2 | |
companies[].company_number | credit, full | Account.CompanyNumber__c | pad to 8 | |
properties[].address.* | property, full | Collateral__c.* | split | |
properties[].uprn | recommended | Collateral__c.UPRN__c | digits | |
properties[].asset_type/property_type/tenure | property, full | … | map → enum | |
write-back: decision.recommendation | Opportunity.WALT_Recommendation__c | — | ||
| write-back: report PDF | Files 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-Keyon every create; retries reuse the same key.external_refset on the case and subjects; duplicate send → no duplicate case.- Pre-flight validation in the CRM;
422errors displayed to users per field. - Documents uploaded and
availablebefore 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.versionrespected (no overwrite by an older event).awaiting_actionhandled without starting new assessments.402/403/409/412/429/5xxhandled 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.