Developers · API v1

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.
Start

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>" }
CodeStatusWhat it means
unauthenticated401No key, a malformed key, or a revoked one.
forbidden403The key is real but its roles do not allow this, or the record is outside its reach.
plan_required403The agency is not on a plan that includes the API.
not_found404No such record, or one the key cannot see.
validation422 · 400The body or query did not pass. 400 when an id is malformed.
rate_limited429Too many requests. Wait the seconds in the Retry-After header.
db_error500Our 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.

Resources · ezpz_v1

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.

FieldTypeNotes
idstringThe lead id.
owner_idagent idThe agent who owns the record (an agent id).
first_namestring or nullFirst name.
last_namestring or nullLast name.
emailstring or nullEmail address.
phonestring or nullPhone number, E.164 where known.
statestring or nullTwo-letter US state.
zipstring or nullZIP code.
ageinteger or nullAge in years, as given.
dispositionstringThe fixed disposition (new_lead, called, booked, sold, …). Canonical; stage is the agency's word for it.
stageobject or nullThe agency stage it shows: {id, key, label}, or null for the fixed label.
sourcestringWhere the lead came from (facebook, transferred, mail_in, api, …).
product_typestringThe product line the lead asked about.
tagsarrayTags, as strings.
notesstring or nullThe agent's notes on the lead.
scoreinteger or nullLead score, 0–100.
score_warmth_labelstring or nullThe warmth word beside the score.
call_countintegerThe current call streak.
dial_attemptsintegerEvery dial ever placed to the lead.
voicemails_leftintegerVoicemails left.
callback_attimestamp or nullA scheduled callback.
closed_attimestamp or nullWhen the lead was closed.
last_touched_attimestampThe last time anyone acted on the lead.
last_dialed_attimestamp or nullThe last dial.
last_inbound_attimestamp or nullThe last inbound text or call.
cost_centsinteger (cents)What the lead cost (cents).
campaign_idstring or nullThe ad campaign it came from (an EZPZ campaign id).
origin_channelstring or nullThe channel it first arrived through.
referred_by_lead_idlead id or nullThe lead who referred this one.
customobjectAgency field values, keyed by field key. Health-shaped fields are never included.
created_attimestampWhen the record was created.
updated_attimestampWhen the record last changed.
Request
curl https://www.buildezpz.com/api/v1/leads/ld_2b3c4d5e-0000-4000-8000-00000001ead1 \
  -H "Authorization: Bearer ezpz_live_…"
