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- Keys are tied to one store; the API only ever reads/writes that store's data.
- Keep the key secret — use it from a server or an automation tool (Zapier, Make, a backend). Never expose it in browser/client code.
- Missing/invalid key →
401; valid key without the required scope →403.
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
- Base URL:
https://www.mvpconnect.io(use your MVPConnect domain). - Requests + responses are JSON. POST bodies use
Content-Type: application/json. - Errors return a non-2xx status with
{ "error": "message" }(validation errors add adetailsarray). - Money is in whole currency units (dollars), dates are ISO 8601.
- List endpoints take
?limit=and?offset=and returntotalalongsideitems. - Rate limits: up to 240 requests/minute per IP across the API, and 120 writes/minute per key on the write endpoints. Over the limit returns
429with aRetry-Afterheader — back off and retry.
Endpoints
/api/v1/customersscope: write:customersCreate 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.
{
"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
}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" }'{ "customerId": "ckxyz…", "isNew": true }/api/v1/customersscope: read:customersList 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.
curl "https://www.mvpconnect.io/api/v1/customers?q=capps" \
-H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"{ "total": 128, "items": [ { "id": "…", "email": "…", "firstName": "…", … } ] }/api/v1/customers/{id}scope: read:customersFetch one client by id. The id is resolved within your store; a foreign id returns 404.
curl https://www.mvpconnect.io/api/v1/customers/ckcust123 \
-H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"{ "id": "ckcust123", "type": "RETAIL", "firstName": "…", "email": "…", … }/api/v1/customers/{id}scope: write:customersUpdate a client by id — only the fields you send change. Fires the customer.updated webhook.
{ "phone": "+1 555 0100", "company": "Acme Travel" }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" }'{ "id": "ckcust123", "phone": "+1 555 0100", … }/api/v1/customers/{id}/activitiesscope: read:crmThe 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.
curl "https://www.mvpconnect.io/api/v1/customers/ckcust123/activities?categories=messages,calls" \
-H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"{ "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 }, … ] }/api/v1/customers/{id}/messagesscope: read:conversationsMessage 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.
curl "https://www.mvpconnect.io/api/v1/customers/ckcust123/messages?channel=SMS" \
-H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"{ "total": 128, "limit": 100, "offset": 0, "items": [ { "id": "…", "channel": "SMS", "direction": "inbound", "body": "Is the August use year still available?", "at": "…" }, … ] }/api/v1/customers/{id}/callsscope: read:conversationsCall history with the client (direction, duration, answered, recording, transcription), newest first. Supports ?limit=, ?offset=.
curl "https://www.mvpconnect.io/api/v1/customers/ckcust123/calls" \
-H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"{ "total": 6, "limit": 100, "offset": 0, "items": [ { "id": "…", "direction": "outbound", "answered": true, "durationSec": 210, "transcription": "…", "at": "…" } ] }/api/v1/customers/{id}/notesscope: read:crmThe client's CRM notes, newest first. Supports ?limit=, ?offset=.
curl "https://www.mvpconnect.io/api/v1/customers/ckcust123/notes" \
-H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"{ "total": 4, "limit": 100, "offset": 0, "items": [ { "id": "…", "body": "Prefers a call after 5pm", "author": "Justin Brown", "createdAt": "…" } ] }/api/v1/leadsscope: write:leadsCreate 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.
{
"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"
}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" }'{ "opportunityId": "ckabc…", "customerId": "ckxyz…", "customer": { "isNew": true } }/api/v1/opportunitiesscope: read:crmList deals. Filters: ?status=OPEN|WON|LOST, ?pipelineId=, ?customerId=.
curl "https://www.mvpconnect.io/api/v1/opportunities?status=OPEN" \
-H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"{ "total": 8, "limit": 100, "offset": 0, "items": [ { "id": "…", "title": "AKV 150 pts", "status": "OPEN", "pipeline": "Buyers", "stage": "Negotiating", "estimatedValue": 18500, "customerId": "…" } ] }/api/v1/tasksscope: read:crmList tasks in your store, newest first. Filters: ?status=OPEN|DONE|CANCELLED, ?customerId=. Supports ?limit=, ?offset=.
curl "https://www.mvpconnect.io/api/v1/tasks?status=OPEN" \
-H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"{ "total": 12, "limit": 100, "offset": 0, "items": [ { "id": "…", "title": "Call back about the December UY", "status": "OPEN", "dueAt": "…", "customerId": "…" } ] }/api/v1/appointmentsscope: read:crmList appointments. Filters: ?status=, ?customerId=, ?from=/?to= (ISO bounds on the scheduled time).
curl "https://www.mvpconnect.io/api/v1/appointments?from=2026-09-01" \
-H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"{ "total": 3, "limit": 100, "offset": 0, "items": [ { "id": "…", "type": "Buyer consult", "scheduledAt": "…", "status": "BOOKED", "customerId": "…" } ] }/api/v1/invoicesscope: write:invoicesCreate 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.
{
"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 }
]
}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 }] }'{ "id": "ckinv123", "invoiceNumber": "INV-2026-0007", "status": "DRAFT" }/api/v1/invoicesscope: read:invoicesList invoices (filter with ?status=DRAFT|SENT|PAID|VOID). Each item includes its payUrl.
curl "https://www.mvpconnect.io/api/v1/invoices?status=SENT" \
-H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"{ "total": 12, "items": [ { "id": "…", "invoiceNumber": "…", "total": 18500, "payUrl": "…", … } ] }/api/v1/invoices/{id}scope: read:invoicesFetch one invoice by id, including its line items and payUrl. Scoped to your store; a foreign id returns 404.
curl https://www.mvpconnect.io/api/v1/invoices/ckinv123 \
-H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"{ "id": "ckinv123", "invoiceNumber": "INV-2026-0007", "status": "SENT", "total": 18500, "payUrl": "…", "lineItems": [ … ] }/api/v1/invoices/{id}/statusscope: write:invoicesSet an invoice's status: DRAFT, SENT, PAID or VOID. Marking PAID fires the invoice.paid webhook.
{ "status": "PAID" }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" }'{ "id": "ckinv123", "status": "SENT" }/api/v1/invoices/{id}/sendscope: write:invoicesSend 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.
{ "channel": "sms" } // optional; "email" (default) or "sms"curl -X POST https://www.mvpconnect.io/api/v1/invoices/ckinv123/send \
-H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"{ "id": "ckinv123", "sent": "email" }/api/v1/vendor-billsscope: read:payablesList the bills you owe. Filters: ?status=UNPAID|PAID. Supports ?limit=, ?offset=.
curl "https://www.mvpconnect.io/api/v1/vendor-bills?status=UNPAID" \
-H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"{ "total": 5, "limit": 100, "offset": 0, "items": [ { "id": "…", "vendorName": "Title company", "amount": 595, "status": "UNPAID", "dueDate": "…" } ] }/api/v1/vendor-billsscope: write:payablesRecord a bill you owe — closing costs, escrow, a commission payable. A linked vendorCustomerId must belong to your store.
{
"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"
}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 }'{ "id": "ckbill123", "vendorName": "Title company", "amount": 595, "status": "UNPAID", … }/api/v1/vendor-bills/{id}scope: read:payablesFetch one bill by id. Scoped to your store; a foreign id returns 404.
curl https://www.mvpconnect.io/api/v1/vendor-bills/ckbill123 \
-H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"{ "id": "ckbill123", "vendorName": "Title company", "amount": 595, "status": "UNPAID", … }/api/v1/vendor-bills/{id}scope: write:payablesUpdate a bill — only the fields you send change. Setting status to PAID stamps paidAt.
{ "status": "PAID" }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" }'{ "id": "ckbill123", "status": "PAID", "paidAt": "…", … }/api/v1/salesscope: read:salesRead-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.
curl "https://www.mvpconnect.io/api/v1/sales?updated_since=2026-08-01T00:00:00Z" \
-H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"{ "total": 87, "limit": 100, "offset": 0, "items": [ { "id": "…", "saleDate": "…", "updatedAt": "…", "status": "completed", "currency": "USD", "total": 18500, "refundedAmount": 0, … } ] }/api/v1/reports/{kind}scope: read:reportsReporting 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.
curl "https://www.mvpconnect.io/api/v1/reports/receivables" \
-H "Authorization: Bearer twc_live_xxxxxxxxxxxxxxxx"{ "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.
{
"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)
- Trigger on your form (a form submission, a Catch Hook, etc.).
- Action: Webhooks by Zapier → POST to
https://www.mvpconnect.io/api/v1/customers(payload typejson) — or/api/v1/leadsif the submission should also open a deal. - Add headers
Authorization: Bearer twc_live_…andContent-Type: application/json. - Map your form fields to the body fields above, then test.