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.
  • A key can be limited to chosen fields. Every read and webhook it makes then carries only those fields, plus id.
  • Included in the Agency plan.
  • Spec file (OpenAPI 3.1) for code generators and API tools.
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.
unknown_param400A custom endpoint got a query parameter it does not take. The endpoint is the query.
rate_limited429Too many requests. Wait the seconds in the Retry-After header.
db_error500Our fault. Retry with backoff.
not_ready503This part of the API is still being switched on. Retry in a few minutes.

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/leads?phone=&email=List leads, newest first. phone finds a lead by its number in any common US spelling; email by its address as typed or in lower case.
  • GET /api/v1/leads/{id}Get one lead.
  • POST /api/v1/leadsCreate a lead. Once your agency signs the key's consent promise in Studio, a lead the key posts with a phone can get texts. sms_consent: {text, at} is optional: the person's own consent words and when, kept as the proof. Before the promise is signed, a lead needs them to get texts.
  • PATCH /api/v1/leads/{id}Change its name, phone, email, state or product (first_name, last_name, phone, email, state, product_type), its agency fields (custom) or its stage (stage_id). A note is its own call (below). address and source are refused.
  • POST /api/v1/leads/{id}/notesAdd a note. It shows in the lead's notes.
  • POST /api/v1/leads/{id}/tagsAdd or remove tags: {add?, remove?}, lists of tag names.
  • 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, manual, …). A lead created through this API reads manual.
product_typestringThe product line the lead asked about (mp, iul, fex, annu or life). A lead filed on a lead type holds the type's base product.
lead_typeobject or nullThe lead type it is filed on: {key, label} — the agent's or the agency's own type, which works like its product_type. Null for a plain product.
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",
  "lead_type": {
    "key": "veterans",
    "label": "Veterans"
  },
  "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 — 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.
  • POST /api/v1/policiesRecord a sale on a lead your key's creator owns: {lead_id, ap_cents, commission_pct, close_date, …}. The lead closes, as on the screen.
  • PATCH /api/v1/policies/{id}Change a sale your key's creator owns.

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, send_subkind, workflow_id; 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.
  • DELETE /api/v1/records/{id}Delete it.

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.

sale

A sale posted to the agency's book of business, with the agent who posted it. Read-only, oldest change first: poll GET /api/v1/sales with the next_cursor you got last time to get every new, changed or deleted sale once, and upsert each by id. Changes appear after about 10 seconds. A deleted sale comes back with deleted: true. An API key sees the sales posted after it was created.

  • GET /api/v1/salesEvery sale on your agency's book, oldest change first. Unlike other lists, next_cursor is never null after your first sale: store it and pass it back as cursor to get only what is new, changed or deleted since. Fewer rows than limit means you are caught up. Changes appear after about 10 seconds. A key sees sales posted after it was created. Needs Team production at Agency reach.
FieldTypeNotes
idstringThe sale id.
agent_emailstringThe agent's sign-in email, lower-cased.
agent_contact_emailstring or nullThe agent's contact email, when they set one apart from the sign-in email.
agent_first_namestring or nullThe first word of the agent's full name. EZPZ stores one full name; this is a split of it. Null when the name is blank.
agent_last_namestring or nullThe rest of the agent's full name after the first word. Null when the name is one word or blank.
annual_premium_centsinteger (cents)Annual premium (cents): the monthly premium × 12.
carrierstring or nullThe carrier.
productstring or nullThe product within the carrier.
sale_datedateThe sale date the agent entered; when none, the day it was posted (Mountain time).
statusstringWhere it stands, as entered: Submitted, Submitted - Pending Requirements, Packet Received, Issue Paid, Canceled, Declined, Lapsed or Chargeback. Empty when none is set yet.
countedbooleanIt counts toward premium totals: a status is set and it is not Canceled, Declined, Lapsed or Chargeback. Always false when deleted.
created_attimestampWhen the record was created.
updated_attimestampWhen the sale last changed; on a deleted sale, when it was deleted.
deletedbooleanThe sale was deleted. Its other fields are the last values it had.
Request
curl https://www.buildezpz.com/api/v1/sales \
  -H "Authorization: Bearer ezpz_live_…"
