The EZPZ API
Read and write your agency's book with the same permissions your team has.
- Make a key in Studio › API & webhooks.
- A key acts as the roles you give it. It never reaches more than the person who made it.
- Included in the Agency plan.
Authentication
Send your key as a bearer token on every request.
Authorization: Bearer ezpz_live_…We show the secret once, when you make the key. We keep only a hash of it, so copy it then. Revoke a key in Studio at any time; it stops working at once.
Pagination
List endpoints take limit (default 50, max 100) and cursor. Pass the next_cursor you got back to read the next page. It is null on the last page.
{
"object": "list",
"data": [ … ],
"next_cursor": "…" | null
}Errors
A failed request answers with a status code and one stable code you can branch on.
{ "error": "<code>" }| Code | Status | What it means |
|---|---|---|
unauthenticated | 401 | No key, a malformed key, or a revoked one. |
forbidden | 403 | The key is real but its roles do not allow this, or the record is outside its reach. |
plan_required | 403 | The agency is not on a plan that includes the API. |
not_found | 404 | No such record, or one the key cannot see. |
validation | 422 · 400 | The body or query did not pass. 400 when an id is malformed. |
rate_limited | 429 | Too many requests. Wait the seconds in the Retry-After header. |
db_error | 500 | Our fault. Retry with backoff. |
Rate limits
Each key gets 600 requests a minute and 50,000 a day. Past either, you get 429 rate_limited with a Retry-After header in seconds.
Ids carry a prefix (ld_…, ag_…). Money is integer cents. Times are ISO 8601 in UTC; dates are YYYY-MM-DD. A key sees only the records its roles reach.
lead
A person an agent is working: contact facts, where they came from, and where they stand.
- GET /api/v1/leadsList leads, newest first.
- GET /api/v1/leads/{id}Get one lead.
- POST /api/v1/leadsCreate a lead.
- PATCH /api/v1/leads/{id}Change its agency fields (
custom), its stage (stage_id) or its notes. - POST /api/v1/leads/{id}/notesAdd a note.
- POST /api/v1/leads/{id}/appointmentsBook an appointment.
custom holds your agency's own fields on this object, keyed by field key. A field that looks like health data is never sent.
| Field | Type | Notes |
|---|---|---|
id | string | The lead id. |
owner_id | agent id | The agent who owns the record (an agent id). |
first_name | string or null | First name. |
last_name | string or null | Last name. |
email | string or null | Email address. |
phone | string or null | Phone number, E.164 where known. |
state | string or null | Two-letter US state. |
zip | string or null | ZIP code. |
age | integer or null | Age in years, as given. |
disposition | string | The fixed disposition (new_lead, called, booked, sold, …). Canonical; stage is the agency's word for it. |
stage | object or null | The agency stage it shows: {id, key, label}, or null for the fixed label. |
source | string | Where the lead came from (facebook, transferred, mail_in, api, …). |
product_type | string | The product line the lead asked about. |
tags | array | Tags, as strings. |
notes | string or null | The agent's notes on the lead. |
score | integer or null | Lead score, 0–100. |
score_warmth_label | string or null | The warmth word beside the score. |
call_count | integer | The current call streak. |
dial_attempts | integer | Every dial ever placed to the lead. |
voicemails_left | integer | Voicemails left. |
callback_at | timestamp or null | A scheduled callback. |
closed_at | timestamp or null | When the lead was closed. |
last_touched_at | timestamp | The last time anyone acted on the lead. |
last_dialed_at | timestamp or null | The last dial. |
last_inbound_at | timestamp or null | The last inbound text or call. |
cost_cents | integer (cents) | What the lead cost (cents). |
campaign_id | string or null | The ad campaign it came from (an EZPZ campaign id). |
origin_channel | string or null | The channel it first arrived through. |
referred_by_lead_id | lead id or null | The lead who referred this one. |
custom | object | Agency field values, keyed by field key. Health-shaped fields are never included. |
created_at | timestamp | When the record was created. |
updated_at | timestamp | When the record last changed. |
curl https://www.buildezpz.com/api/v1/leads/ld_2b3c4d5e-0000-4000-8000-00000001ead1 \
-H "Authorization: Bearer ezpz_live_…"{
"object": "lead",
"id": "ld_2b3c4d5e-0000-4000-8000-00000001ead1",
"owner_id": "ag_1a2b3c4d-0000-4000-8000-0000000a6e01",
"first_name": "Dana",
"last_name": "Ruiz",
"email": "dana.ruiz@example.com",
"phone": "+15555550142",
"state": "TX",
"zip": "78701",
"age": 54,
"disposition": "booked",
"stage": {
"id": "st_92a3b4c5-0000-4000-8000-0000000057a6",
"key": "appointment_set",
"label": "Appointment set"
},
"source": "facebook",
"product_type": "final_expense",
"tags": [
"evenings"
],
"notes": "Prefers a call after 6 pm.",
"score": 82,
"score_warmth_label": "Warm",
"call_count": 2,
"dial_attempts": 5,
"voicemails_left": 1,
"callback_at": null,
"closed_at": null,
"last_touched_at": "2026-09-30T18:04:11Z",
"last_dialed_at": "2026-09-30T17:58:40Z",
"last_inbound_at": "2026-09-30T18:02:09Z",
"cost_cents": 2400,
"campaign_id": "c5d6e7f8-0000-4000-8000-0000000ca3e1",
"origin_channel": "facebook",
"referred_by_lead_id": null,
"custom": {
"coverage_goal": "burial"
},
"created_at": "2026-09-28T15:20:00Z",
"updated_at": "2026-09-30T18:04:11Z"
}Never exposed: sensitive — date_of_birth, raw_payload; vendor ids — meta_*, gclid, base44_id; internal — lead_session_id, custody_*, copied_from_lead_id, listing_id, purchased_from_listing_id, pool_listed_at, resale_owner_id, purge_hold_at, scrub_*, last_inbound_preview.
policy
A sale (or a policy in motion) on a lead: carrier, product, premium and where it stands.
- GET /api/v1/policiesList policies.
- GET /api/v1/policies/{id}Get one policy.
custom holds your agency's own fields on this object, keyed by field key. A field that looks like health data is never sent.
| Field | Type | Notes |
|---|---|---|
id | string | The policy id. |
lead_id | lead id | The lead it was sold to. |
owner_id | agent id | The agent who owns the record (an agent id). |
ap_cents | integer (cents) | Annual premium (cents). |
advanced_pay_cents | integer (cents) | Advance paid (cents). |
commission_cents | integer (cents) | Commission (cents). |
commission_pct | number or null | Commission percent (two decimals). |
face_amount_cents | integer (cents) or null | Face amount (cents). |
carrier_name | string or null | Carrier. |
carrier_product | string or null | Carrier product. |
policy_number | string or null | Policy number. |
policy_stage | string | Where the policy stands (submitted, issued, paid, lapsed, …). |
policy_stage_at | timestamp | When it reached that stage. |
close_date | date | The sale date. |
effective_date | date or null | The policy effective date. |
sold_reason | string or null | Why it sold, as noted. |
policy_note | string or null | The agent's note on the policy. |
client_first_name | string or null | The insured's first name, when not the lead. |
client_last_name | string or null | The insured's last name, when not the lead. |
call_id | call id or null | The call it closed on. |
custom | object | Agency policy field values, keyed by field key. Health-shaped fields are never included. |
created_at | timestamp | When the record was created. |
updated_at | timestamp | When the record last changed. |
curl https://www.buildezpz.com/api/v1/policies/pl_3c4d5e6f-0000-4000-8000-0000000b0011 \
-H "Authorization: Bearer ezpz_live_…"{
"object": "policy",
"id": "pl_3c4d5e6f-0000-4000-8000-0000000b0011",
"lead_id": "ld_2b3c4d5e-0000-4000-8000-00000001ead1",
"owner_id": "ag_1a2b3c4d-0000-4000-8000-0000000a6e01",
"ap_cents": 102000,
"advanced_pay_cents": 76500,
"commission_cents": 102000,
"commission_pct": 100,
"face_amount_cents": 1500000,
"carrier_name": "Example Mutual",
"carrier_product": "Level Benefit Whole Life",
"policy_number": "EX-0001245",
"policy_stage": "issued",
"policy_stage_at": "2026-10-01T14:00:00Z",
"close_date": "2026-09-30",
"effective_date": "2026-10-15",
"sold_reason": "Wanted burial covered for her kids.",
"policy_note": null,
"client_first_name": null,
"client_last_name": null,
"call_id": "cl_5e6f7081-0000-4000-8000-0000000ca111",
"custom": {},
"created_at": "2026-09-30T19:10:00Z",
"updated_at": "2026-10-01T14:00:00Z"
}Never exposed: sensitive — client_dob; internal — closed_by, stage_nudge_*, contact_idx.
appointment
A booked meeting with a lead and what came of it.
- GET /api/v1/appointmentsList appointments.
- GET /api/v1/appointments/{id}Get one appointment.
| Field | Type | Notes |
|---|---|---|
id | string | The appointment id. |
lead_id | lead id | The lead it is with. |
owner_id | agent id | The agent who owns the record (an agent id). |
starts_at | timestamp | Start. |
ends_at | timestamp | End. |
timezone | string or null | The IANA time zone it was booked in. |
status | scheduled | cancelled | completed | no_show | Its state. |
outcome | string or null | What happened at the meeting. |
title | string or null | Title. |
notes | string or null | Notes. |
source | string or null | How it was booked. |
created_by | agent id or null | Who booked it (an agent id). |
created_at | timestamp | When the record was created. |
updated_at | timestamp | When the record last changed. |
curl https://www.buildezpz.com/api/v1/appointments/ap_4d5e6f70-0000-4000-8000-0000000a1101 \
-H "Authorization: Bearer ezpz_live_…"{
"object": "appointment",
"id": "ap_4d5e6f70-0000-4000-8000-0000000a1101",
"lead_id": "ld_2b3c4d5e-0000-4000-8000-00000001ead1",
"owner_id": "ag_1a2b3c4d-0000-4000-8000-0000000a6e01",
"starts_at": "2026-10-01T23:00:00Z",
"ends_at": "2026-10-01T23:30:00Z",
"timezone": "America/Chicago",
"status": "scheduled",
"outcome": null,
"title": "Dana Ruiz — coverage review",
"notes": null,
"source": "echo",
"created_by": "ag_1a2b3c4d-0000-4000-8000-0000000a6e01",
"created_at": "2026-09-30T18:03:00Z",
"updated_at": "2026-09-30T18:03:00Z"
}Never exposed: vendor ids — google_*, html_link, meet_link.
call
A phone call, inbound or outbound, and how it went.
- GET /api/v1/callsList calls.
- GET /api/v1/calls/{id}Get one call.
| Field | Type | Notes |
|---|---|---|
id | string | The call id. |
lead_id | lead id or null | The lead on the call, when known. |
owner_id | agent id | The agent who owns the record (an agent id). |
direction | string | inbound or outbound. |
status | string | The call state (ringing, in_progress, completed, no_answer, …). |
started_at | timestamp or null | When it started. |
answered_at | timestamp or null | When it was answered. |
ended_at | timestamp or null | When it ended. |
duration_sec | integer or null | Talk time in seconds. |
voicemail | boolean | A voicemail was left or taken. |
from_number | string | From. |
to_number | string | To. |
caller_name | string or null | Caller name, when known. |
placed_by_actor_id | agent id or null | Who placed it, when not the owner (an agent id). |
answered_by | string or null | Who picked up (human or machine), when detected. |
recording_duration_sec | integer or null | Recording length in seconds. |
has_recording | boolean | A recording exists and has not been purged. |
transcription | string or null | The call transcript, when one was made. |
recording_link | string or null | A stable link to the audio while it exists: GET it with a key holding calls.listen for the call's owner and it answers 302 to a fresh short-lived file link (404 once the audio is gone). Null without calls.listen, or when there is no recording. |
created_at | timestamp | When the record was created. |
curl https://www.buildezpz.com/api/v1/calls/cl_5e6f7081-0000-4000-8000-0000000ca111 \
-H "Authorization: Bearer ezpz_live_…"{
"object": "call",
"id": "cl_5e6f7081-0000-4000-8000-0000000ca111",
"lead_id": "ld_2b3c4d5e-0000-4000-8000-00000001ead1",
"owner_id": "ag_1a2b3c4d-0000-4000-8000-0000000a6e01",
"direction": "outbound",
"status": "completed",
"started_at": "2026-09-30T17:58:40Z",
"answered_at": "2026-09-30T17:58:52Z",
"ended_at": "2026-09-30T18:05:44Z",
"duration_sec": 412,
"voicemail": false,
"from_number": "+15555550100",
"to_number": "+15555550142",
"caller_name": null,
"placed_by_actor_id": null,
"answered_by": "human",
"recording_duration_sec": 405,
"has_recording": true,
"transcription": null,
"recording_link": null,
"created_at": "2026-09-30T17:58:40Z"
}Never exposed: vendor ids — provider*, twilio_*, *_sid, agent_leg_call_control_id, stir_*, caller_line_type, hangup_cause, recording_url; internal — quality_flags, error_*, *_alerted_at, *_started_at.
message
A text message to or from a lead.
- GET /api/v1/messagesList text messages.
| Field | Type | Notes |
|---|---|---|
id | string | The message id. |
lead_id | lead id or null | The lead, when known. |
owner_id | agent id | The agent who owns the record (an agent id). |
direction | string | inbound or outbound. |
body | string | The text. |
status | string | Delivery state. |
from_number | string | From. |
to_number | string | To. |
num_media | integer or null | Attached media count. |
send_origin | string or null | What sent it (manual, automation, …). |
sent_by_kind | string or null | owner, delegate or echo. |
sent_by_actor_id | agent id or null | Who sent it, when not the owner (an agent id). |
read_at | timestamp or null | When the agent read it. |
created_at | timestamp | When the record was created. |
curl "https://www.buildezpz.com/api/v1/messages?limit=1" \
-H "Authorization: Bearer ezpz_live_…"{
"object": "list",
"data": [
{
"object": "message",
"id": "ms_6f708192-0000-4000-8000-0000000e5501",
"lead_id": "ld_2b3c4d5e-0000-4000-8000-00000001ead1",
"owner_id": "ag_1a2b3c4d-0000-4000-8000-0000000a6e01",
"direction": "inbound",
"body": "Tuesday at 6 works for me.",
"status": "received",
"from_number": "+15555550142",
"to_number": "+15555550100",
"num_media": 0,
"send_origin": null,
"sent_by_kind": null,
"sent_by_actor_id": null,
"read_at": "2026-09-30T18:02:30Z",
"created_at": "2026-09-30T18:02:09Z"
}
],
"next_cursor": null
}Never exposed: vendor ids — provider*, twilio_message_sid, subaccount_sid; internal — error_*, opt_out_*, send_class; billing — num_segments.
agent
A person on the agency's team.
- GET /api/v1/agentsList the agents your key can see.
- GET /api/v1/agents/{id}Get one agent.
custom holds your agency's own fields on this object, keyed by field key. A field that looks like health data is never sent.
| Field | Type | Notes |
|---|---|---|
id | string | The agent id. |
full_name | string or null | Full name. |
display_name | string or null | The name they go by. |
business_name | string or null | Their business name. |
email | string | Email. |
phone | string or null | Phone. |
agency_id | agency id or null | Their agency. |
role | agency_owner | agency_admin | agency_assistant | member or null | Their seat in the agency; null when they hold none. |
manager_id | agent id or null | Their upline manager. |
npn | string or null | National Producer Number. |
timezone | string or null | IANA time zone. |
work_state | string or null | Home work state. |
avatar_url | string or null | Avatar image URL. |
status | string | Account status. |
custom | object | Agency agent field values, keyed by field key. Health-shaped fields are never included. |
created_at | timestamp | When the record was created. |
curl https://www.buildezpz.com/api/v1/agents/ag_1a2b3c4d-0000-4000-8000-0000000a6e01 \
-H "Authorization: Bearer ezpz_live_…"{
"object": "agent",
"id": "ag_1a2b3c4d-0000-4000-8000-0000000a6e01",
"full_name": "Sam Okafor",
"display_name": "Sam",
"business_name": "Okafor Family Coverage",
"email": "sam.okafor@example.com",
"phone": "+15555550100",
"agency_id": "ay_0c1d2e3f-0000-4000-8000-00000000a9e1",
"role": "member",
"manager_id": "ag_1a2b3c4d-0000-4000-8000-0000000a6e02",
"npn": "99990001",
"timezone": "America/Chicago",
"work_state": "TX",
"avatar_url": null,
"status": "active",
"custom": {
"contract_level": 90
},
"created_at": "2026-01-12T16:00:00Z"
}Never exposed: sign-in secrets — client_data_pin_hash, phone_e164_*; vendor ids — base44_id, telephony_provider; internal — referral_code*, pz_*, va_of_user_id, paused_*, scrub_*, *_override*, sms_*, email_*, setup_progress, discovery_questions, dismissed_announcements; billing — admin_*, access_granted_by_admin, comp_source, lead_ceiling.
role
A custom role the agency defined in Studio, and the switches it grants.
- GET /api/v1/rolesList your agency's custom roles.
| Field | Type | Notes |
|---|---|---|
id | string | The role id. |
agency_id | agency id | The defining agency. |
name | string | Name. |
description | string | Description. |
switches | object | Switch key → the reaches it grants (a data switch) or true (an agency-wide switch). |
push_mode | offered | suggested | extendable | locked or null | Pushed to the agencies below: offered, suggested, extendable or locked; null = not pushed. |
status | draft | live | retired | draft, live or retired. |
created_at | timestamp | When the record was created. |
curl "https://www.buildezpz.com/api/v1/roles?limit=1" \
-H "Authorization: Bearer ezpz_live_…"{
"object": "list",
"data": [
{
"object": "role",
"id": "rl_708192a3-0000-4000-8000-0000000501e1",
"agency_id": "ay_0c1d2e3f-0000-4000-8000-00000000a9e1",
"name": "Closer",
"description": "Works their own leads and sees the team board.",
"switches": {
"leads.view": [
"own"
],
"leads.edit": [
"own"
],
"calls.dial": [
"own"
],
"team.production": [
"team"
],
"build.reports": true
},
"push_mode": null,
"status": "live",
"created_at": "2026-09-01T15:00:00Z"
}
],
"next_cursor": null
}Never exposed: internal — grants, created_by.
field
A custom field the agency defined on leads, policies or agents.
- GET /api/v1/fieldsList your agency's custom fields.
| Field | Type | Notes |
|---|---|---|
id | string | The field id. |
agency_id | agency id | The defining agency. |
object | lead | policy | agent | What it is on. |
key | string | The key its values appear under in custom. |
label | string | Label. |
type | string | Field type (text, number, date, select, multi_select, checkbox, …). |
options | array | Choices {value, label} for a pick field. |
required | boolean | Required. |
help | string | Help text. |
position | integer | Display order. |
push_mode | offered | suggested | extendable | locked or null | Pushed to the agencies below; null = not pushed. |
status | draft | live | retired | draft, live or retired. |
created_at | timestamp | When the record was created. |
curl "https://www.buildezpz.com/api/v1/fields?limit=1" \
-H "Authorization: Bearer ezpz_live_…"{
"object": "list",
"data": [
{
"object": "lead",
"id": "fd_8192a3b4-0000-4000-8000-0000000f1e1d",
"agency_id": "ay_0c1d2e3f-0000-4000-8000-00000000a9e1",
"key": "coverage_goal",
"label": "Coverage goal",
"type": "select",
"options": [
{
"value": "burial",
"label": "Burial"
},
{
"value": "income",
"label": "Income"
}
],
"required": false,
"help": "What the client wants the policy to do.",
"position": 1,
"push_mode": "extendable",
"status": "live",
"created_at": "2026-09-01T15:00:00Z"
}
],
"next_cursor": null
}Never exposed: internal — created_by.
stage
An agency stage: the agency's own word for one of the fixed dispositions.
- GET /api/v1/stagesList your agency's stages.
| Field | Type | Notes |
|---|---|---|
id | string | The stage id. |
agency_id | agency id | The defining agency. |
key | string | Key. |
label | string | Label. |
maps_to | string | The fixed disposition it counts as. |
tone | string or null | Dot colour. |
position | integer | Display order. |
push_mode | offered | suggested | extendable | locked or null | Pushed to the agencies below; null = not pushed. |
status | draft | live | retired | draft, live or retired. |
created_at | timestamp | When the record was created. |
curl "https://www.buildezpz.com/api/v1/stages?limit=1" \
-H "Authorization: Bearer ezpz_live_…"{
"object": "list",
"data": [
{
"object": "stage",
"id": "st_92a3b4c5-0000-4000-8000-0000000057a6",
"agency_id": "ay_0c1d2e3f-0000-4000-8000-00000000a9e1",
"key": "appointment_set",
"label": "Appointment set",
"maps_to": "booked",
"tone": "sea",
"position": 3,
"push_mode": null,
"status": "live",
"created_at": "2026-09-01T15:00:00Z"
}
],
"next_cursor": null
}Never exposed: internal — created_by.
table
A custom table that applies in your agency: its own, or one pushed to it. Only live tables are listed.
- GET /api/v1/tablesList the custom tables live in your agency, each with its fields.
- GET /api/v1/tables/{id}Get one table.
| Field | Type | Notes |
|---|---|---|
id | string | The table id. |
agency_id | agency id | The defining agency. |
key | string | Key. |
name | string | Name. |
record_noun | string | What one record is called. |
icon | string or null | Icon name. |
shared | boolean | Shared across the agency. |
on_lead | boolean | Shown on the lead card. |
status | draft | live | retired | draft, live or retired. |
fields | array | Its fields [{key, label, type, options, required}] — the keys a record's data uses. A link field is read-only here (its values are links). Health-shaped fields are never included. |
curl https://www.buildezpz.com/api/v1/tables/tb_a3b4c5d6-0000-4000-8000-00000007ab1e \
-H "Authorization: Bearer ezpz_live_…"{
"object": "table",
"id": "tb_a3b4c5d6-0000-4000-8000-00000007ab1e",
"agency_id": "ay_0c1d2e3f-0000-4000-8000-00000000a9e1",
"key": "referrals",
"name": "Referrals",
"record_noun": "referral",
"icon": null,
"shared": true,
"on_lead": true,
"status": "live",
"fields": [
{
"key": "relationship",
"label": "Relationship",
"type": "text",
"options": [],
"required": true
}
]
}Never exposed: internal — created_by.
record
A record in a custom table. owner_id is null on an agency-shared table.
- GET /api/v1/records?table_id=&owner_id=&updated_since=List one table's records, newest first.
table_idis required. A list past your agency's rows-per-filter limit (20,000 unless raised) answers 422too_many_rows: narrow it byowner_idorupdated_since. - GET /api/v1/records/{id}Get one record.
- POST /api/v1/recordsCreate a record:
{table_id, owner_id?, data},datakeyed by field key. Withoutowner_idit belongs to your key's default owner (a shared table's records have none). Link fields are read-only. - PATCH /api/v1/records/{id}Change some of its values:
{data}, keyed by field key;nullclears one.
data holds the table's fields, keyed by field key (the table's fields list them). A field that looks like health data is never sent.
| Field | Type | Notes |
|---|---|---|
id | string | The record id. |
table_id | table id | Its table. |
agency_id | agency id | The agency. |
owner_id | agent id or null | The agent who owns the record (an agent id). |
title | string | Title: the value of the table's first text field. |
data | object | Values keyed by the table's field KEY (not the definition id). Health-shaped fields are never included. |
links | array | What it links to [{kind, id}] (kind: record, lead or policy) — only targets your key can read. Read-only in v1. |
created_at | timestamp | When the record was created. |
updated_at | timestamp | When the record last changed. |
curl https://www.buildezpz.com/api/v1/records/rc_b4c5d6e7-0000-4000-8000-0000000ec0d1 \
-H "Authorization: Bearer ezpz_live_…"{
"object": "record",
"id": "rc_b4c5d6e7-0000-4000-8000-0000000ec0d1",
"table_id": "tb_a3b4c5d6-0000-4000-8000-00000007ab1e",
"agency_id": "ay_0c1d2e3f-0000-4000-8000-00000000a9e1",
"owner_id": "ag_1a2b3c4d-0000-4000-8000-0000000a6e01",
"title": "Referral from Dana Ruiz",
"data": {
"relationship": "Neighbor"
},
"links": [
{
"kind": "lead",
"id": "ld_2b3c4d5e-0000-4000-8000-00000001ead1"
}
],
"created_at": "2026-09-30T19:20:00Z",
"updated_at": "2026-09-30T19:20:00Z"
}Never exposed: nothing beyond the rule — a column that is not a field above is never sent.
agency
The agency itself and where it sits in the tree.
- GET /api/v1/agencyGet your agency.
| Field | Type | Notes |
|---|---|---|
id | string | The agency id. |
name | string | Name. |
parent_id | agency id or null | The agency above it, if any. |
tier | string | Agency kind (portal, enterprise, external, spinoff). |
created_at | timestamp | When the record was created. |
curl https://www.buildezpz.com/api/v1/agency \
-H "Authorization: Bearer ezpz_live_…"{
"object": "agency",
"id": "ay_0c1d2e3f-0000-4000-8000-00000000a9e1",
"name": "Example Agency Group",
"parent_id": null,
"tier": "portal",
"created_at": "2025-11-03T15:00:00Z"
}Never exposed: vendor ids — telnyx_*; internal — owner_email; billing — monthly_price, portal_comped_*, stripe_*; platform costs — *pricing*.
kpi_day
One agent's numbers for one day, computed from the ledgers. Id kd_<agent uuid>_<YYYY-MM-DD>. Totals: not subject to the consent rule, like Team production.
- GET /api/v1/kpi_days?from=&to=&agent_id=One row per agent per day between
fromandto(dates, at most 93 days).agent_idis optional.
| Field | Type | Notes |
|---|---|---|
id | string | The day id: kd_<agent uuid>_<date>. |
agent_id | agent id | The agent. |
date | date | The day, in the agent's time zone. |
dials | integer | Dials. |
contacts | integer | Contacts. |
appointments_set | integer | Appointments booked. |
appointments_held | integer | Appointments held. |
presentations | integer | Presentations. |
closes | integer | Closes. |
texts_sent | integer | Texts sent. |
talk_seconds | integer | Talk time (seconds). |
leads_received | integer | New leads received. |
ap_cents | integer (cents) | Annual premium closed (cents). |
spend_cents | integer (cents) | Ad and lead spend (cents). |
lead_cost_cents | integer (cents) | The summed cost of the leads received (cents). |
cost_per_lead_cents | integer (cents) or null | DERIVED: spend_cents ÷ leads_received, rounded; null when no leads. |
cost_per_close_cents | integer (cents) or null | DERIVED: spend_cents ÷ closes, rounded; null when no closes. |
curl "https://www.buildezpz.com/api/v1/kpi_days?from=2026-09-30&to=2026-09-30&agent_id=ag_1a2b3c4d-0000-4000-8000-0000000a6e01" \
-H "Authorization: Bearer ezpz_live_…"{
"object": "list",
"data": [
{
"object": "kpi_day",
"id": "kd_1a2b3c4d-0000-4000-8000-0000000a6e01_2026-09-30",
"agent_id": "ag_1a2b3c4d-0000-4000-8000-0000000a6e01",
"date": "2026-09-30",
"dials": 118,
"contacts": 31,
"appointments_set": 6,
"appointments_held": 4,
"presentations": 4,
"closes": 2,
"texts_sent": 85,
"talk_seconds": 9420,
"leads_received": 14,
"ap_cents": 204000,
"spend_cents": 42000,
"lead_cost_cents": 33600,
"cost_per_lead_cents": 3000,
"cost_per_close_cents": 21000
}
],
"next_cursor": null
}Never exposed: nothing beyond the rule — a column that is not a field above is never sent.
Events
Add an endpoint in Studio › API & webhooks and pick the events it gets. Each delivery is a POST with this JSON body.
| Event | When |
|---|---|
lead.created | A lead was added. |
lead.updated | A lead's fields or agency field values changed. |
lead.stage_changed | The disposition or the agency stage moved. |
lead.moved | The lead moved to another agent. |
lead.deleted | A lead was deleted. |
policy.created | A policy was recorded. |
policy.updated | A policy changed. |
policy.sold | A policy was sold (inserted, or its stage or close date set). |
policy.deleted | A policy was deleted. |
appointment.booked | An appointment was booked. |
appointment.rescheduled | Its time moved. |
appointment.outcome | Its outcome or status was set. |
appointment.deleted | An appointment was deleted. |
call.started | A call began. |
call.ended | A call ended. |
message.sent | An outbound text was sent (direction outbound). |
message.received | An inbound text arrived (direction inbound). |
record.created | A custom-table record was added (Phase 3). |
record.updated | A record or its links changed (Phase 3). |
record.deleted | A record was deleted (Phase 3). |
kpi_day.updated | An agent's day changed; keyed by (agent, date). |
data.object is the full resource, exactly as the API returns it. changed lists the fields that changed.
{
"id": "evt_01J9EXAMPLE0000000000000001",
"type": "lead.stage_changed",
"created_at": "2026-09-30T18:04:12Z",
"api_version": "v1",
"data": {
"object": {
"object": "lead",
"id": "ld_2b3c4d5e-0000-4000-8000-00000001ead1",
"owner_id": "ag_1a2b3c4d-0000-4000-8000-0000000a6e01",
"first_name": "Dana",
"last_name": "Ruiz",
"email": "dana.ruiz@example.com",
"phone": "+15555550142",
"state": "TX",
"zip": "78701",
"age": 54,
"disposition": "booked",
"stage": {
"id": "st_92a3b4c5-0000-4000-8000-0000000057a6",
"key": "appointment_set",
"label": "Appointment set"
},
"source": "facebook",
"product_type": "final_expense",
"tags": [
"evenings"
],
"notes": "Prefers a call after 6 pm.",
"score": 82,
"score_warmth_label": "Warm",
"call_count": 2,
"dial_attempts": 5,
"voicemails_left": 1,
"callback_at": null,
"closed_at": null,
"last_touched_at": "2026-09-30T18:04:11Z",
"last_dialed_at": "2026-09-30T17:58:40Z",
"last_inbound_at": "2026-09-30T18:02:09Z",
"cost_cents": 2400,
"campaign_id": "c5d6e7f8-0000-4000-8000-0000000ca3e1",
"origin_channel": "facebook",
"referred_by_lead_id": null,
"custom": {
"coverage_goal": "burial"
},
"created_at": "2026-09-28T15:20:00Z",
"updated_at": "2026-09-30T18:04:11Z"
},
"changed": [
"stage",
"disposition"
]
}
}A delete carries only the id.
{
"id": "evt_01J9EXAMPLE0000000000000002",
"type": "lead.deleted",
"created_at": "2026-09-30T18:10:00Z",
"api_version": "v1",
"data": {
"object": {
"object": "lead",
"id": "ld_2b3c4d5e-0000-4000-8000-00000001ead1",
"deleted": true
}
}
}- Delivery is at least once, and events of different resources arrive in no set order. Dedupe on
ezpz-id, and read the record when order matters. policy.soldcan arrive more than once for the same policy — each time its stage or close date is set while it is live. Key on the policy id.- When a lead moves to an agent outside your reach, your endpoint gets
lead.movedwith only{ object, id, moved: true }— the agency it left hears that it went, not where. - An endpoint hears only changes made after it was created, and nothing while it is paused.
- Send test in Studio sends a
pingevent, signed like any other.
Verify a signature
Every delivery carries three headers:
ezpz-id— the event id.ezpz-timestamp— when we signed it, in unix seconds.ezpz-signature—v1,<base64>.
The signed content is ${id}.${timestamp}.${rawBody}, signed with HMAC-SHA256. The key is your endpoint secret (whsec_…): base64 after the prefix. Reject a timestamp more than 5 minutes off, and compare in constant time.
import { createHmac, timingSafeEqual } from 'node:crypto';
// rawBody: the request body exactly as received (a string, before JSON.parse).
export function verifyEzpz(rawBody, headers, secret) {
const id = headers['ezpz-id'];
const timestamp = headers['ezpz-timestamp'];
const signature = headers['ezpz-signature'] ?? '';
if (!id || !timestamp || !signature.startsWith('v1,')) return false;
// Reject anything more than 5 minutes off.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
const expected = createHmac('sha256', key)
.update(`${id}.${timestamp}.${rawBody}`)
.digest();
const given = Buffer.from(signature.slice('v1,'.length), 'base64');
return given.length === expected.length && timingSafeEqual(given, expected);
}Retries
- Answer with a 2xx within 5 seconds. Anything else is a failure.
- We try 8 times over about 16 hours: 1 m → 5 m → 15 m → 1 h → 2 h → 4 h → 4 h → 4 h.
- We do not follow redirects. Give us the final URL.
- An endpoint pauses after 50 failures in a row. Turn it back on in Studio.
- Endpoints must be HTTPS to a public host.
- Delivery is at least once. You may get the same event twice — dedupe on
ezpz-id.
Connect an AI agent
EZPZ runs an MCP server, so an AI agent such as Claude can read your book and propose changes. It sees the same records the API does, through the same permissions.
https://www.buildezpz.com/api/mcp- As yourself. Add the server URL as a connector in your AI app and sign in to EZPZ. On the consent screen you pick what it acts as: you, with all your powers or only some of your roles; or, if you manage API keys, one of your agency's keys.
- Headless. An agent with no sign-in screen sends an API key as the bearer:
Authorization: Bearer ezpz_live_…. It acts as that key. - Scopes.
ezpz.readreads.ezpz.writeadds the tools that propose changes. A key also needs theread_writescope to write. - Propose, then confirm. A
propose_…tool changes nothing. It checks the change, stores it for 15 minutes and answers an id and a summary. The agent shows you the summary and runsconfirm_actiononly when you say yes. Each confirm runs once and is logged in Studio › Activity. - Autopilot. Off by default. Turn it on for a key in Studio, or for your own connection in Settings › Connections, and proposals run at once. An agent can be steered by text it reads in a lead's messages, so turn it on only for agents you trust.
- Limits. Per connection: 60 requests a minute, 3,000 a day, and 30 writes a minute. Past one, you get
429with aRetry-Afterheader.
Tools
Each read answers exactly what its API endpoint answers. Write tools are listed only for a connection that may write.
| Tool | Kind | What it does | Same as |
|---|---|---|---|
ezpz_list_leads | Read | List leads, newest first. | GET /api/v1/leads |
ezpz_get_lead | Read | One lead. | GET /api/v1/leads/{id} |
ezpz_list_policies | Read | List policies, newest first. | GET /api/v1/policies |
ezpz_get_policy | Read | One policy. | GET /api/v1/policies/{id} |
ezpz_list_appointments | Read | List appointments, newest first. | GET /api/v1/appointments |
ezpz_list_calls | Read | List calls, newest first (a transcript only where the connection may listen). | GET /api/v1/calls |
ezpz_get_call | Read | One call. | GET /api/v1/calls/{id} |
ezpz_list_messages | Read | List text messages, newest first. | GET /api/v1/messages |
ezpz_list_agents | Read | List the agents this connection reaches. | GET /api/v1/agents |
ezpz_list_fields | Read | The agency’s own fields on leads, policies and agents. | GET /api/v1/fields |
ezpz_list_stages | Read | The agency’s lead stages. | GET /api/v1/stages |
ezpz_list_tables | Read | The agency’s custom tables, each with its fields. | GET /api/v1/tables |
ezpz_list_records | Read | List one custom table’s records, newest first. | GET /api/v1/records |
ezpz_get_record | Read | One custom-table record. | GET /api/v1/records/{id} |
propose_create_lead | Proposes a change | Propose adding a lead to the connection’s own book. | POST /api/v1/leads |
propose_update_lead | Proposes a change | Propose changing a lead’s agency fields or stage. | PATCH /api/v1/leads/{id} |
propose_add_note | Proposes a change | Propose adding a dated note to a lead. | POST /api/v1/leads/{id}/notes |
propose_create_record | Proposes a change | Propose adding a record to a custom table. | POST /api/v1/records |
propose_update_record | Proposes a change | Propose changing a custom-table record. | PATCH /api/v1/records/{id} |
propose_book_appointment | Proposes a change | Propose booking an appointment with a lead on its agent’s calendar. | POST /api/v1/leads/{id}/appointments |
confirm_action | Pending actions | Run a pending action the user approved. | — |
cancel_action | Pending actions | Drop a pending action the user declined. | — |
list_pending_actions | Pending actions | This connection’s pending actions that have not expired. | — |
Embedded apps
Add your own web app in Studio › Apps & AI and it opens inside EZPZ, in the rail, for the people you pick. When someone opens it, EZPZ loads your start URL in a frame with a one-time token in the URL fragment. Your page trades that token for a one-hour session that acts as that person — it sees exactly what they see in EZPZ and can do only what they can.
- Your app must be served over HTTPS, and your server must allow EZPZ to frame it:
Content-Security-Policy: frame-ancestors https://www.buildezpz.com https://buildezpz.com - Read
ezpz_tokenfromlocation.hash, then remove it withhistory.replaceState. - From the browser,
POST /api/v1/auth/exchangewith{ "token": "…" }. The token works once, for 5 minutes, and only from your app's registered origin. Anything else answers401 invalid_token. - Use the
access_tokenyou get back (ezpz_sess_…) asAuthorization: Beareron/api/v1/*. It lasts an hour. - On a
401, post{ type: 'ezpz:token-expired' }tohttps://www.buildezpz.comand wait for{ type: 'ezpz:token', token }, then exchange that token. Checkevent.originon every message. - To move EZPZ to one of its own pages, post
{ type: 'ezpz:navigate', path: '/leads' }. - Never store either token — not in localStorage, a cookie or a log. Keep the session in memory and get a new one when it expires.
// 1. Read the one-time token from the fragment, then remove it from the URL.
const token = new URLSearchParams(location.hash.slice(1)).get('ezpz_token');
history.replaceState(null, '', location.pathname + location.search);
// 2. Exchange it from the browser (the Origin header must be your app's).
const PARENT = 'https://www.buildezpz.com';
async function exchange(t) {
const res = await fetch(PARENT + '/api/v1/auth/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: t }),
});
if (!res.ok) throw new Error('exchange failed');
return (await res.json()).access_token; // ezpz_sess_… — keep it in memory only
}
let session = await exchange(token);
// 3. Call the API as the signed-in person.
async function api(path, init = {}) {
const res = await fetch(PARENT + '/api/v1' + path, {
...init,
headers: { ...init.headers, Authorization: 'Bearer ' + session },
});
if (res.status !== 401) return res;
session = await renew(); // 4. On 401, ask EZPZ for a fresh token, then retry once.
return fetch(PARENT + '/api/v1' + path, { ...init, headers: { ...init.headers, Authorization: 'Bearer ' + session } });
}
function renew() {
return new Promise((resolve) => {
window.addEventListener('message', async function onMessage(e) {
if (e.origin !== PARENT || e.data?.type !== 'ezpz:token') return;
window.removeEventListener('message', onMessage);
resolve(await exchange(e.data.token));
});
parent.postMessage({ type: 'ezpz:token-expired' }, PARENT);
});
}
// Move EZPZ itself to one of its pages:
// parent.postMessage({ type: 'ezpz:navigate', path: '/leads' }, PARENT);A live, one-way copy of your agency's data in your own Postgres, under ezpz_v1. Set it up in Studio › Data copy.
What the copy is
EZPZ can keep a live copy of your agency’s data in a Postgres database you own. Changes in EZPZ reach your copy within seconds. The copy is one-way: nothing you write in your database comes back.
EZPZ stays the system of record. Make changes in EZPZ; read, join and report in your copy. The copy holds the same records and fields as API v1, under the schema ezpz_v1, ids included.
Connect it in Studio › Data copy. It is included in the Agency plan, and anyone whose role holds the Data copy switch can manage it.
Connect your database
- Postgres 14 or newer.
- SSL on. A connection string with
sslmode=disableis refused. - On Supabase, paste the pooler connection string (
…pooler.supabase.com), not the directdb.<project>.supabase.cohost. Use a paid project: a free project pauses when idle, and a paused project cannot be copied to. - If your database has a firewall, allow the two addresses Studio › Data copy shows (one IPv4, one IPv6).
Paste a connection string for a user that can create a schema and a role. We use it once: we create the schema ezpz_v1 and a role ezpz_copier with its own password, keep that role’s connection sealed, and discard yours. ezpz_copier owns ezpz_v1 and can reach nothing else in your database.
Studio runs the checks and shows each one, with the fix beside any that fail. Then the first copy starts.
Tables
One table per resource, in ezpz_v1. id is the public id (ld_…, ag_…) and the primary key; references to other records are public ids too. Every public field is a column, typed:
text— text, a fixed set of words.bigint— whole numbers, money in cents.numeric— decimals.boolean— true / false.timestamptz— times.date— dates.jsonb— objects, lists.
Custom values are in custom, keyed by each field’s key. Records of a custom table keep their values in records.data.
| Table | Columns | Bookkeeping |
|---|---|---|
ezpz_v1.leads | id text, owner_id text, first_name text, last_name text, email text, phone text, state text, zip text, age bigint, disposition text, stage jsonb, source text, product_type text, tags jsonb, notes text, score bigint, score_warmth_label text, call_count bigint, dial_attempts bigint, voicemails_left bigint, callback_at timestamptz, closed_at timestamptz, last_touched_at timestamptz, last_dialed_at timestamptz, last_inbound_at timestamptz, cost_cents bigint, campaign_id text, origin_channel text, referred_by_lead_id text, custom jsonb, created_at timestamptz, updated_at timestamptz | _xact, _seq, _synced_at, deleted_at, departed_at, moved_at, reason |
ezpz_v1.policies | id text, lead_id text, owner_id text, ap_cents bigint, advanced_pay_cents bigint, commission_cents bigint, commission_pct numeric, face_amount_cents bigint, carrier_name text, carrier_product text, policy_number text, policy_stage text, policy_stage_at timestamptz, close_date date, effective_date date, sold_reason text, policy_note text, client_first_name text, client_last_name text, call_id text, custom jsonb, created_at timestamptz, updated_at timestamptz | _xact, _seq, _synced_at, deleted_at, departed_at, moved_at, reason |
ezpz_v1.appointments | id text, lead_id text, owner_id text, starts_at timestamptz, ends_at timestamptz, timezone text, status text, outcome text, title text, notes text, source text, created_by text, created_at timestamptz, updated_at timestamptz | _xact, _seq, _synced_at, deleted_at, departed_at, moved_at, reason |
ezpz_v1.calls | id text, lead_id text, owner_id text, direction text, status text, started_at timestamptz, answered_at timestamptz, ended_at timestamptz, duration_sec bigint, voicemail boolean, from_number text, to_number text, caller_name text, placed_by_actor_id text, answered_by text, recording_duration_sec bigint, has_recording boolean, transcription text, recording_link text, created_at timestamptz, recording_object text, recording_gone_at timestamptz | _xact, _seq, _synced_at, deleted_at, departed_at, moved_at, reason |
ezpz_v1.messages | id text, lead_id text, owner_id text, direction text, body text, status text, from_number text, to_number text, num_media bigint, send_origin text, sent_by_kind text, sent_by_actor_id text, read_at timestamptz, created_at timestamptz | _xact, _seq, _synced_at, deleted_at, departed_at, moved_at, reason |
ezpz_v1.agents | id text, full_name text, display_name text, business_name text, email text, phone text, agency_id text, role text, manager_id text, npn text, timezone text, work_state text, avatar_url text, status text, custom jsonb, created_at timestamptz | _xact, _seq, _synced_at, deleted_at, departed_at, moved_at, reason |
ezpz_v1.roles | id text, agency_id text, name text, description text, switches jsonb, push_mode text, status text, created_at timestamptz | _xact, _seq, _synced_at, deleted_at |
ezpz_v1.fields | id text, agency_id text, object text, key text, label text, type text, options jsonb, required boolean, help text, position bigint, push_mode text, status text, created_at timestamptz | _xact, _seq, _synced_at, deleted_at |
ezpz_v1.stages | id text, agency_id text, key text, label text, maps_to text, tone text, position bigint, push_mode text, status text, created_at timestamptz | _xact, _seq, _synced_at, deleted_at |
ezpz_v1.tables | id text, agency_id text, key text, name text, record_noun text, icon text, shared boolean, on_lead boolean, status text, fields jsonb | _xact, _seq, _synced_at, deleted_at |
ezpz_v1.records | id text, table_id text, agency_id text, owner_id text, title text, data jsonb, links jsonb, created_at timestamptz, updated_at timestamptz | _xact, _seq, _synced_at, deleted_at, departed_at, moved_at, reason |
ezpz_v1.agencies | id text, name text, parent_id text, tier text, created_at timestamptz | _xact, _seq, _synced_at, deleted_at |
ezpz_v1.kpi_days | id text, agent_id text, date date, dials bigint, contacts bigint, appointments_set bigint, appointments_held bigint, presentations bigint, closes bigint, texts_sent bigint, talk_seconds bigint, leads_received bigint, ap_cents bigint, spend_cents bigint, lead_cost_cents bigint, cost_per_lead_cents bigint, cost_per_close_cents bigint | _xact, _seq, _synced_at, deleted_at, departed_at, moved_at, reason |
Bookkeeping columns
Every table also has:
_xact— With_seq: the position in EZPZ’s change log this row was last written at._seq— An older change never overwrites a newer one, and applying the same change twice writes nothing._synced_at— When this row was last written in your copy.deleted_at— Set when the record was deleted in EZPZ. The row stays.
Tables of records an agent owns (leads, policies, appointments, calls, messages, agents, records, kpi_days) also have:
departed_at— Set when the agent who owns it left your agency. The row stays and stops updating.moved_at— Set when the record moved to an agent outside your copy. The row stays and stops updating.reason— Why it stopped updating:departed,movedorconsent_withdrawn.
Deletes, departures and moves
We never delete a row in your database. What happened to a record is a column on its row:
- Deleted in EZPZ:
deleted_atis set (a tombstone). - Its agent left your agency:
departed_atis set andreasonisdeparted. The row stops updating. - Moved to an agent outside your copy, or to one who has not accepted:
moved_atis set andreasonismoved. The row stops updating. - If the record comes back to your copy, these clear and it updates again.
select *
from ezpz_v1.leads
where deleted_at is null
and departed_at is null
and moved_at is null;Typed views
Custom fields become columns in views we keep beside the tables: leads_typed, policies_typed, agents_typed, and one records_<table key> per custom table. Each field key is a column, cast by its type:
- number, currency →
numeric - date →
date - checkbox →
boolean - multi_select →
text[] - everything else →
text
A field key that matches an existing column is named custom_<key>. The views are rebuilt when a field or table changes; the field and table definitions themselves are in fields and tables.
Lag and sync_meta
ezpz_v1.sync_meta has one row that tells you how current your copy is:
model_version— The version of the table layout your copy has.last_synced_at— When the copy last caught up with every change.lag_ms— Milliseconds between a change in EZPZ and it landing here, for the latest batch.backlog— Changes waiting to copy.status— The copy’s state, as Studio shows it.backfill_done_at— When the first (or the latest full) copy finished.worker_version— The version of the program that wrote it.updated_at— When this row was last written.
select status, last_synced_at, lag_ms, backlog
from ezpz_v1.sync_meta;Studio › Data copy shows the same. If the copy falls more than 15 minutes behind, or cannot reach your database again and again, the owner gets an email and a notice.
Recordings
Choose in Studio › Data copy: Off, Links (the default) or Your bucket.
Links. recording_link on each row is a stable link, https://www.buildezpz.com/api/v1/calls/{id}/recording. Request it with an API key whose roles may listen to that agent’s recordings (calls.listen). It answers with a redirect to the audio, good for 10 minutes, or 404 when the audio is gone.
Your bucket. Each recording is also copied into your S3-compatible bucket (AWS S3, Cloudflare R2, Backblaze B2 and others) as ezpz-recordings/cl_….mp3 (or under the path prefix you set), and recording_object names it. recording_gone_at is set when the audio was gone before it could copy. We check the bucket the first time a recording copies; a failure shows in Studio and alerts the owner.
A bucket copy is kept by you, outside EZPZ’s deletion schedule. When a recording is removed in EZPZ we delete the object we copied into your bucket; copies you moved elsewhere are yours to delete.
Whose records copy
Your agency and every agency under it are included. agents, roles, fields, stages, tables, agencies, kpi_days copy whether or not an agent accepted: your roster, its production totals and your definitions.
Record rows (leads, policies, appointments, calls, messages, records) copy only for agents who accepted the data copy notice. Each agent is asked the next time they sign in; Studio › Data copy shows how many of each agency have accepted.
Studio’s What copies switches turn whole groups off: leads with their deals and activity; conversations with their transcripts; agents with their roles, custom tables and records. Production totals, fields, stages and agencies always copy.
Pause, outages and re-copy
- Pause. Changes wait up to 7 days. Resume and what waited copies first.
- Can’t reach your database. We keep retrying, waiting longer each time. After 7 days the copy pauses and asks for a full re-copy, because changes older than 7 days are no longer kept.
- Re-copy all. Every record copies again from the start. Rows already in your database are updated in place, never deleted.
- Disconnect. Copying stops and we erase the connection. What is already in your database stays there.
Versioning
Changes to ezpz_v1 are additive: a new table or a new column, applied to your copy automatically. Nothing is ever dropped or retyped. A breaking change ships ezpz_v2 beside ezpz_v1.
What we never do
- Read your data. Outside setup (the server version, SSL, our role’s own rights) the one thing we read is
ezpz_v1.sync_meta. - Delete a row or drop a table in your database.
- Touch anything outside
ezpz_v1. - Show your connection string or bucket keys again. Both are stored sealed.
Versioning
The version is in the path: /api/v1. Changes inside v1 are additive only — a new resource, a new field, a new event or a new enum value. Ignore fields you do not know.
A breaking change — a field removed or renamed, or its type or meaning changed — ships /api/v2 and ezpz_v2 beside v1, with a deprecation window.