Changelog
History of changes to the Capturia API v1, dated and categorized.
- Added
v1The API reference and the OpenAPI spec are available in English
The English reference pages (
/en/developers/api/v1/reference) describe every endpoint, parameter, schema and response in English, including in the docs search. The English spec is published next to the French one:/openapi.en.yaml,/openapi.en.jsonand the Postman collection/postman-collection.en.json. The contract does not change: same paths, same fields, same values; only the descriptive text is translated. The French Postman collection (/postman-collection.json) is now served onapi.capturia.io, like the spec. - Changed
v1AI assistant connector: get_todays_bookings says who booked and who cancelled each booking (breaking change)
Breaking change to the MCP connector.
get_todays_bookingsacceptsrange(todayby default,upcomingfor the next ones,recentfor the most recent ones, cancellations included) andcontact_id(one contact's bookings,recentby default). Each booking carriesorigin(who booked it),cancel_originandcancelled_at(who cancelled it, and when),meeting_type, and underuntrusted_user_contentthe booking type, the host and the names of whoever booked or cancelled it. Thesourcefield is no longer returned: it labelled "automation" a booking the agent took in the conversation, andoriginreplaces it.dateisnulloutsiderange: today. The tool now follows the plan like the Bookings screen: an account whose plan does not include the Bookings and calendars module gets a refusal that points to “My Plan”, and an unreadable plan is to be retried. The result is still capped at 20 bookings, withtotal_countandhas_more.search_contacts,get_pipeline_summaryandget_todays_bookingsalso publish their output schema (outputSchema). - Changed
v1Pipeline stages: each stage names its pipeline, hidden stages are marked, archived contacts no longer count
GET /v1/pipeline/stagesnow returns the stages of every pipeline in the account with their pipeline:pipeline_idandpipeline(id,name,is_default). An account with two pipelines can have two stages with the same name; the pipeline tells them apart. Stages are sorted by pipeline, in the dashboard's order, then bystage_order. A stage hidden on the dashboard pipeline is still listed, withis_hidden: true: its contacts exist and count.contact_countno longer counts archived or merged contacts, like the Pipeline screen. A missing stage flag (is_default,is_won,is_lost) now readsfalseinstead ofnull. The MCP connector (get_pipeline_summary) returns the same fields. - Changed
v1Webhooks: an event's author says whether it came from an AI assistant or the API, and no longer invents a member
The events'
data.actorfield gainsvia:ai_assistantwhen the action comes from an AI assistant connected to the connector (Claude, ChatGPT),api_keywhen it comes from the public API,nullotherwise. When no member acted (system, automation, integration),typeandidare nownull; they previously announced a team member without an identifier. A write made with an API key is named after its key inname, a value written in French (for exampleClé API « Zapier », orAssistant IA — clé API « Zapier »when the key goes through the connector), neverSystème. - Changed
v1POST /v1/contacts: email or phone, never a second record, no implied consent
A contact is now created with an email or a phone number: either one is enough. The email is stored in lowercase, and the person is searched across every identity of the account (primary and secondary emails and phone numbers, case-insensitive). A person who already exists returns
409 duplicate_contactwith the existing record indataand the matching fields indetails.conflicting_fields, whether the match comes from the email or the phone; an email and a phone held by two different records return409 duplicate_resource. The new contact has the sourceapi, and no implied consent (text or email) is recorded for it anymore: an integration names no one who met this contact. For a lead who just filled in your form, with their consent, usePOST /v1/leads/capture. - Changed
v1DELETE /v1/contacts/{id} requires the contacts:delete scope
Deleting a contact now requires the
contacts:deletescope, separate fromcontacts:write, as in the dashboard where deleting is a permission of its own. Only someone allowed to delete prospects in Capturia can grant this scope to a key; it is only included in the Full integration preset. A key with onlycontacts:writegets403 insufficient_scopeon this route. - Changed
v1/v1/bookings now serves your account's calendar (breaking change)
Breaking change. The
/v1/bookingsroutes now serve your account's calendar: the bookings your team holds on your calendars (booking page, dashboard, AI agents, automations). They used to read and write the meetings of your delivery projects with Capturia.GET /v1/bookingslists the calendar's bookings (archived ones excluded). Theproject_idfilter is gone;event_type_idandcontact_idjoinfrom,toandstatus(confirmed,cancelled,completed,no_show). An unreadable filter is refused with400 validation_error. Each booking carriesevent_type_id,contact_id,host_idandmeeting_link;project_id,assigned_to_id,video_providerandzoom_join_urlare no longer returned.POST /v1/bookingsbooks through the same service as your booking page, under the calendar's rules (hosts, hours, booking window, the host's Google calendar, required questions, one upcoming booking per contact per calendar). The body requiresevent_type_id(one of your account's calendars) andstart_datetime, pluscontact_idorguest_name+guest_email. Optional:guest_phone,host_id,timezone,notes,form_responses(one entry per question id, shaped{ value }; a form refusal lists the calendar's questions indetails.questions).project_idno longer exists, andend_datetimeis refused: the calendar sets the duration. The requested time is booked or refused with its reason indetails.reason, never replaced:400 validation_error(slot_in_past,outside_window,off_cadence,contact_or_guest_required,contact_without_email),404 resource_not_found(details.field:event_type_id,contact_id,host_id),409 duplicate_resource(already_booked),422 business_rule_violation(calendar_without_hosts,host_not_on_calendar,host_unavailable,slot_unavailable,contact_archived,lead_not_qualified),503 service_unavailable(retry afterRetry-After).DELETE /v1/bookings/{id}cancels the way the dashboard does: the event is removed from the host's calendar and the host is notified. An already cancelled booking now answers422 business_rule_violation(already_cancelled) instead of409 already_cancelled; a completed or no-show booking,422(booking_not_cancellable). An unknown booking answers404 resource_not_found.The AI assistant connector follows the same change:
get_todays_bookingsreads the account's calendar, andbook_meetingbooks in it (start_datetime, thencontact_idorguest_name+guest_email, optionalevent_type_idandform_responses;project_idandend_datetimeare no longer accepted). The connector never cancels a booking. - Added
v1GET /v1/usage: new status_unknown counter
Every bucket and the totals of
GET /v1/usagenow carrystatus_unknown: the number of requests whose outcome was not recorded. That is a response still in flight when the bucket was aggregated (the current hourly bucket is recomputed every few minutes, and the request then gets its real outcome back), or an outcome whose recording failed. These requests are neither successes nor errors: leave them out of any error-rate calculation. Thestatus_2xx,status_4xx,status_5xx,rate_limited_countandstatus_unknownfamilies are disjoint: a request is never counted in two of them. - Changed
v1GET /v1/usage: status_2xx now only counts confirmed successes
Until now, a request whose outcome was never recorded could be counted as a success. It is now counted in
status_unknown. The affected historical buckets are corrected once: theirstatus_2xxdrops by that amount, andstatus_unknownpicks up the difference. Refusals by the per-IP limit of/v1/eventsand/v1/leads/capturealso move fromstatus_4xxtorate_limited_count, like refusals by the key's hourly quota: a refusal by a rate limiter is always inrate_limited_countand does not consume the quota. A business refusal returned as 429 (trigger called too often, test-send quota, webhook subscription cap) stays astatus_4xxerror and consumes the quota. Finally, the reference wrongly said thatrate_limited_countwas a subset ofstatus_4xx: the two families have always been distinct, and this documentation fix changes no calculation. - Changed
v1GET /v1/conversations/{id}/messages: content no longer carries internal agent control markers
AI agents pace their replies with internal markers ([PAUSE:3s] to send in several bubbles, [INPUT:email] to request a field from the visitor). They were never part of the contract, yet they surfaced in the content field. They are now stripped: a reply the agent sent as several bubbles reaches you as paragraphs separated by a blank line. This applies to messages already stored, so the value of content changes once for existing agent messages. Worth knowing: a message that carried ONLY control markers becomes an empty string. The row stays in the page, with its id, role and created_at, so that total_count, has_more and the pagination cursor stay accurate. If your integration checks for a message with if (!content), switch it to checking that the object is present.
- Added
v1Dedicated api.capturia.io subdomain + legacy host sunset 2027-05-03
The public API is now served at https://api.capturia.io/v1/* (enterprise convention aligned with Stripe/Twilio/GitHub). OpenAPI bundle auto-discoverable at api.capturia.io/openapi.{yaml,json}. The legacy host app.capturia.io/api/v1/* remains active until 2027-05-03; every legacy response carries IETF Deprecation, Sunset and Link successor-version headers. At sunset, the legacy host switches to HTTP 308 Permanent Redirect to the new host. No 410 Gone — your integrations will keep working even if you forget to migrate (with one round-trip of overhead).
- Added
v1OpenAPI 3.1 public spec + Postman + TS types
The API now exposes an OpenAPI 3.1 spec at
/openapi.yaml(235KB) and/openapi.json(289KB). Postman collection (/postman-collection.json) downloadable, importable in Insomnia 2023+ via the OpenAPI spec directly. TypeScript types generated at build (lib/api/v1/openapi-types.ts). Request validation against spec in shadow mode by default. - Added
v1GET /v1/usage endpoint + observability headers
Programmatic
/v1/usageendpoint returns consumption aggregations (filterable bybucket_typeand range). Every response now emitsX-Request-IDULID,Capturia-Request-ID(alias),Capturia-Version: v1, andVary: Accept-Language, Accept-Encoding. - Added
v1Opaque cursor pagination + Deprecation/Sunset legacy headers
All
GETcollection endpoints now accept?cursor=opaque base64url withidtiebreaker for stable sort. Legacytotal_count+cursorISO format accepted alongside for 12 months (sunset 2027-05-03).Deprecation(RFC 9745) +Sunset(RFC 8594) +Link rel=deprecationheaders announce the removal. - Added
v1Idempotency-Key header on POST/PUT/DELETE
All mutating routes accept an optional
Idempotency-Keyheader (regex[a-zA-Z0-9_-]{1,64}). TTL 24h via Vercel KV. Verbatim replay returnsIdempotent-Replayed: true. Conflict (different payload or concurrent request) →409 idempotency_conflict. - Added
v1RFC 9331 headers + enriched 429 body
Parallel emission of
RateLimit-Limit/Remaining/Reset/Policyheaders (RFC 9331) andX-RateLimit-*(legacy, sunset 2027-05-03). 429 body now includes actionabledetails.limit_type(per_keyorbrute_force) anddetails.retry_after_seconds.