Nuba

Nuba Verify API

The Nuba Verify API lets you run fully automated tenant verification from your own product. It handles document parsing, address validation, Open Banking transaction matching and landlord verification — returning a single weighted score and a pass/fail outcome for each tenancy.

JSON REST API

All requests and responses use application/json

Webhook delivery

Events pushed to your endpoint on session completion

HMAC signatures

Every webhook is signed with SHA-256 for authenticity

Authentication

All partner endpoints require an API key passed in the x-api-key request header. Keys are prefixed with nuba_sk_ and are created in your partner dashboard.

Never expose your API key client-side. Always create verification sessions from your server and pass only the resulting sessionId to the browser.
http
POST /v1/verify/sessions HTTP/1.1
Host: verify-api.nubarewards.com
Content-Type: application/json
x-api-key: nuba_sk_<your_key>

Base URL

text
Production:  https://verify-api.nubarewards.com
Sandbox:     use the Sandbox runner in your partner dashboard

There is no separate sandbox host. Use the Sandbox tab in your dashboard to trigger test sessions with pre-seeded data.

Types & Enums

SessionStatus

The current state of a verification session.

typescript
type SessionStatus =
  | 'PENDING'        // Session created; awaiting document upload
  | 'EXTRACTING'     // Document uploaded; AI extraction in progress
  | 'PROCESSING'     // Tenant confirmed details; all layers running
  | 'PASSED'         // Score ≥ partner's pass mark threshold
  | 'MANUAL_REVIEW'  // Score between review and pass mark thresholds
  | 'FAILED'         // Score < partner's manual review threshold

LayerStatus

typescript
type LayerStatus =
  | 'PENDING'    // Layer not yet started
  | 'RUNNING'    // Layer job in progress
  | 'EXTRACTED'  // Document layer: extraction complete, awaiting confirm
  | 'COMPLETE'   // Layer finished successfully with a score
  | 'FAILED'     // Layer failed (score is 0 or null)
  | 'SKIPPED'    // Layer intentionally bypassed (e.g. Open Banking when
                 //   financialVerificationEnabled = false on the partner)

LayerType

typescript
type LayerType =
  | 'DOCUMENT'     // Layer 1 — AI document parsing & tampering detection (35%)
  | 'ADDRESS'      // Layer 2 — Address & rent benchmarking (25%)
  | 'OPEN_BANKING' // Layer 3 — Bank transaction matching via TrueLayer (25%)
  | 'LANDLORD'     // Layer 4 — Landlord/agent entity verification (15%)

CreateSessionRequest

typescript
interface CreateSessionRequest {
  tenantName:        string;            // Full name of the tenant
  tenantEmail:       string;            // Tenant's email address
  webhookUrl?:       string;            // URL to receive completion events (optional)
  redirectUrl?:      string;            // Where to redirect after wizard completes (optional)

  // ── BYO (Bring Your Own) banking data — added v1.5 ──────────────
  // Partners that already hold tenant transaction history can submit
  // it here instead of redirecting the tenant through TrueLayer.
  // Requires byoEnabled = true on your partner config; see the
  // Verification Config tab in your dashboard.
  transactionStream?: TransactionStream; // Pre-collected bank data
  byoMode?:           'fallback' | 'strict'; // Per-session override
}

interface TransactionStream {
  source:        string;             // Free-text source label (e.g. "Plaid", "in-house")
  attestedAt:    string;             // ISO 8601 — when the data was collected
  windowMonths:  number;             // How many months of history are included (1–24)
  account: {
    sortCode:      string;           // 6 digits
    accountNumber: string;           // 8 digits
    accountName?:  string;
  };
  transactions: Array<{
    date:        string;             // ISO 8601
    amount:      number;             // GBP, negative for outgoing
    description: string;
    reference?:  string;
  }>;
}

CreateSessionResponse

typescript
interface CreateSessionResponse {
  sessionId:       string;            // UUID — use in all subsequent calls
  status:          SessionStatus;     // Always 'PENDING' on creation
  verificationUrl: string;            // Tenant-facing wizard URL

  // Present only when transactionStream was supplied on the request.
  byo?: {
    accepted: boolean;                // false ⇒ stream rejected, see reason
    reason?:  string;                 // Why the stream was rejected (strict mode)
    mode:     'fallback' | 'strict';  // Effective mode used for this session
  };
}

ExtractedData