Response
{
  "object": "list",
  "data": [
    {
      "object": "sale",
      "id": "sl_d6e7f809-0000-4000-8000-00000005a1e1",
      "agent_email": "sam.okafor@example.com",
      "agent_contact_email": null,
      "agent_first_name": "Sam",
      "agent_last_name": "Okafor",
      "annual_premium_cents": 102000,
      "carrier": "Mutual of Omaha",
      "product": "Living Promise",
      "sale_date": "2026-09-30",
      "status": "Submitted",
      "counted": true,
      "created_at": "2026-09-30T18:04:11.204518+00:00",
      "updated_at": "2026-09-30T18:04:11.204518+00:00",
      "deleted": false
    }
  ],
  "next_cursor": "WyIyMDI2LTA5LTMwVDE4OjA0OjExLjIwNDUxOCswMDowMCIsImQ2ZTdmODA5LTAwMDAtNDAwMC04MDAwLTAwMDAwMDA1YTFlMSJd"
}

Never exposed: sensitive — client_name, state; internal — lead_type, external_id, month_label, month_order, row_order, discord_posted_at, effective_date*, fi_date, landed_at, submitted_date, canceled_at; billing — commission_percent.

onboarding_call

An onboarding call someone booked on one of the agency's onboarding pages (Desk ▸ Booking). Sent as the onboarding_call.booked webhook; there is no /api/v1 route for it yet. A key gets it when it holds team.production at a reach covering the whole agency (the sales feed's rule).

Webhook only: subscribe an endpoint to `onboarding_call.booked`. No /api/v1 route yet.

FieldTypeNotes
idstringThe onboarding call id.
full_namestringThe name the person typed when they booked.
emailstringThe email they typed, lower-cased.
phonestringThe phone they typed, as typed.
member_emailstring or nullSet when the email is an agency member's: the member it matched. Null for someone who has not joined.
host_emailstringThe sign-in email of the staff member whose page it was booked on, lower-cased. They host the call.
starts_attimestampWhen the call starts.
ends_attimestampWhen the call ends.
timezonestringThe page's time zone (an IANA name, e.g. America/Chicago).
meet_linkstring or nullThe Google Meet link, when the calendar made one.
statusbooked | done | no_show | cancelledWhere it stands: booked, then done, no_show or cancelled.
created_attimestampWhen the record was created.
updated_attimestampWhen the record last changed.
Shape
{
  "object": "onboarding_call",
  "id": "oc_e7f8091a-0000-4000-8000-0000000ca11b",
  "full_name": "Jordan Lee",
  "email": "jordan.lee@example.com",
  "phone": "(555) 010-4477",
  "member_email": "jordan.lee@example.com",
  "host_email": "sam.okafor@example.com",
  "starts_at": "2026-10-01T16:00:00+00:00",
  "ends_at": "2026-10-01T16:30:00+00:00",
  "timezone": "America/Chicago",
  "meet_link": "https://meet.google.com/abc-defg-hij",
  "status": "booked",
  "created_at": "2026-09-30T18:04:11.204518+00:00",
  "updated_at": "2026-09-30T18:04:11.204518+00:00"
}

Never exposed: vendor ids — google_event_id; internal — agency_id, host_user_id, recruit_id, status_changed_at, status_changed_by.

Custom endpoints

Custom endpoints

An agency can save its own read in Studio › API & webhooks: one kind of data, up to five filters, the fields to send and a name. It answers at its own URL.

GET /api/v1/views/<slug>
Authorization: Bearer ezpz_live_…
  • It runs as your key. You get only the records your key's roles reach, and only the fields your key may see.
  • The endpoint is the query. Send only cursor, limit and the time floor of its data: updated_since for leads and records, since for every other kind but sales. Anything else answers 400 unknown_param.
  • Some filters run after a page is read, so a page can hold fewer items than limit, even none. Keep paging until next_cursor is null.
  • An endpoint on sales pages like GET /api/v1/sales: store next_cursor and poll with it.
  • A slug your agency has not saved, or a key the endpoint does not let in, answers 404 not_found.
Inbound URLs

Inbound URLs

