Developer Platform

Build a fully custom borrower experience
on the VectraLMS API.

The Customer API exposes everything our borrower portal does as clean, tokened JSON — loans, statements, payments, scheduled payments, autopay, profile, and notifications. Ship a white-labeled web or mobile portal on top of a battle-tested servicing engine.

Everything you need to go white-label

A first-class REST API designed so a third party can build the entire borrower experience — no access to our UI required.

Token auth + 2FA

Password + SMS/email 2FA issues an opaque bearer token. 30-min idle / 8-hr absolute expiry, revocable server-side.

Full servicing surface

Loans, transactions, statements (with rendered HTML), notifications, profile, and multi-address/phone — all borrower-scoped.

PCI-safe payments

Cards are added through a hosted vault — your app never touches the PAN. Make payments, schedule them, and manage autopay.

Idempotency keys

Every money POST accepts an Idempotency-Key, so a retry can never double-charge a borrower.

Predictable errors

RFC-7807 problem responses with stable machine codes and a consistent pagination envelope across every list.

Branding built in

Public branding + reference endpoints let your SPA theme its login screen and render pickers before a borrower signs in.

Quickstart

Authenticate, then read the borrower's loans — in three calls.

  1. Start a login

    Post the borrower's email + password with your company UUID. You get back a 2FA challenge.

  2. Verify the code

    Exchange the challenge + the SMS/email code for a bearer access_token.

  3. Call the API

    Send Authorization: Bearer <token> on every request. Explore them all in the interactive reference.

# 1. Start login → returns a 2FA challenge
curl -X POST https://api.example.com/api/v1/portal/auth/login \
  -H "X-Company-UUID: <company-uuid>" \
  -H "Content-Type: application/json" \
  -d '{"email":"borrower@example.com","password":"..."}'

# 2. Verify the code → returns a bearer token
curl -X POST https://api.example.com/api/v1/portal/auth/verify \
  -H "X-Company-UUID: <company-uuid>" \
  -d '{"challenge_uuid":"...","code":"123456"}'
# → { "access_token": "…", "token_type": "Bearer", "expires_at": "…" }

# 3. Use the token
curl https://api.example.com/api/v1/portal/loans \
  -H "Authorization: Bearer <access_token>"

Webhooks

Get pushed events instead of polling. Register endpoints with your company API key and verify every delivery's HMAC signature.

Manage subscriptions on the company API (company API key + X-User-UUID, webhooks permission):

# Register an endpoint — the signing secret is returned ONCE
curl -X POST https://api.example.com/api/v1/webhooks \
  -H "X-API-Key: <company-api-key>" \
  -H "X-Company-UUID: <company-uuid>" \
  -H "X-User-UUID: <your-user-uuid>" \
  -d '{"url":"https://you.example.com/hooks","events":["loan.created","payment.received"]}'
# → 201 { "webhook_uuid":"…", "secret":"whsec_…", "events":[…], "is_active":true }

# GET /api/v1/webhooks · GET/PUT/DELETE /api/v1/webhooks/{uuid} (secret never returned again)
# POST /api/v1/webhooks/{uuid}/rotate-secret  → returns a fresh secret once
# POST /api/v1/webhooks/{uuid}/test           → queues a signed "ping" delivery
# GET  /api/v1/webhooks/{uuid}/deliveries     → delivery history (status, response, attempts)

Events

loan.created · loan.updated · loan.paid_off · payment.received · payment.voided · application.submitted · application.decided · application.withdrawn · rewards.earned · rewards.redeemed · rewards.reversed · held_check.created · held_check.presentment_requested · lease.created · lease.item_returned · lease.item_swapped · lease.item_owned · lease.repossessed · lease.terminated · lease.reinstated

Store-register consumers: held_check.created lists a deferred-presentment check the register should take into custody (custody_state awaiting until it posts POST /api/v1/loans/{uuid}/held-checks/{uuid}/custody), and held_check.presentment_requested asks it to release the paper. The lease.* events carry every merchandise item with its inventory_item_uuid so register inventory can flip on_lease from the agreement's lifecycle; lease.item_swapped names swapped_item_uuid and replacement_item_uuid.

