The short version
- Every answer has the same shape. A successful response carries the record, or the list of records, under
data, and anything extra undermeta. Nothing arrives in a surprising place. - Errors tell you what to fix. Each error says what kind of problem it is, which field or setting it refers to, and gives a message that is safe to show to a person. Every error also carries a support id you can quote to us.
- Long lists page. Deals, people, offers and so on come back a page at a time, up to 100 per page, with a pointer to the next page.
- Retries are safe. Anything that sends something, such as a submission or a document request, can be retried without sending twice: the request carries an idempotency key, and a repeat with the same key replays the first answer instead of running again.
- There is a fair-use limit of 300 requests a minute per key, which is far more than a busy brokerage’s integration needs. Notifications are the better way to learn about changes than asking repeatedly.
- Nothing breaks without notice. New fields and endpoints can appear at any time; existing ones are not removed or renamed within the same version. Changes are listed in the changelog.
For developers
Requests
Base URLhttps://api.nextlevelmca.com/v1, HTTPS only, JSON in and out (Content-Type: application/json with any body), Authorization: Bearer <key or token> on every request (see Access and permissions). Creates return 201, everything else 200.
Response envelope
Single object
List
meta is empty for single objects, carries pagination for lists, and sometimes adds context (meta.deal on lender matches, meta.primary_offer_id on offers, meta.warning when a send step was skipped). Even DELETE /v1/webhooks/{id} returns an object ({ "deleted": true, "id": … }), so the envelope never changes shape.
Errors
type is one of seven values (branch on it); code is stable and machine-readable (branch on it, never on message); param names the field, header or scope concerned when there is one; request_id matches the X-Request-Id header every response carries, success or error, and is what support uses to find the trace. Some errors add fields: existing_id on duplicate_business and duplicate_person (the latter also has matched_by: email or phone), retry_after_seconds on a blocked submission retry.
Pagination
limit is the page size (1 to 100, default 25); cursor is the meta.next_cursor from the previous page. meta.has_more is true while there are more rows and meta.next_cursor is null on the last page. Keep filters identical between pages; cursors are opaque, and one the server cannot read is a 400 with code: "invalid_cursor". Lists are newest first (created_at desc, id desc) unless the endpoint says otherwise, and some also report meta.total (lender matches, submissions, deal activity). On /deals, /businesses, /businesses/{id}/deals and /people (unless filtered by business_id, phone or email) the cursor marks the last row you received, so a record created or deleted while you page doesn’t repeat or skip a row. Other lists page by position in the result (an offset), where a change between fetches can shift the window by one; dedupe on id if you need exactly-once processing. Cursors issued earlier keep working.
Idempotency
Send anIdempotency-Key header on any POST to make it safe to retry; use it on everything that sends email or text: submissions, document requests, portal links, webhook tests. UUIDs are a good choice.
- Keys are scoped to the credential, up to 255 characters (longer is
400withcode: "invalid_idempotency_key"). - The first successful response (status and body) is stored for 24 hours. A retry with the same key, method, path and body returns it with the header
Idempotent-Replayed: true; the action does not run again. - The same key with a different body or path is
409withcode: "idempotency_key_reused". While the first request is still running, a concurrent retry gets409withcode: "idempotency_in_progress"; wait a moment and retry. - Only successful responses are stored: after a 4xx or 5xx, the same key runs the request again. Only
POSTreads the header;PATCH,PUTandDELETEare repeatable by nature.
Rate limits
Each credential is limited, per workspace, to 300 requests per minute with a burst of 50 requests per second, both fixed windows. Every response carriesX-RateLimit-Limit (300), X-RateLimit-Remaining and X-RateLimit-Reset (Unix seconds when the minute resets). Exceeding either limit returns 429 with type: "rate_limit_error", code: "rate_limited" and a Retry-After header in seconds (the time to the minute reset, or 1 when the burst tripped). Sleep for Retry-After, then retry; never retry in a tight loop. Limits are not per endpoint.
Timestamps, ids and values
Timestamps are ISO 8601 in UTC with millisecond precision (2026-09-04T14:02:11.000Z). Ids are UUID strings; CRM ids (such as ghl_contact_id) appear as read-only fields where a record is synced. Money is a JSON number in US dollars, percentages run 0 to 100, rates such as factor_rate are decimals (1.32). Phone numbers are returned as stored; GET /v1/people?phone= accepts any format and normalises both sides to E.164 digits before matching.
Versioning
The major version is in the path (/v1). Within v1, changes are additive: new endpoints, optional parameters, response fields and webhook event types can appear without notice, so ignore fields you do not recognise. Breaking changes ship only in a new major version, with the old one kept running through a published sunset date.