MVPConnect API

A small REST API for reading your clients, deals, tasks, appointments, invoices, vendor bills, sales and reports — and for pushing new clients and leads into your account from your own website forms. Built for brokers who want their forms, directly or through an automation tool, to save the person instead of leaving them to be re-keyed by hand.

Authentication

Every request is authenticated with a store-scoped API key. The store owner creates one in Settings → API keys, picks the scopes it needs, and copies the secret (shown once). Send it as a Bearer token:

Authorization: Bearer twc_live_xxxxxxxxxxxxxxxxxxxxx

OpenAPI specification

A machine-readable OpenAPI 3.0 spec is served at https://www.mvpconnect.io/api/v1/openapi.json. Import it into Swagger UI, Postman, Insomnia, or a code generator to scaffold a client.

Conventions

Endpoints

POST/api/v1/customersscope: write:customers

Create or update a client. Dedupes within your store: if you send an email, that alone decides the match (case-insensitively); phone is used only when no email is sent, and is compared digits-to-digits so formatting doesn't matter. Found = update only the fields you send; not found = create. Ideal for website-form intake, directly or via Zapier.

Request body
{
  "firstName": "Jane",
  "lastName": "Doe",
  "email": "jane@example.com",      // email OR phone required
  "phone": "+1 555 123 4567",
  "company": "Acme",
  "addressLine1": "12 Main St",
  "city": "Austin",
  "state": "TX",
  "postalCode": "78701",
  "country": "US",
  "type": "RETAIL",                  // RETAIL | WHOLESALE (default RETAIL)
  "note": "Submitted the contact form",
  "customFields": { "home_resort": "AKV" }  // keys = your custom-field slugs
}
Example
curl -X POST https://www.mvpconnect.io/api/v1/customers \
  -H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "email": "jane@example.com", "firstName": "Jane" }'
Response
{ "customerId": "ckxyz…", "isNew": true }
GET/api/v1/customersscope: read:customers

List the clients in your store. Pass ?q= to search: a full name matches in any order ("Mike Capps", "Capps, Mike"), accents are ignored in both directions, and a phone number matches however it was saved or typed — brackets, dashes, spaces and a leading +1 are ignored on both sides.

Example
curl "https://www.mvpconnect.io/api/v1/customers?q=capps" \
  -H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"
Response
{ "total": 128, "items": [ { "id": "…", "email": "…", "firstName": "…", … } ] }
GET/api/v1/customers/{id}scope: read:customers

Fetch one client by id. The id is resolved within your store; a foreign id returns 404.

Example
curl https://www.mvpconnect.io/api/v1/customers/ckcust123 \
  -H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"
Response
{ "id": "ckcust123", "type": "RETAIL", "firstName": "…", "email": "…", … }
PATCH/api/v1/customers/{id}scope: write:customers

Update a client by id — only the fields you send change. Fires the customer.updated webhook.

Request body
{ "phone": "+1 555 0100", "company": "Acme Travel" }
Example
curl -X PATCH https://www.mvpconnect.io/api/v1/customers/ckcust123 \
  -H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "phone": "+1 555 0100" }'
Response
{ "id": "ckcust123", "phone": "+1 555 0100", … }
GET/api/v1/customers/{id}/activitiesscope: read:crm

The client's unified activity timeline — sales, invoices, messages, calls, tasks, appointments, email-broadcast engagement, deal-stage changes, notes and field changes — newest first. Optional ?categories= (comma-separated: sales, messages, calls, tasks, appointments, deals, marketing, changes) and ?limit=. Only shared inbox messages are exposed.

Example
curl "https://www.mvpconnect.io/api/v1/customers/ckcust123/activities?categories=messages,calls" \
  -H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"
Response
{ "total": 42, "limit": 60, "items": [ { "kind": "messages_day", "at": "…", "channel": "SMS", "inboundCount": 3, "outboundCount": 1, "preview": "…" }, { "kind": "call", "at": "…", "direction": "inbound", "answered": true, "durationSec": 92 }, … ] }
GET/api/v1/customers/{id}/messagesscope: read:conversations

