{"openapi":"3.1.0","info":{"title":"Mendly Agent API","version":"1.0.0","description":"A versioned, authenticated HTTP API for operating Mendly programmatically. Agents can submit service requests, read the catalogue and the dispatchable professionals, and, with the admin scopes, run the platform. Every key carries scopes; a key can only do what it was granted, and every scoped call is recorded server-side. Request a key at https://mendly.services/developers.","contact":{"email":"partnerships@mendly.services"}},"servers":[{"url":"https://mendly.services/api/v1","description":"Production"}],"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"Your Mendly API key as the bearer token. Keys carry scopes: requests:write, requests:read, catalog:read, providers:read, providers:write, jobs:write, admin:read, admin:write. A call outside the key's scopes returns 403 naming the scope required. admin:write is never granted by default."}},"schemas":{"CreateRequestBody":{"type":"object","required":["problem"],"properties":{"problem":{"type":"string","description":"Natural-language description of the issue."},"urgency":{"type":"string","enum":["low","medium","high","emergency"]},"hints":{"type":"object","properties":{"service_type":{"type":"string"},"location":{"type":"string"},"contact_name":{"type":"string"},"contact_email":{"type":"string"},"contact_phone":{"type":"string"},"preferred_time":{"type":"string"}}},"media_refs":{"type":"array","items":{"type":"string"}},"webhook_url":{"type":"string","format":"uri"}}},"CreateResponse":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"},"job_draft_id":{"type":"string","format":"uuid"},"status":{"type":"string"},"classified_service_type":{"type":"string"},"classified_category":{"type":"string"},"intent":{"type":"string"},"urgency":{"type":"string"},"location":{"type":"string"},"confidence":{"type":"number"},"agent_originated":{"type":"boolean"}}},"StatusResponse":{"type":"object","properties":{"request_id":{"type":"string","format":"uuid"},"status":{"type":"string"},"job_draft_id":{"type":"string","format":"uuid"},"job_status":{"type":"string","nullable":true},"workflow_state":{"type":"string","nullable":true},"provider_assigned":{"type":"boolean"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"Error":{"type":"object","properties":{"error":{"type":"string"}}}}},"security":[{"BearerAuth":[]}],"paths":{"/catalog":{"get":{"summary":"List the services Mendly covers","description":"Scope: catalog:read. You do not need a service id to create a request; describe the problem in plain language and Mendly classifies it. Use this to tell a user whether Mendly covers what they need before submitting.","operationId":"listCatalog","responses":{"200":{"description":"Catalogue"},"403":{"description":"Missing catalog:read scope"}}}},"/providers":{"get":{"summary":"List professionals who can take work","description":"Scope: providers:read. Returns only approved, listed and unpaused professionals by default, because a wider list becomes a promise Mendly cannot keep. include_unavailable=true additionally requires admin:read. Contact details are never returned.","operationId":"listProviders","parameters":[{"name":"service","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"default":50}},{"name":"include_unavailable","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"Professionals"},"403":{"description":"Missing scope"}}}},"/admin/overview":{"get":{"summary":"Operating numbers and dispatch health","description":"Scope: admin:read. Includes health.can_dispatch, which distinguishes a quiet platform from a broken one. Zero dispatchable professionals means every request will be created and then stall, and the job counts look identical in both cases.","operationId":"adminOverview","responses":{"200":{"description":"Overview"},"403":{"description":"Missing admin:read scope"}}}},"/admin/providers/{id}":{"patch":{"summary":"Update a professional","description":"Scope: admin:write. Allow-listed fields only: visibility, onboarding_status, do_not_book, business_name, phone, bio. Any other field is refused by name rather than silently ignored.","operationId":"updateProvider","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Updated"},"400":{"description":"Field not settable"},"403":{"description":"Missing admin:write scope"}}},"delete":{"summary":"Delete a professional and everything attached to them","description":"Scope: admin:write. Removes the login, the email address and all jobs, bookings, appointments, messages and calendar connections. Irreversible. The route verifies the row is gone and returns 500 rather than reporting a delete that did not happen. Recorded in account_deletions before it runs.","operationId":"deleteProvider","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted"},"403":{"description":"Missing admin:write scope"},"404":{"description":"Not found"}}}},"/admin/customers":{"get":{"summary":"List customers","description":"Scope: admin:read. Paginated (limit, default 50, max 200; offset). Optional q searches name/email/phone, status filters exactly. Returns the same sane default field set the admin dashboard's customer table shows: id, full_name, email, phone, city, state, zip_code, status, customer_type, property_type, created_at. The rest of the profile (address, notes, square footage, AI summary, ...) is one GET away at /admin/customers/{id}.","operationId":"listCustomers","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string"}},{"name":"status","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"default":50}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"Customers"},"403":{"description":"Missing admin:read scope"}}}},"/admin/customers/{id}":{"get":{"summary":"Read a customer's full profile","description":"Scope: admin:read.","operationId":"getCustomer","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Customer"},"403":{"description":"Missing admin:read scope"},"404":{"description":"Not found"}}},"patch":{"summary":"Update a customer","description":"Scope: admin:write. Allow-listed fields only: full_name, email, phone, address, city, state, zip_code, status, notes, customer_type, property_type, square_footage, room_count, preferred_contact_method, urgency_preference, ai_summary. Any other field is refused by name rather than silently ignored.","operationId":"updateCustomer","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Updated"},"400":{"description":"Field not settable"},"403":{"description":"Missing admin:write scope"}}},"delete":{"summary":"Delete a customer and everything attached to them","description":"Scope: admin:write. Removes the login, the email address and all jobs, bookings, appointments and messages. Irreversible. The route verifies the row is gone and returns 500 rather than reporting a delete that did not happen. Recorded in account_deletions before it runs.","operationId":"deleteCustomer","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted"},"403":{"description":"Missing admin:write scope"},"404":{"description":"Not found"}}}},"/admin/settings":{"get":{"summary":"Read platform settings","description":"Scope: admin:read. Returns only the agent-editable subset, not the full admin settings surface: require_pro_verification, require_event_vendor_verification, require_provider_approval, auto_assign_providers, allow_customer_self_booking, platform_fee_percentage, provider_payout_percentage, dispatch_default_radius_miles.","operationId":"getSettings","responses":{"200":{"description":"Settings"},"403":{"description":"Missing admin:read scope"}}},"patch":{"summary":"Change platform settings","description":"Scope: admin:write. Allow-listed fields only: require_pro_verification, require_event_vendor_verification, require_provider_approval, auto_assign_providers, allow_customer_self_booking, platform_fee_percentage, provider_payout_percentage, dispatch_default_radius_miles. Any other field is refused by name. Secrets (external_ai_api_key), the webhook destination, the payment processor choice, and the controls governing the automation layer itself (require_human_approval, rate_limit_ai_actions, max_ai_jobs_per_day, ai_operating_mode) are deliberately not settable through this API.","operationId":"updateSettings","responses":{"200":{"description":"Updated"},"400":{"description":"Field not settable"},"403":{"description":"Missing admin:write scope"}}}},"/jobs/{id}/respond":{"post":{"summary":"Accept or decline a job on a professional's behalf","description":"Scope: jobs:write. acting_provider_id is required: an API key is not a person, so there is no current professional to infer, and the job must already be assigned to the one named. Answering a job that has a different answer returns 409 with the existing answer rather than overwriting it; repeating the same answer succeeds, so retries are safe.","operationId":"respondToJob","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action","acting_provider_id"],"properties":{"action":{"type":"string","enum":["accepted","declined"]},"acting_provider_id":{"type":"string","format":"uuid"},"decline_reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Recorded"},"403":{"description":"Not assigned to that professional"},"409":{"description":"Already answered differently"}}}},"/jobs/{id}/cancel":{"post":{"summary":"Cancel a job","description":"Scope: jobs:write. Also removes the event from the professional's connected calendar. Read calendar_event_removed: if false, the slot is still blocked in their calendar and they will quietly stop being offered work in that window. Cancelling an already-cancelled job returns success.","operationId":"cancelJob","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Cancelled"},"404":{"description":"Not found"}}}},"/jobs/{id}/schedule":{"post":{"summary":"Propose a time for an accepted job","description":"Scope: jobs:write. Propose, not confirm: a customer who arranged a visit did not agree to let software decide when a stranger arrives at their home, so the time is offered and they accept it. Availability is checked against Mendly's own bookings and the professional's connected calendar; a busy slot returns 409 with the conflicting periods. If Mendly cannot read its own bookings it returns 503 rather than claiming anyone is free. If an external calendar is unreachable the call proceeds and reports it in availability_checked.external_errors.","operationId":"proposeJobTime","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["date","time","acting_provider_id"],"properties":{"date":{"type":"string","example":"2026-08-20"},"time":{"type":"string","example":"14:00"},"acting_provider_id":{"type":"string","format":"uuid"},"duration_minutes":{"type":"integer","default":120},"note":{"type":"string"}}}}}},"responses":{"200":{"description":"Proposed"},"409":{"description":"Not accepted yet, or the professional is not free"},"503":{"description":"Availability could not be established"}}}},"/agent/requests":{"post":{"summary":"Create a service request","operationId":"createRequest","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRequestBody"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResponse"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/agent/requests/status":{"post":{"summary":"Get request status","operationId":"getRequestStatus","parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusResponse"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}