typescript
interface ExtractedData {
  tenantName:        string;
  landlordName:      string;
  landlordEntityType: 'individual' | 'company';
  address:           string;       // Full address string from the document
  rentAmount:        number;       // Monthly rent in GBP
  tenancyStartDate:  string;       // ISO 8601 date
  bankSortCode:      string | null;
  bankAccountNumber: string | null;
  paymentReference:  string | null;
  bedrooms:          number | null;
  tamperingFlags:    string[];     // List of detected anomaly codes
  documentConfidence: number;      // 0–1 extraction confidence
}

ConfirmDetailsRequest

typescript
interface ConfirmDetailsRequest {
  tenantName?:           string;
  addressLine1:          string;   // required
  addressLine2?:         string;
  city:                  string;   // required
  postcode:              string;   // required
  propertyType:          'residential' | 'commercial';  // required
  bedrooms?:             number;   // required for residential
  furnished:             boolean;  // required
  rentAmount:            number;   // required — monthly GBP
  rentDueDate:           number;   // required — day of month 1–31
  landlordName:          string;   // required
  landlordType:          'individual' | 'company';   // required
  landlordEmail?:        string;
  accountName:           string;   // required — name on rent-receiving bank account
  landlordSortCode:      string;   // required — 6 digits
  landlordAccountNumber: string;   // required — 8 digits
  paymentReference?:     string;
  tenancyStartDate?:     string;   // ISO 8601
  tenancyEndDate?:       string;   // ISO 8601; omit for rolling tenancies
}

SessionDetails

typescript
interface SessionDetails {
  sessionId:   string;
  status:      SessionStatus;
  totalScore:  number | null;   // 0–100; null while PENDING/EXTRACTING
  createdAt:   string;          // ISO 8601
  updatedAt:   string;

  tenant: {
    name:  string | null;
    email: string | null;
  };

  property: {
    addressLine1:  string | null;
    addressLine2:  string | null;
    city:          string | null;
    postcode:      string | null;
    propertyType:  'residential' | 'commercial';
    bedrooms:      number | null;
    furnished:     boolean | null;
  };

  tenancy: {
    rentAmount:  number | null;
    rentDueDate: number | null;  // day of month
    startDate:   string | null;
    endDate:     string | null;
  };

  rentRecipient: {
    accountName:      string | null;
    sortCode:         string | null;
    accountNumber:    string | null;
    paymentReference: string | null;
  };

  landlord: {
    name:  string | null;
    type:  'individual' | 'company' | null;
    email: string | null;
  };

  document: {
    url:            string | null;   // Presigned URL, expires in 1 hour
    mimeType:       string | null;
    fileSize:       number | null;   // bytes
    confidence:     number | null;   // 0–1
    tamperingFlags: string[];
  };

  verificationSummary: string | null;  // AI-generated plain-English summary
}

WebhookPayload

Discriminate the event type by the status field (PASSED / MANUAL_REVIEW / FAILED). The dispatch timestamp is delivered in the x-nuba-timestamp header alongside the signature, not inside the body.

typescript
interface WebhookPayload {
  sessionId:  string;
  status:     SessionStatus;            // PASSED | MANUAL_REVIEW | FAILED
  totalScore: number;
  breakdown: {
    document:    { score: number; flags: string[] };
    address:     { score: number; propertyConfirmed: boolean; rentRatio: number };
    openBanking: { score: number; consecutiveMonthsFound: number; accountMatchConfirmed: boolean };
    landlord:    { score: number; entityType: 'individual' | 'company'; networkMatch: boolean };
  };
  summary: string | null;               // AI-generated plain-English summary
}
Fields you've hidden via your disclosure scope are silently omitted from this payload (no placeholder is sent in their place). Configure scope under Integration → Disclosure.

Endpoints

POST /v1/verify/sessions

Creates a new verification session. Returns a verificationUrl to send to the tenant (or open in the embedded SDK), and a sessionId for all subsequent calls. Requires x-api-key.

Request body

FieldTypeDescription
tenantNamereqstringFull legal name of the tenant.
tenantEmailreqstringTenant's email address. Used to send the verification link.
webhookUrlstringURL to receive session completion events. Can also be configured in the dashboard.
redirectUrlstringWhere the wizard redirects after completion (non-embedded flows). Defaults to the request origin.
transactionStreamTransactionStreamPre-collected bank transaction history. Skips the TrueLayer step. Requires byoEnabled = true on your partner config. See the TransactionStream type.
byoMode'fallback' | 'strict'Per-session override of your default BYO mode. 'fallback' falls back to TrueLayer if the stream is invalid; 'strict' rejects the request with 422 instead.
json
// Response — 201 Created
{
  "sessionId":       "e324ad63-1018-4f42-a0ae-db764682f5a6",
  "status":          "PENDING",
  "verificationUrl": "https://verify.nubarewards.com/verify/e324ad63-..."
}