An agency can make a private URL in Studio › API & webhooks for an outside system — a lead vendor, a form tool, Zapier — to post leads to. It needs no key: the URL is the secret. Keep it private.

POST https://www.buildezpz.com/api/in/<token>
Content-Type: application/json

{ "first_name": "Ann", "phone": "512-555-0100", "email": "ann@example.com" }
  • Send one lead per request: one JSON object, up to 64 KB. The agency maps your fields onto its leads in Studio, so send your own shape.
  • Each post needs a phone or an email. A lead the agency already has (same phone, then same email) is matched, not made twice.
  • Until the agency signs its consent statement for the URL, every lead is held: it lands, and nothing texts it. Once it is signed, every lead with a phone can get texts. You may also send the consumer's own consent words and the time they gave it; they are kept as proof.
  • Answers: 201 created · 200 matched · 202 held for consent, each with lead_id and result. 400 not JSON or unreadable · 413 over 64 KB · 422 needs_phone_or_email · 429 too many (wait the seconds in Retry-After) · 404 an unknown, paused or replaced URL.
  • Limits: 120 posts a minute and 10,000 a day per URL.
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).
onboarding_call.bookedSomeone booked an onboarding call on one of the agency's onboarding pages.
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",
      "lead_type": {
        "key": "veterans",
        "label": "Veterans"
      },
      "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 › Apps, 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.—

Set up your agency with your AI

An agency's owner or admin can let their own AI set up the agency. On the consent screen, tick Set up my agency. That adds the ezpz.setup scope and the tools below, for that agency only.

  • Small changes run at once. Each one shows in Studio › History, where the owner can Undo it.
  • Big changes ask first. A tool marked “Asks first” stores a pending action. The AI asks you in its chat and runs confirm_action only when you say yes.
  • It never sees your database password. Connecting a database stays in Studio › Data copy.
  • Disconnect any time in Studio › Apps & AI.