Message history with the client across SMS, email and social, newest first. Optional ?channel= (SMS, EMAIL, FB_MESSENGER, IG_DM), ?limit=, ?offset=. Only shared messages are returned — personal-mailbox mail is never exposed via the API.

Example
curl "https://www.mvpconnect.io/api/v1/customers/ckcust123/messages?channel=SMS" \
  -H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"
Response
{ "total": 128, "limit": 100, "offset": 0, "items": [ { "id": "…", "channel": "SMS", "direction": "inbound", "body": "Is the August use year still available?", "at": "…" }, … ] }
GET/api/v1/customers/{id}/callsscope: read:conversations

Call history with the client (direction, duration, answered, recording, transcription), newest first. Supports ?limit=, ?offset=.

Example
curl "https://www.mvpconnect.io/api/v1/customers/ckcust123/calls" \
  -H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"
Response
{ "total": 6, "limit": 100, "offset": 0, "items": [ { "id": "…", "direction": "outbound", "answered": true, "durationSec": 210, "transcription": "…", "at": "…" } ] }
GET/api/v1/customers/{id}/notesscope: read:crm

The client's CRM notes, newest first. Supports ?limit=, ?offset=.

Example
curl "https://www.mvpconnect.io/api/v1/customers/ckcust123/notes" \
  -H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"
Response
{ "total": 4, "limit": 100, "offset": 0, "items": [ { "id": "…", "body": "Prefers a call after 5pm", "author": "Justin Brown", "createdAt": "…" } ] }
POST/api/v1/leadsscope: write:leads

Create a lead: upserts the client AND drops a card into a pipeline. Use this (instead of /customers) when a form should become a deal somebody has to work.

Request body
{
  "email": "jane@example.com",       // email OR phone required
  "phone": "+1 555 123 4567",
  "firstName": "Jane",
  "lastName": "Doe",
  "pipelineSlug": "buyer-inquiries", // required
  "stageSlug": "new-lead",           // optional (defaults to first stage)
  "title": "150 pts, August UY",
  "itemKind": "DVC_CONTRACT",        // DVC_CONTRACT | NA (omit if not contract-related)
  "estimatedValue": 18500,
  "expectedCloseAt": "2026-09-30",
  "source": "website_form",
  "notes": "Wants Animal Kingdom, flexible on use year"
}
Example
curl -X POST https://www.mvpconnect.io/api/v1/leads \
  -H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "email": "jane@example.com", "pipelineSlug": "buyer-inquiries" }'
Response
{ "opportunityId": "ckabc…", "customerId": "ckxyz…", "customer": { "isNew": true } }
GET/api/v1/opportunitiesscope: read:crm

List deals. Filters: ?status=OPEN|WON|LOST, ?pipelineId=, ?customerId=.

Example
curl "https://www.mvpconnect.io/api/v1/opportunities?status=OPEN" \
  -H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"
Response
{ "total": 8, "limit": 100, "offset": 0, "items": [ { "id": "…", "title": "AKV 150 pts", "status": "OPEN", "pipeline": "Buyers", "stage": "Negotiating", "estimatedValue": 18500, "customerId": "…" } ] }
GET/api/v1/tasksscope: read:crm

List tasks in your store, newest first. Filters: ?status=OPEN|DONE|CANCELLED, ?customerId=. Supports ?limit=, ?offset=.

Example
curl "https://www.mvpconnect.io/api/v1/tasks?status=OPEN" \
  -H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"
Response
{ "total": 12, "limit": 100, "offset": 0, "items": [ { "id": "…", "title": "Call back about the December UY", "status": "OPEN", "dueAt": "…", "customerId": "…" } ] }
GET/api/v1/appointmentsscope: read:crm

List appointments. Filters: ?status=, ?customerId=, ?from=/?to= (ISO bounds on the scheduled time).

Example
curl "https://www.mvpconnect.io/api/v1/appointments?from=2026-09-01" \
  -H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"
Response
{ "total": 3, "limit": 100, "offset": 0, "items": [ { "id": "…", "type": "Buyer consult", "scheduledAt": "…", "status": "BOOKED", "customerId": "…" } ] }
POST/api/v1/invoicesscope: write:invoices