// Response — 201 Created (with BYO transaction stream)
{
  "sessionId":       "e324ad63-...",
  "status":          "PENDING",
  "verificationUrl": "https://verify.nubarewards.com/verify/e324ad63-...",
  "byo": {
    "accepted": true,
    "mode":     "fallback"
  }
}

// Response — 422 (BYO rejected, strict mode)
{
  "error":   "BYO_VALIDATION_FAILED",
  "message": "transactionStream.windowMonths must be ≥ 6 for strict mode"
}

POST /v1/verify/sessions/:sessionId/document

Uploads the tenancy agreement (as a Base64-encoded string). Triggers Layer 1 extraction. The wizard handles this step automatically when tenants upload their document. No x-api-key required — the session ID acts as the credential.

FieldTypeDescription
documentBase64reqstringBase64-encoded PDF or image of the tenancy agreement.
json
// Response — 202 Accepted
{
  "sessionId": "e324ad63-...",
  "status":    "EXTRACTING",
  "message":   "Document uploaded. Extraction in progress."
}

GET /v1/verify/sessions/:sessionId/extracted

Returns the AI-extracted data from Layer 1. Poll every 2 seconds until ready: true. No auth required — safe to call from the tenant's browser.

json
// Response — extraction complete
{
  "status": "EXTRACTED",
  "ready":  true,
  "extracted": {
    "tenantName":         "Jane Smith",
    "landlordName":       "Acme Properties Ltd",
    "landlordEntityType": "company",
    "address":            "42 Victoria Road, Manchester, M1 2JB",
    "rentAmount":         1200,
    "tenancyStartDate":   "2025-01-15",
    "bankSortCode":       "112233",
    "bankAccountNumber":  "87654321",
    "paymentReference":   "SMITH42",
    "bedrooms":           2,
    "tamperingFlags":     [],
    "documentConfidence": 0.97
  }
}

// Response — still processing
{
  "status": "RUNNING",
  "ready":  false,
  "extracted": null
}

POST /v1/verify/sessions/:sessionId/confirm

Submits the tenant-confirmed (and optionally corrected) details. Runs Layer 1 cross-checks, then kicks off Layers 2 and 4 in parallel. Returns a TrueLayer auth URL for Layer 3 (Open Banking). No auth required.

Request body — see ConfirmDetailsRequest

json
// Response — 200 OK
{
  "sessionId":       "e324ad63-...",
  "status":          "PROCESSING",
  "truelayerAuthUrl": "https://auth.truelayer.com/?...",
  "message":         "Details confirmed. Verification in progress."
}

GET /v1/verify/:sessionId

Public status endpoint. Returns real-time session state and per-layer progress. No auth required — poll from the tenant's browser or your own dashboard.

json
// Response — session in progress
{
  "sessionId":  "e324ad63-...",
  "status":     "PROCESSING",
  "totalScore": null,
  "layers": {
    "document":    { "status": "COMPLETE",  "score": 88 },
    "address":     { "status": "COMPLETE",  "score": 75 },
    "openBanking": { "status": "RUNNING",   "score": null },
    "landlord":    { "status": "RUNNING",   "score": null }
  },
  "theme": {
    "primaryColor":   "#E8A23A",
    "partnerName":    "Acme Rentals",
    "partnerLogoUrl": "https://..."
  }
}

// Response — session complete
{
  "sessionId":  "e324ad63-...",
  "status":     "PASSED",
  "totalScore": 82.5,
  "layers": {
    "document":    { "status": "COMPLETE", "score": 88 },
    "address":     { "status": "COMPLETE", "score": 75 },
    "openBanking": { "status": "COMPLETE", "score": 91 },
    "landlord":    { "status": "COMPLETE", "score": 70 }
  },
  "breakdown": {
    "document":    { "score": 88, "flags": [] },
    "address":     { "score": 75, "propertyConfirmed": true, "rentRatio": 1.1 },
    "openBanking": { "score": 91, "consecutiveMonthsFound": 6, "accountMatchConfirmed": true },
    "landlord":    { "score": 70, "entityType": "company", "networkMatch": true }
  }
}