ToolStepKindWhat it does
setup_statusThe pathReadWhere the agency is on its setup: first Get running (the cards before Go live), then Go live (go_live_done), then Make it yours — the queue, in four phases and by how many people each item reaches inside a phase, and the next item. Also each path step, done or not, skipped or not.
setup_briefThe pathReadThe Get running cards (the quick setup questions), the answers so far and the setup parts they draft.
setup_brief_submitThe pathChangeSave the owner’s Get running answers.
setup_list_changesThe pathReadThe agency’s setup changes, newest first, each with who made it and whether it can be undone.
setup_skip_stepThe pathChangeSkip a setup step for now, put it or a Make it yours item off until later, or bring it back.
setup_undoThe pathChange (asks first for a big one)Undo one setup change from setup_list_changes.
setup_data_copy_statusDatabaseReadThe data copy to the agency’s own database: connected or not, running or paused, and how far it has copied.
setup_pause_data_copyDatabaseAsks firstPause the data copy to the agency’s own database.
setup_resume_data_copyDatabaseAsks firstResume a paused data copy.
setup_get_brandBrand and web addressReadThe live brand, the draft waiting (if any) and the past versions.
setup_check_domainBrand and web addressReadThe agency’s web address: its status and the DNS records it still needs, as last checked.
setup_save_brand_draftBrand and web addressChangeSave brand changes as a DRAFT. Members see nothing until it is published.
setup_discard_brand_draftBrand and web addressChangeThrow away the brand draft. The live brand stays.
setup_publish_brandBrand and web addressAsks firstPublish the brand draft: every member sees it.
setup_revert_brandBrand and web addressAsks firstPut an earlier brand version back live.
setup_connect_domainBrand and web addressAsks firstConnect the agency’s own web address. The answer gives the DNS records to add.
setup_set_contactContact infoChangeSet the agency’s contact details and the welcome line new agents see. Only the keys given change.
setup_list_fieldsLead fields and stagesReadThe agency’s lead, policy and agent fields, and its lead stages.
setup_add_fieldsLead fields and stagesChangeAdd up to 20 fields at once. Each one shows on the leads, policies or agents it is for.
setup_update_fieldLead fields and stagesChangeChange one of the agency’s own fields: its label, hint, choices or whether it is required.
setup_set_stagesLead fields and stagesChangeSet the agency’s whole list of lead stages, in order. Every fixed stage needs at least one.
setup_list_tablesTablesReadThe agency’s custom tables, each with its fields and record count.
setup_create_tableTablesChangeCreate a custom table with up to 20 fields, published so agents can use it (or a draft).
setup_add_table_fieldsTablesChangeAdd up to 20 fields to one of the agency’s tables.
setup_list_supabase_tablesTablesReadThe tables in the agency’s linked Supabase project (each schema.table with its rough row count), to bring in.
setup_import_supabase_tableTablesChangeBring up to 10 tables in from the agency’s linked Supabase project as draft tables, in the background (we pick a field per column; Social Security, tax id, card, password, secret and token columns are left out). mode "copy" (the default) copies once; "sync" keeps each table in sync every 15 minutes, and then it is read-only in the portal (edit it in Supabase). Sync needs a primary key.
setup_add_table_viewTablesChangeSave a named view of a published table (its filters, sort and columns).
setup_list_rolesWho can do whatReadThe agency’s roles, what each one lets people do, who holds it, and the members. builtIns are Owner, Admin, Assistant and Member: each has its id (builtin:<tier>), what it grants here (grants), its defaults (defaultGrants), changed, and editable — true when this person may change it (only the agency owner; the Owner never).
setup_save_roleWho can do whatChange (asks first for a big one)Create a role, or change one, with the switches it turns on. A role grants nothing until someone holds it. To change what the built-in Member tier can do, pass role_id builtin:member and its full new switches. Admin and Assistant are the agency’s own starter roles: change them by their own role_id.
setup_reset_roleWho can do whatAsks firstPut the built-in Member tier back on today’s defaults.
setup_assign_roleWho can do whatAsks firstGive a member a role.
setup_get_comp_chartPay chartReadThe pay chart in force, the draft waiting and its open questions.
setup_save_comp_draftPay chartChangeChange the draft pay chart’s levels: rank names, codes, tracks and requirements, or how agents between levels are paid.
setup_publish_comp_chartPay chartAsks firstPublish the draft pay chart: new deals are paid from it.
setup_revert_comp_chartPay chartAsks firstPut an earlier pay chart version back.
setup_list_courseOnboarding courseReadThe agency’s onboarding course: its sections and their steps.
setup_add_course_itemsOnboarding courseChangeAdd up to 20 steps to the onboarding course, in a section that exists or a new one.
setup_install_starter_courseOnboarding courseChangeInstall the starter onboarding course. Only when the course is empty.
setup_add_trainingsWeekly trainingsChangeAdd up to 20 weekly trainings.
setup_update_trainingWeekly trainingsChangeChange one weekly training.
setup_get_kitStarter kitReadThe agency’s starter kit: each item, shared or not, and its draft.
setup_save_kit_itemStarter kitChangeCreate a kit item, or save a new draft of one. Nobody gets it until it is shared.
setup_share_kit_itemStarter kitChangeShare a kit item for the first time: every agent gets their copy.
setup_update_kit_itemStarter kitAsks firstPush the new draft of an already-shared kit item to every agent who has it.
setup_get_join_linkInvite your teamReadThe agency's two links. team_link is the one to send working agents: they sign up for EZ Leads and join the agency right away. recruit_link is the onboarding form for people being recruited and licensed. Share them exactly as returned.
setup_set_phone_policyExtrasChangeSet the agency’s phone rules: which features agents may use and the daily caps. Only the keys given change.
setup_set_phone_priceExtrasAsks firstSet next month’s price agents pay for a phone service (never under the floor), or put it back at the floor.
setup_export_configExtrasAsks firstExport the agency’s whole setup as one file (a backup).
setup_list_clone_targetsExtrasReadThe agencies below this one that its setup can be copied to.
setup_clone_setupExtrasAsks firstCopy this agency’s setup to an agency below it (or send it an offer to accept).
setup_post_announcementExtrasAsks firstPost an announcement to every member.
setup_distribute_leadsExtrasAsks firstHand out leads from the agency’s lead inventory: the newest count, round-robin or to one agent.
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, lead_type jsonb, 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.