This API cannot be used to bypass the compliance controls in the product, and that is enforced structurally rather than by a check somebody has to remember: POST /api/v1/messages calls the same send functions the composer and the AI call, so the pre-send gate runs. There is no second code path.
What runs on every send
- Account status — a suspended workspace sends nothing on any channel.
- Suppression / opt-out — STOP means STOP, on replies too.
- A2P registration (SMS) — the sending number must be carrier-approved.
- Quiet hours (proactive SMS) — the recipient's local time, resolved from their area code. If we
cannot resolve a timezone we fail closed.
- Consent (proactive SMS) — a non-expired consent artifact for that number, or the send is
refused. This is TCPA, and it is the one that catches importers by surprise.
Email is opt-out-based: the hard gate is the suppression list.
What a refusal looks like
{
"error": {
"code": "compliance_blocked",
"message": "Blocked (no_consent): No valid consent on file for +1••••••0142",
"request_id": "req_…",
"details": { "message_id": "clx…", "status": "FAILED", "recorded": true }
}
}
422, not a cheerful 201. recorded: true means the attempt exists in the workspace's own inbox as a refused message — it is on file, not lost, so do not retry it in a loop.
Creating leads
POST /api/v1/leads without a consent object creates a lead carrying no consent artifact. The response says so:
{ "id": "clx…", "ai_outbound_allowed": false }
The AI will not dial or text that lead until consent is captured. This is a feature. If you have real capture evidence — a form submission with a URL, an IP and a timestamp — pass it:
{
"phone": "+15125550142",
"source": "your-integration",
"consent": {
"captured_at": "2026-08-13T12:00:00Z",
"source_url": "https://yoursite.example/contact",
"ip": "203.0.113.10",
"form_snapshot": { "checkbox": "I agree to be contacted" }
}
}
Only send this if it is true. It is the record that gets produced if anybody asks.
MLS listings
GET /api/v1/listings returns the workspace's own listings, not MLS inventory. MLS data is governed by per-board display rules — attribution, permitted fields, refresh windows — that a general-purpose REST feed cannot honour, because we would have no idea where the data ended up.