GET /v1/verify/sessions/:sessionId/details

Returns the full session result including all structured fields, scores, document URL and a plain-English AI summary. Requires x-api-key. The API key must belong to the partner that owns the session.

json
// Response — 200 OK
{
  "sessionId":   "e324ad63-...",
  "status":      "PASSED",
  "totalScore":  82.5,
  "createdAt":   "2026-04-17T09:12:00.000Z",
  "updatedAt":   "2026-04-17T09:14:31.000Z",

  "tenant":  { "name": "Jane Smith", "email": "jane@example.com" },

  "property": {
    "addressLine1": "42 Victoria Road",
    "addressLine2": null,
    "city":         "Manchester",
    "postcode":     "M1 2JB",
    "propertyType": "residential",
    "bedrooms":     2,
    "furnished":    true
  },

  "tenancy": {
    "rentAmount":  1200,
    "rentDueDate": 1,
    "startDate":   "2025-01-15",
    "endDate":     "2026-01-14"
  },

  "rentRecipient": {
    "accountName":      "Acme Properties Ltd",
    "sortCode":         "112233",
    "accountNumber":    "87654321",
    "paymentReference": "SMITH42"
  },

  "landlord": {
    "name":  "Acme Properties Ltd",
    "type":  "company",
    "email": "info@acmeprops.co.uk"
  },

  "document": {
    "url":            "https://verify-api.nubarewards.com/...",
    "mimeType":       "application/pdf",
    "fileSize":       204800,
    "confidence":     0.97,
    "tamperingFlags": []
  },

  "verificationSummary": "The tenant has provided a valid tenancy agreement ..."
}

POST /v1/verify — Legacy

This endpoint is kept for backward compatibility. New integrations should use the multi-step flow above.

Single-request verification — all data submitted in one payload. Requires x-api-key. Returns a TrueLayer auth URL for Open Banking immediately.

typescript
// Request body
interface LegacyVerifyRequest {
  tenantName:          string;
  tenantEmail:         string;
  documentBase64:      string;
  addressLine1:        string;
  city:                string;
  postcode:            string;
  bedrooms?:           number;
  furnished:           boolean;
  rentAmount:          number;
  landlordName:        string;
  landlordEmail?:      string;
  landlordSortCode:    string;
  landlordAccountNumber: string;
  webhookUrl?:         string;
}
json
// Response — 202 Accepted
{
  "sessionId":       "e324ad63-...",
  "truelayerAuthUrl": "https://auth.truelayer.com/?..."
}

Recurring Payment Confirmation

Coming v1.6. This API is currently in implementation. The shape below is the committed contract — partners can build against it now. Reach out to your Nuba contact for sandbox access during the pilot window.

Once a session reaches PASSED or MANUAL_REVIEW, you can submit ongoing transaction data and have Nuba identify which transactions are the verified rent payment for that tenancy. Each match emits a zero-PII payment.confirmed webhook — useful for triggering rewards, on-time-payment reporting, or rent-financing actions without re-implementing matching logic on your side.

Submission is idempotent — re-POSTing overlapping windows never double-confirms or double-bills. Sessions in FAILED state reject transaction submissions with HTTP 409 (no verified rent contract to match against).

POST /v1/verify/sessions/:sessionId/transactions

Submits one or more transactions for matching against the verified tenancy. Synchronous for batches ≤ 100 transactions; queued (returns 202) above that or when ?async=true is passed. Requires x-api-key; the key must own the session.

Request body

typescript
interface SubmitTransactionsRequest {
  transactions: Array<{
    transactionId?:              string;   // Your internal ID — used as idempotency key if present
    date:                        string;   // ISO 8601
    amountPence:                 number;   // Negative for outgoing (tenant → landlord)
    description:                 string;
    reference?:                  string;
    counterpartySortCode?:       string;   // Optional but improves match confidence
    counterpartyAccountNumber?:  string;   // Optional but improves match confidence
  }>;
}
json
// Response — 200 OK (synchronous)
{
  "sessionId": "e324ad63-...",
  "received":  4,
  "matched":   1,
  "matches": [
    {
      "transactionId":    "boogi_txn_8829",
      "matchId":          "pm_a3f2...",
      "paidOnDate":       "2026-04-21",
      "amountPence":      120000,
      "matchedReference": "SMITH42",
      "confidence":       0.94,
      "billingPeriod":    "2026-04"
    }
  ],
  "duplicates": 0
}

