Data Share API Reference
All endpoints require a franchise-scoped bearer token. See Data Share API Authentication.
The franchise is always derived from the token, so no franchise_id parameter is accepted.
Rate Limits
Each API key is limited to 60 requests per minute and 5,000 requests per day. Every response includes the current limit state:
| Header | Description |
|---|---|
X-RateLimit-Limit | Per-minute request limit |
X-RateLimit-Remaining | Requests remaining in the current minute |
X-RateLimit-Reset | Unix epoch (seconds) when the minute window resets |
X-RateLimit-Limit-Day | Per-day request limit |
X-RateLimit-Remaining-Day | Requests remaining in the current day |
When a limit is exceeded the API responds with 429 Too Many Requests and a Retry-After header (seconds to wait). Clients should honor Retry-After and back off.
Pagination
The calls, jobs, contacts, and calendar/events list endpoints use cursor (keyset) pagination. Each response returns a pagination object:
{ "page_size": 100, "has_more": true, "next_cursor": "eyJ0Ijoi..." }To fetch the next page, pass the returned next_cursor value back as the cursor query parameter. When has_more is false, next_cursor is null and there are no more records. Cursors are opaque — do not parse or construct them. page_size accepts 1–200 (default 100).
GET https://api.breesy.app/data/locations
Returns the locations in your franchise so you can filter other endpoints by location_id.
Query Parameters
None.
Example Request
GET https://api.breesy.app/data/locationsAuthorization: Bearer <api_token>Example Response
{ "franchise_id": "0c8b2a1e-1111-2222-3333-444455556666", "locations": [ { "location_id": "aaaa1111-2222-3333-4444-555566667777", "location_name": "North Branch" }, { "location_id": "bbbb1111-2222-3333-4444-555566667777", "location_name": "South Branch" } ]}GET https://api.breesy.app/data/calls
Returns call data with Breesy recording links per call. Two kinds of calls are returned, distinguished by call_type:
ai_agent— calls answered by the Breesy voice agent (daytime or after-hours). When the agent transfers the caller to a live person, the recorded “live patch” leg appears in the nestedpost_callobject (transcript, AI summary, sentiment, and its own recording link).breesy_phone— calls handled directly on Breesy Phone by your team: inbound, outbound, and internal (extension-to-extension, including transfers). Breesy Phone-specific data (AI summary, extracted entities, sentiment, who handled the call, direction) appears in the nestedbreesy_phoneobject.
Each call is returned once. When the voice agent answers a call, the phone system also records that leg against the agent’s own extension; that leg is not returned as a breesy_phone call because it is already the ai_agent record. When the voice agent transfers the caller to your office, the office leg is returned as a breesy_phone call, and the ai_agent record points to it through linked_breesy_phone_call_id when the link is known.
Query Parameters
| Name | Required | Description |
|---|---|---|
call_type | No | ai_agent or breesy_phone. Omit to receive both |
direction | No | Breesy Phone only: inbound, outbound, or internal. Implies call_type=breesy_phone; returns 400 when combined with call_type=ai_agent |
location_id | No | Limit to a single location UUID |
start_date | No | Inclusive start of the call timestamp range (ISO 8601 or YYYY-MM-DD) |
end_date | No | Inclusive end of the call timestamp range (ISO 8601 or YYYY-MM-DD) |
call_id | No | Exact match on the call’s conversation_id |
phone | No | Match any of the caller ID, dialed number, callback number, or customer phone, including the Breesy Phone caller, dialed, and customer numbers (digits are normalized) |
cursor | No | Opaque pagination cursor from a previous response’s next_cursor |
page_size | No | Records per page, 1–200 (default 100) |
include_recording_url | No | Set to false to omit recording links (faster for bulk listing). Default true |
Notes On The Recording Links
Each call includes two stable, Breesy-branded links:
recording_url— aGET https://api.breesy.app/data/recordings/{conversation_id}endpoint that streams or downloads the recording through Breesy. Call it with yourAuthorization: Bearer <api_token>header. The response is the audio file directly (audio/mpeg); the upstream storage provider is never exposed. This link does not expire from your side — access is re-authorized on every request — so you can store it safely. SeeGET /data/recordings/{conversation_id}below.recording_breesy_url— a permanent link that opens the call and its recording inside Breesy (sign-in required). Use this when a person should review the call.
Both fields may be null when no conversation_id is available, and are omitted entirely when include_recording_url=false.
When a call has a post-call (live patch) leg, post_call.recording_url links to that leg’s audio (?leg=post_call). It is null when post_call.recording_available is false.
Call Fields
| Field | Description |
|---|---|
call_type | ai_agent or breesy_phone |
agent_type | Underlying agent: daytime, afterhours, or breesy_phone |
caller_id, dialed_phone | Calling and dialed numbers |
transferred, transferred_number | Whether the voice agent transferred the caller, and to what number |
call_ended_reason | Voice-agent end reason. Common values: customer-ended-call, assistant-forwarded-call, assistant-ended-call, assistant-said-end-call-phrase, silence-timeout, customer-did-not-answer, customer-misdialed, carrier-disconnect, transfer-failed, system-error. null for Breesy Phone calls |
linked_breesy_phone_call_id | For a transferred ai_agent call, the conversation_id of the Breesy Phone call that received the transfer (when known) |
transcript | Primary transcript (voice agent, or the Breesy Phone conversation) |
breesy_phone | Present on breesy_phone calls, otherwise null. See Breesy Phone Fields |
post_call | Present when the call has a live patch leg, otherwise null. Contains transcript, ai_summary, entities, sentiment, recording_available, recording_url |
breesy_phone.ai_summary, breesy_phone.entities, and breesy_phone.sentiment are null until Breesy’s analysis runs, which is normally within a few minutes of the call ending. post_call fields fill in as the live patch recording is processed.
Breesy Phone Fields
| Field | Description |
|---|---|
direction | inbound (outside caller reached your team), outbound (your team called out), or internal (extension to extension, including a call transferred from one team member to another). Uses the same rules as Call Insights in Breesy, so counts reconcile with what you see in the app |
handled_by, handler_extension | Team member and extension that handled this leg |
origin_extension | Extension that placed the call, for outbound and internal calls. null for inbound |
caller_phone, dialed_phone | Numbers as recorded by the phone system for this leg |
customer_phone | The outside party’s number on outbound calls. null otherwise |
parent_call_id | Shared by every leg of the same phone call. When a call is transferred between team members, each leg is returned as its own breesy_phone call with the same parent_call_id; group on it to see the full path of the call |
state | Phone-system outcome, e.g. Connected or No answer. This is the Breesy Phone equivalent of call_ended_reason |
started_at, duration_ms | Start time and talk duration of this leg in milliseconds (duration at the top level is in seconds) |
ai_summary, entities, sentiment | Breesy’s analysis of the conversation; sentiment is 1–5 as text |
Example Request
GET https://api.breesy.app/data/calls?start_date=2026-06-01&end_date=2026-06-23&page_size=50Authorization: Bearer <api_token>Example Response
{ "scope": { "franchise_id": "0c8b2a1e-1111-2222-3333-444455556666", "location_id": null }, "pagination": { "page_size": 50, "has_more": true, "next_cursor": "eyJ0IjoiMjAyNi0wNi0yMlQxODowNDoxMSswMDowMCIsImkiOiJDQTAxMjMuLi4ifQ" }, "calls": [ { "conversation_id": "CA0123456789abcdef0123456789abcdef", "call_type": "ai_agent", "caller_id": "+15551234567", "dialed_phone": "+15550001111", "timestamp": "2026-06-22T18:04:11+00:00", "duration": "182", "agent_type": "afterhours", "call_category": "new_urgent_service_request", "call_ended_reason": "assistant-forwarded-call", "transferred": true, "transferred_number": "+15552223333", "linked_breesy_phone_call_id": null, "customer_name": "Jordan Smith", "customer_email": "jordan@example.com", "call_summary": "Water damage in the basement after a pipe burst.", "loss_type": "Water", "referral_source": "Google", "notes": "Caller requested a morning callback.", "property_area": "Basement", "request_type": "INITIAL_SERVICE_REQUEST", "service_location": "123 Main St, Springfield", "callback_number": "+15557654321", "location_id": "aaaa1111-2222-3333-4444-555566667777", "location": "North Branch", "transcript": "Agent: Thanks for calling...", "real_estate": { "address": "123 Main St, Springfield", "last_sale_value": "415000", "current_value": "468000", "owner_name1": "Jordan Smith", "owner_name2": null, "room_count": "7", "square_footage": "2100", "year_built": "1998" }, "breesy_phone": null, "post_call": { "transcript": "Tech: Hi Jordan, this is Mike...", "ai_summary": "Jordan confirmed the basement leak; Mike scheduled a visit for the morning.", "entities": { "customer_name": "Jordan", "loss_type": "water", "next_steps": "Morning visit" }, "sentiment": "4", "recording_available": true, "recording_url": "https://api.breesy.app/data/recordings/CA0123456789abcdef0123456789abcdef?leg=post_call" }, "recording_url": "https://api.breesy.app/data/recordings/CA0123456789abcdef0123456789abcdef", "recording_breesy_url": "https://www.breesy.app/insights?tab=calldata&callId=conv_CA0123456789abcdef0123456789abcdef" }, { "conversation_id": "20260622180411000000-0123456789abcdef0123456789abcdef", "call_type": "breesy_phone", "caller_id": "5551234567", "dialed_phone": "5550001111", "timestamp": "2026-06-22T18:04:11+00:00", "duration": "96", "agent_type": "breesy_phone", "call_summary": "Inbound call - Duration: 1m 36s", "location_id": "aaaa1111-2222-3333-4444-555566667777", "location": "North Branch", "transcript": "Team member: Thanks for calling...", "breesy_phone": { "ai_summary": "Customer called to confirm the crew's arrival window for tomorrow's dry-out.", "entities": { "customer_name": "Jordan Smith", "job_number": "2026-0042", "loss_type": "water" }, "sentiment": "4", "handled_by": "Alex Chen", "handler_extension": "151", "origin_extension": null, "direction": "inbound", "state": "Connected", "started_at": "2026-06-22T18:04:11+00:00", "duration_ms": 96000, "customer_phone": null, "caller_phone": "5551234567", "dialed_phone": "5550001111", "parent_call_id": "20260622180411000000-0123456789abcdef0123456789abcdef" }, "post_call": null, "recording_url": "https://api.breesy.app/data/recordings/20260622180411000000-0123456789abcdef0123456789abcdef", "recording_breesy_url": "https://www.breesy.app/insights?tab=calldata&callId=conv_20260622180411000000-0123456789abcdef0123456789abcdef" } ]}GET https://api.breesy.app/data/recordings/{conversation_id}
Streams or downloads a single call recording. Works for both voice-agent (ai_agent) and Breesy Phone (breesy_phone) calls. The recording is resolved against your franchise on every request, so you can only fetch recordings for calls that belong to you.
Path / Query Parameters
| Name | Required | Description |
|---|---|---|
conversation_id | Yes | The call’s conversation_id (path segment, or ?conversation_id= / ?call_id= query) |
leg | No | primary (default) for the voice-agent or Breesy Phone audio, or post_call for the live patch leg recorded after a transfer |
Behavior
- Default — responds with
200 OKand streams the.mp3through Breesy (Content-Type: audio/mpeg,Accept-Ranges: bytes). Use this URL in an<audio>element, a download link, or any HTTP client. The storage provider is never shown to the client. Accept: application/json— returns metadata only (leg,call_type,recording_url,breesy_url,content_type) without streaming audio. Useful when you want to confirm the call exists before playing it. Note that this does not confirm the audio file itself has been stored yet; issue a normal (orRange: bytes=0-0) request for that.- This Breesy link itself is stable and does not expire; authorization is checked on every request.
Example Request
GET https://api.breesy.app/data/recordings/CA0123456789abcdef0123456789abcdefAuthorization: Bearer <api_token>Example Metadata Response (Accept: application/json)
{ "conversation_id": "CA0123456789abcdef0123456789abcdef", "recording_url": "https://api.breesy.app/data/recordings/CA0123456789abcdef0123456789abcdef", "breesy_url": "https://www.breesy.app/insights?tab=calldata&callId=conv_CA0123456789abcdef0123456789abcdef", "content_type": "audio/mpeg"}Expected Errors
| Status | reason | Meaning |
|---|---|---|
404 | not_found | The call does not exist in your franchise, has no recording on file, or (for leg=post_call) has no live patch leg |
404 | not_synced | A Breesy Phone call whose audio has not been copied into Breesy storage yet. Retry later |
404 | not_available | A live patch leg exists but its audio was never stored in Breesy, so it cannot be served |
400 | leg is not primary or post_call, or conversation_id is missing |
GET https://api.breesy.app/data/jobs
Returns job records for franchises with Breesy Jobs enabled, including live status, status history, loss details, referral source, insurance/carrier data, schedule, assigned team, and invoicing summary.
Responds with 403 when Breesy Jobs is not enabled for the franchise the token is scoped to.
Query Parameters
| Name | Required | Description |
|---|---|---|
location_id | No | Limit to a single location UUID |
status | No | Filter by job status: lead, scheduled, assessment, estimate_sent, approved, in_progress, quality_check, completed, invoiced, paid_closed, cancelled |
job_id | No | Exact match on the job’s UUID |
job_number | No | Exact match on the job number (e.g. 2026-0042) |
start_date | No | Inclusive start of the job created-at range (ISO 8601 or YYYY-MM-DD) |
end_date | No | Inclusive end of the job created-at range (ISO 8601 or YYYY-MM-DD) |
updated_since | No | Only jobs updated at or after this timestamp — useful for incremental sync |
cursor | No | Opaque pagination cursor from a previous response’s next_cursor |
page_size | No | Records per page, 1–200 (default 100) |
Example Request
GET https://api.breesy.app/data/jobs?status=in_progress&page_size=50Authorization: Bearer <api_token>Example Response
{ "scope": { "franchise_id": "0c8b2a1e-1111-2222-3333-444455556666", "location_id": null }, "pagination": { "page_size": 50, "has_more": false, "next_cursor": null }, "jobs": [ { "job_id": "dddd1111-2222-3333-4444-555566667777", "job_number": "2026-0042", "job_name": "Smith Residence — Basement Water Loss", "job_status": "in_progress", "status_updated_at": "2026-07-28T14:22:05+00:00", "priority": "high", "created_at": "2026-07-20T09:15:00+00:00", "updated_at": "2026-07-30T16:40:12+00:00", "customer": { "name": "Jordan Smith", "contact_id": 10422, "property_address": "123 Main St, Springfield", "property_type": "residential", "commercial_property_name": null }, "loss": { "date_of_loss": "2026-07-19", "source_of_loss": "Burst supply line", "damage_type": "Water", "loss_types": [ { "name": "Water Mitigation", "slug": "water_mitigation", "is_primary": true } ] }, "referral": { "source": "Insurance Agent", "notes": null }, "insurance": { "carrier": "State Farm", "claim_number": "55-1234-X89", "policy_number": "SF-99881122", "deductible": 1000, "claim_status": "approved", "claim_status_updated_at": "2026-07-25T11:02:00+00:00", "adjuster_name": "Pat Rivera", "adjuster_phone": "+15550001111", "adjuster_email": "pat.rivera@example.com", "payment_type": "insurance" }, "schedule": { "start_date": "2026-07-21", "target_completion_date": "2026-08-05", "actual_completion_date": null }, "team": { "project_manager": "Alex Chen", "estimator": "Sam Lee", "coordinator": null }, "accounting": { "invoice_number": "INV-2081", "invoice_date": "2026-07-29", "invoice_amount": 18450.00, "amount_paid": 9225.00, "payment_status": "partially_paid", "payment_due_date": "2026-08-28", "estimate_total": 18450.00 }, "counts": { "attachments": 34, "notes": 12 }, "location": { "location_id": "aaaa1111-2222-3333-4444-555566667777", "location_name": "North Branch" }, "closure": { "closed_at": null, "closure_reason": null }, "status_history": [ { "old_status": null, "new_status": "lead", "reason": null, "changed_at": "2026-07-20T09:15:00+00:00" }, { "old_status": "lead", "new_status": "in_progress", "reason": null, "changed_at": "2026-07-28T14:22:05+00:00" } ] } ]}GET https://api.breesy.app/data/contacts
Reads contacts from your CRM. Returns a paginated list, or a single contact when id is supplied.
Query Parameters
| Name | Required | Description |
|---|---|---|
id | No | Return a single contact by its numeric ID |
phone | No | Match the primary or alternate phone (digits are normalized) |
search | No | Case-insensitive match on name, organization, email, or phone |
location_id | No | Limit to contacts tracked at a location UUID |
cursor | No | Opaque pagination cursor from a previous response’s next_cursor |
page_size | No | Records per page, 1–200 (default 100) |
Example Request
GET https://api.breesy.app/data/contacts?search=jordan&page_size=25Authorization: Bearer <api_token>Example Response
{ "scope": { "franchise_id": "0c8b2a1e-1111-2222-3333-444455556666" }, "pagination": { "page_size": 25, "has_more": false, "next_cursor": null }, "contacts": [ { "id": 4821, "first_name": "Jordan", "last_name": "Smith", "organization_name": null, "is_business": false, "primary_phone": "+15551234567", "primary_email": "jordan@example.com", "alternate_phones": [], "alternate_emails": [], "tags": ["VIP"], "primary_contact_tags": [], "notes": "Prefers morning calls.", "referred_by": "Google", "address": { "line1": "123 Main St", "line2": null, "city": "Springfield", "state": "IL", "postal": "62704", "country": "US" }, "location_id": ["aaaa1111-2222-3333-4444-555566667777"], "total_calls": 3, "last_contact_date": "2026-06-22T18:04:11+00:00", "lifetime_value": 12500.0, "is_verified": true, "created_at": "2026-01-10T12:00:00+00:00", "updated_at": "2026-06-22T18:10:00+00:00" } ]}PATCH https://api.breesy.app/data/contacts
Updates an existing contact. Only the fields you include are changed.
Request Body
| Field | Required | Description |
|---|---|---|
id | Yes | The numeric contact ID to update |
first_name | No | |
last_name | No | |
organization_name | No | |
is_business | No | Boolean |
primary_phone | No | Normalized to E.164 on save; must be a valid phone |
primary_email | No | |
alternate_phones | No | Array of phone strings |
alternate_emails | No | Array of email strings |
tags | No | Array of tag strings |
primary_contact_tags | No | Array of tag strings |
notes | No | |
referred_by | No | |
addr_line1, addr_line2, addr_city, addr_state, addr_postal, addr_country | No | Address fields |
location_id | No | Array of location UUID strings |
Example Request
PATCH https://api.breesy.app/data/contactsAuthorization: Bearer <api_token>Content-Type: application/json
{ "id": 4821, "tags": ["VIP", "Repeat Customer"], "notes": "Signed annual maintenance agreement."}Example Response
{ "contact": { "id": 4821, "first_name": "Jordan", "last_name": "Smith", "tags": ["VIP", "Repeat Customer"], "notes": "Signed annual maintenance agreement." }}POST https://api.breesy.app/data/contacts
Creates a contact. Breesy matches on phone number to avoid duplicates; if a matching contact already exists, that contact is updated with the supplied fields and matched_existing is true.
Request Body
primary_phone is required. All other writable fields are optional.
Example Request
POST https://api.breesy.app/data/contactsAuthorization: Bearer <api_token>Content-Type: application/json
{ "first_name": "Alex", "last_name": "Doe", "primary_phone": "555-987-6543", "primary_email": "alex@example.com", "tags": ["New Lead"]}Example Response
{ "contact": { "id": 5099, "first_name": "Alex", "last_name": "Doe", "primary_phone": "+15559876543", "primary_email": "alex@example.com", "tags": ["New Lead"] }, "matched_existing": false}Expected Errors
| Status | Meaning |
|---|---|
400 | Missing required field, invalid JSON, invalid primary_phone, or invalid cursor |
401 | Missing, invalid, expired, or revoked bearer token |
404 | The contact id does not exist within this franchise (PATCH) |
429 | Rate limit exceeded — honor the Retry-After header and retry later |
POST https://api.breesy.app/data/sms
Sends an outbound SMS from a Breesy Phone number that belongs to your franchise. The franchise is derived from the API key. Before sending, Breesy confirms that Breesy Phone is enabled for the account and that the from number is both owned by the franchise and enabled for texting.
This endpoint requires Breesy Phone.
Request Body
| Name | Required | Description |
|---|---|---|
to | Yes | Destination phone number (10-digit US number, with or without formatting) |
from | Yes | Your Breesy Phone number to send from. Must be assigned to this franchise |
message | Yes | Text to send. Maximum 1,600 characters |
destination, from_number, and body are accepted as aliases.
Example Request
POST https://api.breesy.app/data/smsAuthorization: Bearer <api_token>Content-Type: application/json
{ "to": "5207712736", "from": "8156974275", "message": "Your technician is on the way."}Example Response
{ "success": true, "status": "Accepted", "message_id": "m-6a8389e47e971", "session_id": "sms_a1b2c3d4e5f6...", "from": "+18156974275", "to": "+15207712736"}A successful send returns 202 Accepted. message_id may be null until the carrier assigns one; status comes from Breesy Phone.
Expected Errors
| Status | Code | Meaning |
|---|---|---|
400 | invalid_json | Body is not valid JSON |
400 | missing_to / missing_from / missing_message | Required field is missing |
400 | invalid_to / invalid_from | Phone number is not a valid 10-digit US number |
400 | same_from_and_to | from and to are the same number |
400 | message_too_long | message is longer than 1,600 characters |
401 | Missing, invalid, expired, or revoked bearer token | |
403 | from_number_not_owned | from is not a Breesy Phone number on this account |
403 | from_number_disabled | from belongs to this account but is currently disabled |
403 | from_number_not_sms_enabled | from is not enabled for texting |
409 | breesy_phone_not_enabled | Breesy Phone is not enabled for this account |
409 | breesy_phone_not_configured | Breesy Phone is not set up for this account |
429 | Rate limit exceeded — honor the Retry-After header and retry later | |
502 | sms_send_failed | Breesy Phone accepted the request format but could not send the text |
Calendar
The calendar endpoints expose the Breesy Calendar page — Job Scheduling and Service Routes — for your franchise. They require Breesy Calendar to be enabled on the account; otherwise every calendar endpoint returns 403.
The usual flow for booking is:
GET /data/calendar/assigneesonce to learn the location ids, team members, teams, and scheduling rules.GET /data/calendar/availabilityto find open times for a person (or the whole location).POST /data/calendar/eventsto book, thenPATCH/DELETEto change or cancel.
All times are ISO 8601. Send times in UTC (Z) or with an explicit offset; responses are returned in UTC. Each location has a timezone that its scheduling window is interpreted in.
Appointments booked through the API appear on the Calendar page immediately, are recorded in the Change Log as Data Share API - token name, and are pushed to the assigned person’s connected Outlook/Google calendar (when they have one) within about a minute, using the same sync the app uses. Customer email/SMS alerts are off by default for API bookings; pass notify_customer_email / notify_customer_sms as true to opt in.
GET https://api.breesy.app/data/calendar/assignees
Returns, per location: the scheduling settings, active team members, teams (groups of team members), and time blocks.
Query Parameters
| Name | Required | Description |
|---|---|---|
location_id | No | Limit to one location (UUID) |
include_inactive | No | true to include inactive team members and teams |
Example Response
{ "scope": { "franchise_id": "79c038ba-...", "location_id": null }, "locations": [ { "id": "48aac354-...", "name": "Central-Town-60050", "timezone": "America/Chicago", "scheduling": { "window_start": "08:00:00", "window_end": "17:00:00", "days_of_week": [1, 2, 3, 4, 5], "allow_weekend_scheduling": false, "default_appointment_duration_minutes": 60, "fixed_travel_buffer_minutes": 15, "assignment_strategy": "round_robin", "skill_based_scheduling": true }, "team_members": [ { "id": "0b725dc6-...", "name": "Kyle McDougall", "email": "kyle@example.com", "phone_number": "+15125550100", "role": "Project Manager", "is_active": true, "skills": ["Plumbing", "Water Mitigation"], "max_events_per_day": 5, "has_connected_calendar": true } ], "teams": [ { "id": "aff93bde-...", "name": "Mitigation Crew A", "color": "#2563eb", "is_active": true, "skills": [], "max_events_per_day": 5, "team_member_ids": ["0b725dc6-..."] } ], "time_blocks": [ { "id": "37af2e00-...", "name": "Morning Block", "start_time": "08:00:00", "end_time": "12:00:00", "days_of_week": [1, 2, 3, 4, 5], "max_concurrent_jobs": 5, "jobs_per_person": 3, "eligible_team_member_ids": ["0b725dc6-..."], "eligible_team_ids": [], "eligible_roles": [] } ] } ]}days_of_week uses 0 = Sunday through 6 = Saturday. has_connected_calendar tells you whether a booking for that person will also be pushed to their Outlook/Google calendar.
GET https://api.breesy.app/data/calendar/availability
Computes open appointment slots for one location over a date range (up to 31 days). A slot is open for a person when it is inside the scheduling window, does not overlap anything already on their calendar (including buffers and travel time), and they are under their daily appointment limit.
Query Parameters
| Name | Required | Description |
|---|---|---|
location_id | Yes | Location UUID |
start_date | Yes | First day to check (YYYY-MM-DD, in the location’s timezone) |
end_date | No | Last day to check (defaults to start_date; max 31 days) |
duration_minutes | No | Appointment length (defaults to the location’s default, usually 60) |
slot_minutes | No | Spacing between candidate start times (15–240, default 30) |
team_member_id | No | Only check this person |
team_id | No | Only check members of this team |
Example Request
GET https://api.breesy.app/data/calendar/availability?location_id=48aac354-...&start_date=2026-10-22&team_member_id=0b725dc6-...&slot_minutes=60Authorization: Bearer <api_token>Example Response
{ "scope": { "franchise_id": "79c038ba-...", "location_id": "48aac354-..." }, "location_id": "48aac354-...", "timezone": "America/Chicago", "duration_minutes": 60, "slot_minutes": 60, "window_start": "08:00:00", "window_end": "17:00:00", "days_of_week": [1, 2, 3, 4, 5], "team_members": [{ "id": "0b725dc6-...", "name": "Kyle McDougall" }], "days": [ { "date": "2026-10-22", "slots": [ { "start_time": "2026-10-22T13:00:00+00:00", "end_time": "2026-10-22T14:00:00+00:00", "available_team_member_ids": ["0b725dc6-..."] }, { "start_time": "2026-10-22T14:00:00+00:00", "end_time": "2026-10-22T15:00:00+00:00", "available_team_member_ids": ["0b725dc6-..."] } ] } ]}Only slots with at least one available person are returned. Days the location does not schedule on are omitted. Slots in the past are never returned.
GET https://api.breesy.app/data/calendar/events
Lists appointments (and other calendar entries) for the franchise, ordered by start time. Supports cursor pagination (page_size 1–500, default 100).
Query Parameters
| Name | Required | Description |
|---|---|---|
location_id | No | Limit to one location (UUID) |
start_time | No | Return entries that end at or after this time (ISO 8601 or YYYY-MM-DD) |
end_time | No | Return entries that start at or before this time |
team_member_id | No | Only entries on this person’s calendar (assigned to them, on a team they belong to, or imported from their connected calendar) |
event_type | No | service_job (appointments), personal (imported from a connected calendar), buffer, or travel |
page_size | No | Items per page (1–500, default 100) |
cursor | No | Opaque pagination cursor from a previous response’s next_cursor |
Event Fields
| Field | Description |
|---|---|
id | Appointment id (UUID). Use it with the single-event, PATCH, and DELETE endpoints |
location_id, location_name | Owning location |
event_type | service_job, personal, buffer, or travel |
title, description | Appointment title and notes |
start_time, end_time, timezone | When; timezone is the location’s timezone |
assigned_team_member_id, assigned_team_member_name | Person the appointment is assigned to (or null) |
assigned_team_id, assigned_team_name | Team the appointment is assigned to (or null) |
calendar_owner_email | For personal entries, whose connected calendar it came from |
customer_address | Service address |
customer | { name, phone, email } |
job | { dash_job_number, dash_job_id, dash_job_name, loss_description } |
priority | low, normal, high, or vip |
job_type | Free-text job type |
locked_schedule, allow_time_adjustment | Route-optimization flags (a locked appointment is never moved; allow_time_adjustment lets the optimizer shift it) |
notify_customer_email, notify_customer_sms, notifications_pending | Customer alert settings |
synced_to_external_calendar, sync_status | Whether the entry is on the assignee’s connected calendar (synced, pending_retry, failed, or null when not applicable) |
created_at, updated_at | Timestamps |
Entries imported from a person’s connected calendar whose privacy setting is “busy only” are returned with the title Busy and no details.
Example Response
{ "scope": { "franchise_id": "79c038ba-...", "location_id": "48aac354-..." }, "pagination": { "page_size": 100, "has_more": false, "next_cursor": null }, "events": [ { "id": "7855c806-...", "location_id": "48aac354-...", "location_name": "Central-Town-60050", "event_type": "service_job", "title": "Monitoring visit - Nora Espinoza", "description": "Clean up debris", "start_time": "2026-10-19T13:00:00+00:00", "end_time": "2026-10-19T14:00:00+00:00", "timezone": "America/Chicago", "assigned_team_member_id": "0b725dc6-...", "assigned_team_member_name": "Kyle McDougall", "assigned_team_id": null, "assigned_team_name": null, "calendar_owner_email": null, "customer_address": "616 Meadow Ln, Georgetown, TX 78626", "customer": { "name": "Nora Espinoza", "phone": "+15125550136", "email": "nora@example.com" }, "job": { "dash_job_number": "26-BREESY-NE-REC-031", "dash_job_id": null, "dash_job_name": "Nora Espinoza - Laundry Room Rebuild", "loss_description": "Washer hose failure." }, "priority": "normal", "job_type": null, "locked_schedule": true, "allow_time_adjustment": false, "notify_customer_email": false, "notify_customer_sms": false, "notifications_pending": false, "synced_to_external_calendar": true, "sync_status": "synced", "created_at": "2026-10-18T16:10:53+00:00", "updated_at": "2026-10-19T14:16:48+00:00" } ]}GET https://api.breesy.app/data/calendar/events/{event_id}
Returns one appointment as { "event": { ... } }, or 404 when the id does not exist within this franchise.
GET https://api.breesy.app/data/calendar/routes
The Service Routes view: for one location and one day, each team member’s stops in time order plus their route start address.
Query Parameters
| Name | Required | Description |
|---|---|---|
location_id | Yes | Location UUID |
date | Yes | Day (YYYY-MM-DD, in the location’s timezone) |
team_member_id | No | Only this person’s route |
Example Response
{ "scope": { "franchise_id": "79c038ba-...", "location_id": "48aac354-..." }, "location_id": "48aac354-...", "date": "2026-10-19", "timezone": "America/Chicago", "routes": [ { "team_member_id": "0b725dc6-...", "team_member_name": "Kyle McDougall", "start_address": "4949 E Van Buren St, Phoenix, AZ 85026", "last_optimization": null, "stops": [ { "sequence": 1, "event_id": "7855c806-...", "title": "Monitoring visit - Nora Espinoza", "start_time": "2026-10-19T13:00:00+00:00", "end_time": "2026-10-19T14:00:00+00:00", "address": "616 Meadow Ln, Georgetown, TX 78626", "customer_name": "Nora Espinoza", "customer_phone": "+15125550136", "dash_job_number": "26-BREESY-NE-REC-031", "job_type": null, "locked_schedule": true, "allow_time_adjustment": false } ] } ]}last_optimization is { source, created_at, notified } when a route optimization has run for that person and day, otherwise null. Active team members with no appointments that day are included with an empty stops list.
POST https://api.breesy.app/data/calendar/events
Books an appointment. Before saving, Breesy checks that the time is inside the location’s scheduling window, that the assigned person has nothing overlapping (including buffers and travel time), and that they are under their daily limit. A failed check returns 409 with details and nothing is saved.
Request Body
| Name | Required | Description |
|---|---|---|
location_id | Yes | Location UUID (from assignees) |
title | Yes | Appointment title |
start_time | Yes | ISO 8601 start |
end_time | No | ISO 8601 end. If omitted, duration_minutes or the location default (usually 60) is used |
duration_minutes | No | Used only when end_time is omitted |
assigned_team_member_id | No | Person to assign (UUID from assignees) |
assigned_team_id | No | Team to assign (UUID from assignees). Provide a person or a team, not both |
description | No | Notes |
customer_address | No | Service address (also used as the route stop) |
customer | No | { "name", "phone", "email" } |
job | No | { "dash_job_number", "dash_job_id", "dash_job_name", "loss_description" } |
priority | No | low, normal (default), high, or vip |
job_type | No | Free-text job type |
locked_schedule | No | true to keep route optimization from moving this appointment (default false) |
allow_time_adjustment | No | true to let route optimization shift the time (default false; cannot be combined with locked_schedule) |
notify_customer_email / notify_customer_sms | No | Customer alert opt-in (default false) |
override_validation | No | true to skip the window, conflict, and daily-limit checks (recorded in the Change Log). Use sparingly |
Example Request
POST https://api.breesy.app/data/calendar/eventsAuthorization: Bearer <api_token>Content-Type: application/json
{ "location_id": "48aac354-...", "title": "Water mitigation - initial inspection", "start_time": "2026-10-22T14:00:00Z", "duration_minutes": 60, "assigned_team_member_id": "358248dd-...", "customer_address": "616 Meadow Ln, Georgetown, TX 78626", "customer": { "name": "Jane Doe", "phone": "+15125550199" }, "job": { "dash_job_number": "26-ABC-001" }}Example Response
201 Created
{ "event": { "id": "fad5718a-...", "title": "Water mitigation - initial inspection", "start_time": "2026-10-22T14:00:00+00:00", "end_time": "2026-10-22T15:00:00+00:00", "assigned_team_member_id": "358248dd-...", "assigned_team_member_name": "Bill Smith", "...": "..." }, "external_calendar_sync": "not_applicable"}external_calendar_sync is requested when the assignee has a connected calendar (the push happens in the background; watch sync_status on the event), otherwise not_applicable.
PATCH https://api.breesy.app/data/calendar/events/{event_id}
Reschedules, reassigns, or edits an appointment. Send only the fields you want to change; the same field names as POST are accepted. Sending start_time without end_time keeps the existing duration. Set assigned_team_member_id to null to unassign. When the time or the person changes, the same window / conflict / daily-limit checks run (excluding the appointment itself).
Entries imported from a person’s connected calendar (event_type = personal) cannot be changed or cancelled through the API.
Example Request
PATCH https://api.breesy.app/data/calendar/events/fad5718a-...Authorization: Bearer <api_token>Content-Type: application/json
{ "start_time": "2026-10-22T16:00:00Z", "assigned_team_member_id": "0b725dc6-..." }Example Response
{ "event": { "id": "fad5718a-...", "start_time": "2026-10-22T16:00:00+00:00", "end_time": "2026-10-22T17:00:00+00:00", "assigned_team_member_name": "Kyle McDougall", "...": "..." }, "changed_fields": ["start_time", "end_time", "assigned_user_id"], "external_calendar_sync": "requested"}external_calendar_sync is one of updated (the entry on the connected calendar was changed), moved / requested (reassigned; the old copy was removed and the new person’s calendar will be updated in the background), unchanged, not_applicable, or failed (the Breesy appointment was saved but the connected calendar could not be updated — Breesy keeps retrying).
DELETE https://api.breesy.app/data/calendar/events/{event_id}
Cancels an appointment and removes it from the assignee’s connected calendar. POST /data/calendar/events/{event_id}/cancel with an optional JSON body { "reason": "..." } does the same and records the reason in the Change Log.
Example Response
{ "deleted": true, "event_id": "fad5718a-...", "external_calendar_sync": "removed" }Calendar Errors
| Status | Code | Meaning |
|---|---|---|
400 | title_required, start_time_required, invalid_time, invalid_time_range, invalid_priority, invalid_lock_flags, assignee_conflict, assignee_invalid, location_id_required | Request body problem (see message) |
401 | Missing, invalid, expired, or revoked bearer token | |
403 | Breesy Calendar is not enabled for this franchise | |
404 | event_not_found, location_not_found, team_member_not_found, team_not_found | The id does not exist within this franchise |
409 | time_conflict | The person already has something overlapping. conflicts lists the overlapping entries |
409 | daily_limit_reached | The person is at max_events_per_day for that day |
409 | outside_scheduling_window | Outside the location’s scheduling days/hours (window_start, window_end, timezone are included) |
409 | event_not_editable | The entry came from a connected personal calendar and cannot be changed via the API |
429 | Rate limit exceeded — honor the Retry-After header and retry later |