The payment events carry reconciliation fields for POS/register consumers: transaction_uuids (the ledger rows the payment created), payment_method_label, store_uuid, and — when the payment was posted with an external reference — external_source + external_ref (your system's transaction id, echoed back so you can match the event to your own ledger). payment.voided fires once per voided row, including for the payment-group void; correlate on transaction_uuids or external_ref to detect a full reversal. When a payment is posted with posted_by (the register teller who physically took the money), payment.received carries it and each payment.voided carries the original posting's posted_by recovered from the voided row.

Verifying deliveries

Each POST carries X-Webhook-Signature: sha256=<hex> — the HMAC-SHA256 of the raw request body using your endpoint's secret. Recompute it and compare (constant-time) before trusting the payload. The JSON envelope is versioned and carries a unique event_id — dedupe on it, since at-least-once delivery means retries can repeat. All money is integer cents.

Delivery & retries

Respond with any 2xx and process asynchronously on your side. On failure we retry intelligently:

  • Transient failures (timeouts, 5xx, 429) retry with exponential backoff + jitter — ~30s doubling up to a 6h cap — for up to ~3 days before giving up.
  • We honor a Retry-After header on 429/503 (capped).
  • Permanent errors (4xx other than 408/429 — e.g. 400/401/404) are not retried; fix the endpoint and we'll deliver the next event (or use operator Redeliver).
  • If an endpoint fails every recent delivery over a sustained period (not a brief blip) it is auto-suspended (stops receiving new events) so we don't hammer a dead URL; re-enable it via PUT /api/v1/webhooks/{uuid} once it's healthy. In-flight retries continue meanwhile.

Versioning & stability

A clear compatibility contract so you can build with confidence.

  • URL-versioned. Every endpoint lives under /api/v1/. A future incompatible revision ships under a new prefix (/api/v2/) — v1 keeps working.
  • Additive changes are non-breaking and can ship at any time within v1: new endpoints, new response fields, and new enum values. Design your client to ignore unknown JSON fields so these never break you.
  • Breaking changes are announced first. Before an endpoint or field is retired we send the Deprecation and Sunset response headers for a deprecation window, and note it in the changelog below.
  • Consistent shapes. Errors are RFC 7807 application/problem+json with stable machine codes. Every list returns the same envelope: { items, count, limit, offset, has_more } — page by bumping offset until has_more is false.
  • Money is integer cents. All amounts are *_cents int64 — never floats.

Changelog

  • 2026-10-09 — Register custody and counter receipts: POST /api/v1/loans/{uuid}/title-fee-receipt (+ /void) posts the counter receipt for cash-at-origination title fees on a register-owned company (the origination refuses a direct receipt with 422 cash_tender_register_owned and originates with the receipt pending; exactly-once per external_ref); POST .../held-checks/{uuid}/custody and .../custody/release record which register instrument holds a deferred-presentment check (GET .../held-checks?custody=awaiting|held|released); new webhook events held_check.* and lease.* (merchandise carries inventory_item_uuid).
  • 2026-07-21 — Staff API register bridge: optional posted_by operator attribution ({external_id, name}, max 64/128 — 422 invalid_posted_by beyond) on POST /api/v1/loans/{uuid}/payments and the payment-group void. Persisted in each created ledger row's metadata_json, echoed on the payment response, carried on payment.received (and, from the voided rows, on payment.voided), and appended to the maker line on approval-queue rows (API key user <uuid> — posted by <name>) so checkers see the human operator behind the key.
  • 2026-07-20 — Staff API register bridge: POST /api/v1/loans/{uuid}/payments accepts external_source/external_ref/external_display (hard exactly-once backstop per external transaction — duplicates get 409 external_ref_conflict with the original posting's transaction_uuids) and a point-of-receipt store_uuid; typed machine codes on payment errors (loan_not_found, loan_not_payable, invalid_amount, invalid_method, idempotency_conflict, external_ref_conflict, approval_pending, retryable 5xx); payment-group void (POST .../payments/void, idempotent, maker-checker aware) + group status poll (GET .../payments/{key}); teller lookup filters on GET /api/v1/loans (loan_number= exact, q= borrower search); staff GET /api/v1/stores; payment.received/payment.voided webhooks now carry external_source/external_ref.
  • 2026-07-04 — Servicing: authoritative payoff quote, autopay pause/resume, credit-limit-increase requests, transaction disputes, hardship/modification requests, and secure two-way messaging. Per-IP rate limiting on all pre-auth endpoints.
  • 2026-07-03 — Loan documents & per-transaction receipts (PDF), statement PDF download, product catalog and store discovery, and public lead/affiliate application intake with source attribution.
  • 2026-07 — Initial v1: auth & 2FA, profile/addresses/phones, loans/transactions/statements, payment methods, payments, scheduled payments, autopay, notifications, uploaded documents & e-signature requests, applications.

AI assistants (MCP)

VectraLMS is also a Model Context Protocol server. Staff can connect Claude Code, Claude Desktop or claude.ai to read servicing data and manage loan-type configuration under their own login — every call is checked against the user's role and permissions, and every write is audited.

# Claude Code — any machine, including over SSH (--no-browser prints the link to open on your own computer)
claude mcp add --transport http vectralms https://<your-host>/mcp
claude mcp login vectralms --no-browser

# Claude Desktop / claude.ai: Settings → Connectors → Add custom connector → https://<your-host>/mcp

# Scripts and other clients: device-code login, then a refreshing header helper
mcpctl login --server https://<your-host>

Sign-in uses OAuth 2.1 (authorization code with PKCE, dynamic client registration, refresh-token rotation; RFC 8628 device codes for terminals). Discovery: /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource/mcp. Scopes: read, loan_types:write, simulate. Connections can be reviewed and revoked at /admin/settings/mcp.

Ready to build?

Explore every endpoint in the interactive reference, or grab the OpenAPI document to generate a client in your language.