// Response — 202 Accepted (async or batch > 100)
{
  "jobId":     "pmscan_...",
  "received":  428,
  "statusUrl": "/v1/verify/sessions/e324ad63-.../transactions/pmscan_..."
}
Idempotency: Each transaction generates a key from your supplied transactionId, or — if absent — a hash of (sessionId, date, amountPence, normalisedReference). Re-submissions of the same logical transaction increment the duplicates counter and do not emit a webhook or incur a billing event.

GET /v1/verify/sessions/:sessionId/payments

Lists all confirmed payment matches for a session, newest first. Supports ?from/?to (ISO dates) and cursor pagination via ?cursor. Requires x-api-key.

json
// Response — 200 OK
{
  "sessionId": "e324ad63-...",
  "matches": [
    {
      "matchId":          "pm_a3f2...",
      "paidOnDate":       "2026-04-21",
      "amountPence":      120000,
      "matchedReference": "SMITH42",
      "confidence":       0.94,
      "billingPeriod":    "2026-04",
      "createdAt":        "2026-04-21T18:32:11.000Z"
    },
    ...
  ],
  "nextCursor": null
}

GET /v1/verify/sessions/:sessionId/transactions/:jobId

Returns the status of an async match job. Poll until status is COMPLETE or FAILED.

json
// Response — job complete
{
  "jobId":   "pmscan_...",
  "status":  "COMPLETE",
  "matched": 12,
  "matches": [ /* same shape as synchronous response */ ],
  "duplicates": 3
}

payment.confirmed webhook

Delivered (signed with x-nuba-signature) for every match with confidence ≥ 0.60. Dispatched to your configured webhook URL using the same retry policy as session webhooks (2 retries, exponential back-off).

json
{
  "event":            "payment.confirmed",
  "sessionId":        "e324ad63-...",
  "matchId":          "pm_a3f2...",
  "paidOnDate":       "2026-04-21",
  "amountPence":      120000,
  "matchedReference": "SMITH42",
  "confidence":       0.94,
  "billingPeriod":    "2026-04"
}
This is the first webhook event with a top-level event discriminator. The existing session webhooks discriminate via status. Your handler should branch on whichever field is present — see the verification example in the Webhooks section.
Disclosure scope applies. If your partner config hides tenancy.rentAmount, the amountPence field is omitted from the payload. Handle the field as optional in your integration.

Webhooks

Nuba delivers a POST request to your configured webhook URL when a session reaches a terminal state. Configure the URL from the Integration tab in your dashboard.

Events

EventTriggered when
session.passedtotalScore ≥ partner's pass mark threshold (default 70)
session.manual_reviewtotalScore between review and pass mark thresholds
session.failedtotalScore < partner's manual review threshold (default 50)

Signature verification

Each delivery carries two headers:

  • x-nuba-timestamp — Unix seconds at dispatch.
  • x-nuba-signature — HMAC-SHA256 of `${timestamp}.${rawBody}`, formatted as t=<timestamp>,v1=<hex>.
Verify x-nuba-signature against the raw request body (bytes, not parsed JSON) before trusting the event. Reject any delivery whose timestamp is more than 5 minutes old to prevent replay attacks.
typescript
import { createHmac, timingSafeEqual } from 'crypto';

function verifySignature(rawBody: string, header: string, secret: string): boolean {
  // Header format: "t=1734567890,v1=abc123…"
  const parts = Object.fromEntries(
    header.split(',').map((kv) => kv.split('=') as [string, string]),
  );
  const t = parts.t;
  const v1 = parts.v1;
  if (!t || !v1) return false;

  // Reject stale deliveries (replay protection).
  const ageSeconds = Math.floor(Date.now() / 1000) - Number(t);
  if (Number.isNaN(ageSeconds) || ageSeconds > 300) return false;

  const expected = createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex');

  // Constant-time comparison.
  const a = Buffer.from(expected, 'hex');
  const b = Buffer.from(v1, 'hex');
  return a.length === b.length && timingSafeEqual(a, b);
}