Create a DRAFT invoice. taxRate is a fraction (0.0825 = 8.25%). A customerId, if given, must belong to your store. The response item (and GET) include a payUrl — the hosted link you can send the client.

Request body
{
  "customerId": "ckcust123",         // optional; must be your store's
  "taxRate": 0.0825,                 // fraction (8.25%)
  "dueDate": "2026-09-15",
  "notes": "Thanks for your business",
  "lineItems": [
    { "description": "AKV 150 pts, August use year", "quantity": 1, "unitPrice": 18500 }
  ]
}
Example
curl -X POST https://www.mvpconnect.io/api/v1/invoices \
  -H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "lineItems": [{ "description": "Points rental", "quantity": 1, "unitPrice": 2400 }] }'
Response
{ "id": "ckinv123", "invoiceNumber": "INV-2026-0007", "status": "DRAFT" }
GET/api/v1/invoicesscope: read:invoices

List invoices (filter with ?status=DRAFT|SENT|PAID|VOID). Each item includes its payUrl.

Example
curl "https://www.mvpconnect.io/api/v1/invoices?status=SENT" \
  -H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"
Response
{ "total": 12, "items": [ { "id": "…", "invoiceNumber": "…", "total": 18500, "payUrl": "…", … } ] }
GET/api/v1/invoices/{id}scope: read:invoices

Fetch one invoice by id, including its line items and payUrl. Scoped to your store; a foreign id returns 404.

Example
curl https://www.mvpconnect.io/api/v1/invoices/ckinv123 \
  -H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"
Response
{ "id": "ckinv123", "invoiceNumber": "INV-2026-0007", "status": "SENT", "total": 18500, "payUrl": "…", "lineItems": [ … ] }
POST/api/v1/invoices/{id}/statusscope: write:invoices

Set an invoice's status: DRAFT, SENT, PAID or VOID. Marking PAID fires the invoice.paid webhook.

Request body
{ "status": "PAID" }
Example
curl -X POST https://www.mvpconnect.io/api/v1/invoices/ckinv123/status \
  -H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "status": "SENT" }'
Response
{ "id": "ckinv123", "status": "SENT" }
POST/api/v1/invoices/{id}/sendscope: write:invoices

Send the invoice to the client and mark it SENT. channel "email" (default) attaches a PDF and goes to the on-file email; channel "sms" texts the on-file phone a link to the hosted invoice (needs a connected SMS provider). The recipient is always the on-file client — arbitrary recipients aren't allowed via the API.

Request body
{ "channel": "sms" }   // optional; "email" (default) or "sms"
Example
curl -X POST https://www.mvpconnect.io/api/v1/invoices/ckinv123/send \
  -H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"
Response
{ "id": "ckinv123", "sent": "email" }
GET/api/v1/vendor-billsscope: read:payables

List the bills you owe. Filters: ?status=UNPAID|PAID. Supports ?limit=, ?offset=.

Example
curl "https://www.mvpconnect.io/api/v1/vendor-bills?status=UNPAID" \
  -H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"
Response
{ "total": 5, "limit": 100, "offset": 0, "items": [ { "id": "…", "vendorName": "Title company", "amount": 595, "status": "UNPAID", "dueDate": "…" } ] }
POST/api/v1/vendor-billsscope: write:payables

Record a bill you owe — closing costs, escrow, a commission payable. A linked vendorCustomerId must belong to your store.

Request body
{
  "vendorName": "Title company",
  "amount": 595,
  "status": "UNPAID",                // UNPAID (default) | PAID
  "vendorCustomerId": "ckcust123",   // optional; must be your store's
  "dueDate": "2026-09-30T00:00:00Z",
  "reference": "Closing — AKV 150",
  "notes": "Deed recording + transfer"
}
Example
curl -X POST https://www.mvpconnect.io/api/v1/vendor-bills \
  -H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "vendorName": "Title company", "amount": 595 }'
Response
{ "id": "ckbill123", "vendorName": "Title company", "amount": 595, "status": "UNPAID", … }
GET/api/v1/vendor-bills/{id}scope: read:payables