Response
{
  "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.

FieldTypeNotes
idstringThe policy id.
lead_idlead idThe lead it was sold to.
owner_idagent idThe agent who owns the record (an agent id).
ap_centsinteger (cents)Annual premium (cents).
advanced_pay_centsinteger (cents)Advance paid (cents).
commission_centsinteger (cents)Commission (cents).
commission_pctnumber or nullCommission percent (two decimals).
face_amount_centsinteger (cents) or nullFace amount (cents).
carrier_namestring or nullCarrier.
carrier_productstring or nullCarrier product.
policy_numberstring or nullPolicy number.
policy_stagestringWhere the policy stands (submitted, issued, paid, lapsed, …).
policy_stage_attimestampWhen it reached that stage.
close_datedateThe sale date.
effective_datedate or nullThe policy effective date.
sold_reasonstring or nullWhy it sold, as noted.
policy_notestring or nullThe agent's note on the policy.
client_first_namestring or nullThe insured's first name, when not the lead.
client_last_namestring or nullThe insured's last name, when not the lead.
call_idcall id or nullThe call it closed on.
customobjectAgency policy field values, keyed by field key. Health-shaped fields are never included.
created_attimestampWhen the record was created.
updated_attimestampWhen the record last changed.
Request
curl https://www.buildezpz.com/api/v1/policies/pl_3c4d5e6f-0000-4000-8000-0000000b0011 \
  -H "Authorization: Bearer ezpz_live_…"
Response
{
  "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.
FieldTypeNotes
idstringThe appointment id.
lead_idlead idThe lead it is with.
owner_idagent idThe agent who owns the record (an agent id).
starts_attimestampStart.
ends_attimestampEnd.
timezonestring or nullThe IANA time zone it was booked in.
statusscheduled | cancelled | completed | no_showIts state.
outcomestring or nullWhat happened at the meeting.
titlestring or nullTitle.
notesstring or nullNotes.
sourcestring or nullHow it was booked.
created_byagent id or nullWho booked it (an agent id).
created_attimestampWhen the record was created.
updated_attimestampWhen the record last changed.
Request
curl https://www.buildezpz.com/api/v1/appointments/ap_4d5e6f70-0000-4000-8000-0000000a1101 \
  -H "Authorization: Bearer ezpz_live_…"
Response
{
  "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.
FieldTypeNotes
idstringThe call id.
lead_idlead id or nullThe lead on the call, when known.
owner_idagent idThe agent who owns the record (an agent id).
directionstringinbound or outbound.
statusstringThe call state (ringing, in_progress, completed, no_answer, …).
started_attimestamp or nullWhen it started.
answered_attimestamp or nullWhen it was answered.
ended_attimestamp or nullWhen it ended.
duration_secinteger or nullTalk time in seconds.
voicemailbooleanA voicemail was left or taken.
from_numberstringFrom.
to_numberstringTo.
caller_namestring or nullCaller name, when known.
placed_by_actor_idagent id or nullWho placed it, when not the owner (an agent id).
answered_bystring or nullWho picked up (human or machine), when detected.
recording_duration_secinteger or nullRecording length in seconds.
has_recordingbooleanA recording exists and has not been purged.
transcriptionstring or nullThe call transcript, when one was made.
recording_linkstring or nullA 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_attimestampWhen the record was created.
Request
curl https://www.buildezpz.com/api/v1/calls/cl_5e6f7081-0000-4000-8000-0000000ca111 \
  -H "Authorization: Bearer ezpz_live_…"
Response
{
  "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.
FieldTypeNotes
idstringThe message id.
lead_idlead id or nullThe lead, when known.
owner_idagent idThe agent who owns the record (an agent id).
directionstringinbound or outbound.
bodystringThe text.
statusstringDelivery state.
from_numberstringFrom.
to_numberstringTo.
num_mediainteger or nullAttached media count.
send_originstring or nullWhat sent it (manual, automation, …).
sent_by_kindstring or nullowner, delegate or echo.
sent_by_actor_idagent id or nullWho sent it, when not the owner (an agent id).
read_attimestamp or nullWhen the agent read it.
created_attimestampWhen the record was created.
Request
curl "https://www.buildezpz.com/api/v1/messages?limit=1" \
  -H "Authorization: Bearer ezpz_live_…"
Response
{
  "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.

FieldTypeNotes
idstringThe agent id.
full_namestring or nullFull name.
display_namestring or nullThe name they go by.
business_namestring or nullTheir business name.
emailstringEmail.
phonestring or nullPhone.
agency_idagency id or nullTheir agency.
roleagency_owner | agency_admin | agency_assistant | member or nullTheir seat in the agency; null when they hold none.
manager_idagent id or nullTheir upline manager.
npnstring or nullNational Producer Number.
timezonestring or nullIANA time zone.
work_statestring or nullHome work state.
avatar_urlstring or nullAvatar image URL.
statusstringAccount status.
customobjectAgency agent field values, keyed by field key. Health-shaped fields are never included.
created_attimestampWhen the record was created.
Request
curl https://www.buildezpz.com/api/v1/agents/ag_1a2b3c4d-0000-4000-8000-0000000a6e01 \
  -H "Authorization: Bearer ezpz_live_…"
Response
{
  "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.
FieldTypeNotes
idstringThe role id.
agency_idagency idThe defining agency.
namestringName.
descriptionstringDescription.
switchesobjectSwitch key → the reaches it grants (a data switch) or true (an agency-wide switch).
push_modeoffered | suggested | extendable | locked or nullPushed to the agencies below: offered, suggested, extendable or locked; null = not pushed.
statusdraft | live | retireddraft, live or retired.
created_attimestampWhen the record was created.
Request
curl "https://www.buildezpz.com/api/v1/roles?limit=1" \
  -H "Authorization: Bearer ezpz_live_…"
Response
{
  "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.
FieldTypeNotes
idstringThe field id.
agency_idagency idThe defining agency.
objectlead | policy | agentWhat it is on.
keystringThe key its values appear under in custom.
labelstringLabel.
typestringField type (text, number, date, select, multi_select, checkbox, …).
optionsarrayChoices {value, label} for a pick field.
requiredbooleanRequired.
helpstringHelp text.
positionintegerDisplay order.
push_modeoffered | suggested | extendable | locked or nullPushed to the agencies below; null = not pushed.
statusdraft | live | retireddraft, live or retired.
created_attimestampWhen the record was created.
Request
curl "https://www.buildezpz.com/api/v1/fields?limit=1" \
  -H "Authorization: Bearer ezpz_live_…"
Response
{
  "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.
FieldTypeNotes
idstringThe stage id.
agency_idagency idThe defining agency.
keystringKey.
labelstringLabel.
maps_tostringThe fixed disposition it counts as.
tonestring or nullDot colour.
positionintegerDisplay order.
push_modeoffered | suggested | extendable | locked or nullPushed to the agencies below; null = not pushed.
statusdraft | live | retireddraft, live or retired.
created_attimestampWhen the record was created.
Request
curl "https://www.buildezpz.com/api/v1/stages?limit=1" \
  -H "Authorization: Bearer ezpz_live_…"
Response
{
  "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.
FieldTypeNotes
idstringThe table id.
agency_idagency idThe defining agency.
keystringKey.
namestringName.
record_nounstringWhat one record is called.
iconstring or nullIcon name.
sharedbooleanShared across the agency.
on_leadbooleanShown on the lead card.
statusdraft | live | retireddraft, live or retired.
fieldsarrayIts 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.
Request
curl https://www.buildezpz.com/api/v1/tables/tb_a3b4c5d6-0000-4000-8000-00000007ab1e \
  -H "Authorization: Bearer ezpz_live_…"
Response
{
  "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_id is required. A list past your agency's rows-per-filter limit (20,000 unless raised) answers 422 too_many_rows: narrow it by owner_id or updated_since.
  • GET /api/v1/records/{id}Get one record.
  • POST /api/v1/recordsCreate a record: {table_id, owner_id?, data}, data keyed by field key. Without owner_id it 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; null clears 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.

FieldTypeNotes
idstringThe record id.
table_idtable idIts table.
agency_idagency idThe agency.
owner_idagent id or nullThe agent who owns the record (an agent id).
titlestringTitle: the value of the table's first text field.
dataobjectValues keyed by the table's field KEY (not the definition id). Health-shaped fields are never included.
linksarrayWhat it links to [{kind, id}] (kind: record, lead or policy) — only targets your key can read. Read-only in v1.
created_attimestampWhen the record was created.
updated_attimestampWhen the record last changed.
Request
curl https://www.buildezpz.com/api/v1/records/rc_b4c5d6e7-0000-4000-8000-0000000ec0d1 \
  -H "Authorization: Bearer ezpz_live_…"
Response
{
  "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.
FieldTypeNotes
idstringThe agency id.
namestringName.
parent_idagency id or nullThe agency above it, if any.
tierstringAgency kind (portal, enterprise, external, spinoff).
created_attimestampWhen the record was created.
Request
curl https://www.buildezpz.com/api/v1/agency \
  -H "Authorization: Bearer ezpz_live_…"
Response
{
  "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 from and to (dates, at most 93 days). agent_id is optional.
FieldTypeNotes
idstringThe day id: kd_<agent uuid>_<date>.
agent_idagent idThe agent.
datedateThe day, in the agent's time zone.
dialsintegerDials.
contactsintegerContacts.
appointments_setintegerAppointments booked.
appointments_heldintegerAppointments held.
presentationsintegerPresentations.
closesintegerCloses.
texts_sentintegerTexts sent.
talk_secondsintegerTalk time (seconds).
leads_receivedintegerNew leads received.
ap_centsinteger (cents)Annual premium closed (cents).
spend_centsinteger (cents)Ad and lead spend (cents).
lead_cost_centsinteger (cents)The summed cost of the leads received (cents).
cost_per_lead_centsinteger (cents) or nullDERIVED: spend_cents ÷ leads_received, rounded; null when no leads.
cost_per_close_centsinteger (cents) or nullDERIVED: spend_cents ÷ closes, rounded; null when no closes.
Request
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_…"
Response
{
  "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.

Webhooks

Events

Add an endpoint in Studio › API & webhooks and pick the events it gets. Each delivery is a POST with this JSON body.

EventWhen
lead.createdA lead was added.
lead.updatedA lead's fields or agency field values changed.
lead.stage_changedThe disposition or the agency stage moved.
lead.movedThe lead moved to another agent.
lead.deletedA lead was deleted.
policy.createdA policy was recorded.
policy.updatedA policy changed.
policy.soldA policy was sold (inserted, or its stage or close date set).
policy.deletedA policy was deleted.
appointment.bookedAn appointment was booked.
appointment.rescheduledIts time moved.
appointment.outcomeIts outcome or status was set.
appointment.deletedAn appointment was deleted.
call.startedA call began.
call.endedA call ended.
message.sentAn outbound text was sent (direction outbound).
message.receivedAn inbound text arrived (direction inbound).
record.createdA custom-table record was added (Phase 3).
record.updatedA record or its links changed (Phase 3).
record.deletedA record was deleted (Phase 3).
kpi_day.updatedAn 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.

Payload
{
  "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.

Delete payload
{
  "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.sold can 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.moved with 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 ping event, 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.

Node
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.
AI agents · MCP

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.

Server URL
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.read reads. ezpz.write adds the tools that propose changes. A key also needs the read_write scope 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 runs confirm_action only 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 429 with a Retry-After header.

Tools

Each read answers exactly what its API endpoint answers. Write tools are listed only for a connection that may write.

ToolKindWhat it doesSame as
ezpz_list_leadsReadList leads, newest first.GET /api/v1/leads
ezpz_get_leadReadOne lead.GET /api/v1/leads/{id}
ezpz_list_policiesReadList policies, newest first.GET /api/v1/policies
ezpz_get_policyReadOne policy.GET /api/v1/policies/{id}
ezpz_list_appointmentsReadList appointments, newest first.GET /api/v1/appointments
ezpz_list_callsReadList calls, newest first (a transcript only where the connection may listen).GET /api/v1/calls
ezpz_get_callReadOne call.GET /api/v1/calls/{id}
ezpz_list_messagesReadList text messages, newest first.GET /api/v1/messages
ezpz_list_agentsReadList the agents this connection reaches.GET /api/v1/agents
ezpz_list_fieldsReadThe agency’s own fields on leads, policies and agents.GET /api/v1/fields
ezpz_list_stagesReadThe agency’s lead stages.GET /api/v1/stages
ezpz_list_tablesReadThe agency’s custom tables, each with its fields.GET /api/v1/tables
ezpz_list_recordsReadList one custom table’s records, newest first.GET /api/v1/records
ezpz_get_recordReadOne custom-table record.GET /api/v1/records/{id}
propose_create_leadProposes a changePropose adding a lead to the connection’s own book.POST /api/v1/leads
propose_update_leadProposes a changePropose changing a lead’s agency fields or stage.PATCH /api/v1/leads/{id}
propose_add_noteProposes a changePropose adding a dated note to a lead.POST /api/v1/leads/{id}/notes
propose_create_recordProposes a changePropose adding a record to a custom table.POST /api/v1/records
propose_update_recordProposes a changePropose changing a custom-table record.PATCH /api/v1/records/{id}
propose_book_appointmentProposes a changePropose booking an appointment with a lead on its agent’s calendar.POST /api/v1/leads/{id}/appointments
confirm_actionPending actionsRun a pending action the user approved.—
cancel_actionPending actionsDrop a pending action the user declined.—
list_pending_actionsPending actionsThis connection’s pending actions that have not expired.—
Apps

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_token from location.hash, then remove it with history.replaceState.
  • From the browser, POST /api/v1/auth/exchange with { "token": "…" }. The token works once, for 5 minutes, and only from your app's registered origin. Anything else answers 401 invalid_token.
  • Use the access_token you get back (ezpz_sess_…) as Authorization: Bearer on /api/v1/*. It lasts an hour.
  • On a 401, post { type: 'ezpz:token-expired' } to https://www.buildezpz.com and wait for { type: 'ezpz:token', token }, then exchange that token. Check event.origin on 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.
Browser
// 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);
Data copy

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=disable is refused.
  • On Supabase, paste the pooler connection string (…pooler.supabase.com), not the direct db.<project>.supabase.co host. 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.

TableColumnsBookkeeping
ezpz_v1.leadsid 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.policiesid 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.appointmentsid 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.callsid 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.messagesid 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.agentsid 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.rolesid 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.fieldsid 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.stagesid 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.tablesid 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.recordsid 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.agenciesid text, name text, parent_id text, tier text, created_at timestamptz_xact, _seq, _synced_at, deleted_at
ezpz_v1.kpi_daysid 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, moved or consent_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_at is set (a tombstone).
  • Its agent left your agency: departed_at is set and reason is departed. The row stops updating.
  • Moved to an agent outside your copy, or to one who has not accepted: moved_at is set and reason is moved. The row stops updating.
  • If the record comes back to your copy, these clear and it updates again.
Current leads only
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.
How current is it
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.

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.
Changes

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.