// Express (must use express.raw, not express.json)
app.post('/webhooks/nuba', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-nuba-signature'] as string;
  if (!verifySignature(req.body.toString(), sig, process.env.NUBA_WEBHOOK_SECRET!)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(req.body) as WebhookPayload;

  switch (event.status) {
    case 'PASSED':
      await onPassed(event);
      break;
    case 'MANUAL_REVIEW':
      await flagForReview(event);
      break;
    case 'FAILED':
      await onFailed(event);
      break;
  }

  res.status(200).json({ received: true });
});

Retries

If your endpoint does not return a 2xx response within 10 seconds, Nuba retries up to two more times with exponential back-off (3 attempts total). After the final failed attempt the delivery is marked as failed and surfaced under Webhook deliveries in your dashboard, where it can be inspected and re-sent manually.

AttemptDelay after previous
1 (initial)
2~30 seconds
3~5 minutes

Back-off is approximate (Bull queue exponential strategy) and may vary by a few seconds.

Errors

All error responses share the same shape. HTTP status codes follow standard conventions.

typescript
interface ErrorResponse {
  error:    string;           // Short machine-readable message
  details?: Record<string, string[]>;  // Per-field validation errors (400 only)
  message?: string;           // Additional context
}
StatusMeaning
400Validation error — check the details field for per-field messages
401Missing or invalid API key
404Session not found (or owned by a different partner)
409Conflict — e.g. document already confirmed, or session in wrong state
422BYO transaction stream rejected (strict mode) — see message field for the specific validation failure
429Document extraction quota exceeded for the billing period
500Internal server error — contact support if it persists
502Upstream dependency error (TrueLayer, AI provider)

Changelog

v1.6

2026-04-22

  • Recurring Payment Confirmation API — POST /v1/verify/sessions/:id/transactions to submit ongoing transaction data against a verified session.
  • GET /v1/verify/sessions/:id/payments — list confirmed payment matches for a session.
  • New webhook event: payment.confirmed. First event to use a top-level `event` discriminator (existing session webhooks continue to discriminate via `status`).
  • Idempotent ingestion via partner-supplied transactionId or (sessionId, date, amountPence, reference) hash — re-POSTs never double-confirm or double-bill.
  • Currently in implementation; contract is committed. Reach out for sandbox access during the pilot window.
v1.5

2026-04-21

  • BYO (Bring Your Own) banking data — partners with their own transaction history can now submit a transactionStream on POST /v1/verify/sessions and skip the TrueLayer redirect. Gated by partner-level byoEnabled and byoMode (fallback | strict).
  • New 422 response on session create when a BYO stream fails strict-mode validation.
  • LayerStatus enum extended with SKIPPED — used when a layer is intentionally bypassed (e.g. Open Banking with financialVerificationEnabled = false).
  • Disclosure scope — partners can now hide individual fields from the GET /sessions/:id/details response and the webhook payload via the dashboard. Hidden fields are silently omitted (no placeholder).
  • WebhookPayload shape clarified: discriminate on `status`, not a top-level `event` field. Dispatch timestamp is delivered in the x-nuba-timestamp header.
  • Webhook signature header format documented: t=<unix>,v1=<sha256-hex> over `${timestamp}.${rawBody}`. The legacy `sha256=<hex>` example was incorrect.
  • Webhook retry policy corrected: 2 retries (3 attempts total), exponential back-off; failed deliveries surface in the dashboard for manual re-send.
  • Webhook payload now includes a `summary` field (AI-generated plain-English overview) at the top level.
v1.4

2026-04-17

  • Partner-configurable pass mark and manual review thresholds (PUT /partner/scoring)
  • GET /v1/verify/sessions/:sessionId/details now returns verificationSummary field
  • accountName field added to ConfirmDetailsRequest and SessionDetails.rentRecipient
  • paymentReference now surfaced on GET /extracted and pre-filled in the wizard
v1.3

2026-04-10

  • Multi-step flow promoted to recommended integration path
  • Document re-upload now permitted while session is in EXTRACTING state
  • Webhook deliveries now include full score breakdown in payload
  • Presigned document URLs delivered via /details endpoint (expire 1 h)
v1.2

2026-03-01

  • POST /v1/verify/sessions/:id/confirm — new confirm endpoint replaces body-only flow
  • GET /v1/verify/sessions/:id/extracted — polling endpoint for extraction status
  • Partner theming applied to wizard (logo, primary colour)
v1.0

2026-01-15

  • Initial release — POST /verify legacy endpoint
  • Webhook delivery with HMAC-SHA256 signing
  • Four-layer scoring engine (document 35%, address 25%, open banking 25%, landlord 15%)