Fetch one bill by id. Scoped to your store; a foreign id returns 404.

Example
curl https://www.mvpconnect.io/api/v1/vendor-bills/ckbill123 \
  -H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"
Response
{ "id": "ckbill123", "vendorName": "Title company", "amount": 595, "status": "UNPAID", … }
PATCH/api/v1/vendor-bills/{id}scope: write:payables

Update a bill — only the fields you send change. Setting status to PAID stamps paidAt.

Request body
{ "status": "PAID" }
Example
curl -X PATCH https://www.mvpconnect.io/api/v1/vendor-bills/ckbill123 \
  -H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "status": "PAID" }'
Response
{ "id": "ckbill123", "status": "PAID", "paidAt": "…", … }
GET/api/v1/salesscope: read:sales

Read-only sales feed. ?updated_since= is the incremental cursor: it returns sales whose updatedAt is at or after that timestamp, ordered updatedAt ascending, so a poller catches new sales and edits/refunds to older ones. ?from= and ?to= filter the business date (saleDate) for backfills. Money is in major units; timestamps are ISO-8601 UTC.

Example
curl "https://www.mvpconnect.io/api/v1/sales?updated_since=2026-08-01T00:00:00Z" \
  -H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"
Response
{ "total": 87, "limit": 100, "offset": 0, "items": [ { "id": "…", "saleDate": "…", "updatedAt": "…", "status": "completed", "currency": "USD", "total": 18500, "refundedAmount": 0, … } ] }
GET/api/v1/reports/{kind}scope: read:reports

Reporting rollups as JSON, always scoped to your store. kinds: sales, sales-tax, payments, margin-by-brand, fees, receivables, payables. Common filters ?from= and ?to= (dates); sales also takes ?paymentMethod= ?brand= ?soldBy= ?leadSource= ?dealType=; margin-by-brand takes ?sort=; fees takes ?scope=sold|all. An unknown kind returns 400 listing the ones that exist.

Example
curl "https://www.mvpconnect.io/api/v1/reports/receivables" \
  -H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"
Response
{ "kind": "receivables", "count": 7, "rows": [ { "source": "invoice", "invoice": "INV-2026-0007", "customer": "Jane Doe", "issueDate": "2026-08-01", "dueDate": "2026-09-15", "total": 18500, "amountPaid": 4000, "balance": 14500, "status": "SENT" } ] }

Webhooks

Add an HTTPS endpoint under Settings → Webhooksand we'll POST a JSON event to it when something happens in your store. Subscribe to specific events or all of them. Current events: customer.created, customer.updated, sale.created, invoice.created, invoice.paid, message.received, appointment.booked, opportunity.stage_changed.

Payload
{
  "id": "evt_9f2c…",
  "type": "sale.created",
  "createdAt": "2026-08-04T18:05:00.000Z",
  "storeId": "ckstore…",
  "data": { "id": "cksale…", "salePrice": 18500, "customerId": "ckcust…", … }
}

Each request includes X-MVPConnect-Event and a signature header X-MVPConnect-Signature: t=<unix>,v1=<hmac>. The HMAC is SHA256(secret, "<t>.<raw body>") using your endpoint's signing secret (shown once when you create it). Verify it and reject timestamps older than ~5 minutes:

import crypto from "crypto";

function verify(secret, header, rawBody) {
  const parts = Object.fromEntries(
    header.split(",").map((kv) => kv.split("=").map((s) => s.trim()))
  );
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  const ok =
    expected.length === parts.v1.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return ok && fresh;
}

Respond 2xx to acknowledge. Non-2xx (or a timeout) is retried with backoff for several attempts; deliveries and their status are visible under Settings → Webhooks, where you can also re-send.

Wiring a website form (Zapier)

  1. Trigger on your form (a form submission, a Catch Hook, etc.).
  2. Action: Webhooks by Zapier → POST to https://www.mvpconnect.io/api/v1/customers (payload type json) — or /api/v1/leads if the submission should also open a deal.
  3. Add headers Authorization: Bearer twc_live_… and Content-Type: application/json.
  4. Map your form fields to the body fields above, then test.