Skip to content

Core concepts

Audio and billing are not exposed; see Overview. Capacity is published as a resource.

An object has the same fields in a REST response and in an event’s data.object.

  • Talki is the source of truth for Talki appointments. API writes follow the same rules as bookings made by phone and can be refused with the same conflicts.
  • External calendars contribute busy time. Google and Outlook events can narrow the availability Talki reports. They constrain it and receive mirrors of it; they do not replace Talki’s appointment record.
  • Availability is advisory. A window is true when computed, and another caller can still take it. Only a successful POST /v1/appointments holds the time.

Talki IDs are opaque and case-sensitive. Do not parse them, infer a type from their shape, or generate one. Store the ID next to your own record, so webhook redelivery and feed recovery update rather than duplicate.

Contacts are matched to calls by normalised phone number. external_id and system on contact upsert are your cross-references; they do not replace Talki’s contact identity.

An account works from one or more locations, exactly one of them the main location. Calls, refused calls, unresolved requests, appointments, services, resources, documents and caller rules carry the location_id they belong to, and closures carry it when they apply at one location only. Lists of these records take ?location_id= to return one location’s records, and the account, weekly hours and forwarding check are read and written one location at a time. When multi_sede on the account is false, there is only the main location. See Work with locations.

Public instants are RFC 3339 UTC and end in Z. Appointment writes must send Z or an explicit offset; an offsetless value is rejected. GET /v1/account returns the account’s IANA timezone for local dates and UI.

created_at and updated_at are display and reporting values, not sync watermarks: a change can become visible after a later timestamp has already been read. Resume from a feed cursor.

GET /v1/calls has two modes:

  • Without sync, a newest-first list ordered by started_at, for a screen a person reads. next_cursor is null on the last page.
  • With sync=true, a commit-ordered feed for backfill and recovery. next_cursor is your position and comes back on every non-empty page, including the last.

The list filters are refused alongside sync=true: created_after, created_before, phone, contact_id, q, outcome, and a status other than all. location_id is allowed with it. A cursor belongs to the mode that issued it.

Appointments, contacts, services, resources, and unresolved requests

Section titled “Appointments, contacts, services, resources, and unresolved requests”

These list endpoints are change feeds, apart from a contact search. Start without cursor, upsert each object by Talki ID, store every non-null next_cursor, and drive the loop from has_more.

Keep the filters constant for the length of a walk, and do not filter out cancelled appointments, inactive services or closed unresolved requests: those states are precisely the changes a downstream copy needs. A walk filtered by location_id is complete for that location, and its cursor is refused under any other location_id, or without one.

A feed carries current state, not every intermediate edit, and deletions leave no tombstone in it. A deleted contact sends contact.deleted, including one deleted after twelve months without a call. A deleted service sends service.deleted, and a deleted resource resource.deleted, each with the object as it was. A call deleted with DELETE /v1/calls/{id} sends call.deleted, which lists the unresolved requests deleted with it. Calls and unresolved requests deleted after twelve months send no event. Walk each feed in full periodically and reconcile the IDs that no longer come back; that also catches a deletion whose event you missed.

A change to a resource’s unit classes or service links moves the resource itself in the feed.

Cursors are opaque and versioned, and belong to the endpoint and filters that issued them. Do not decode one, edit one, or pass it to another endpoint or filter. Each feed’s next_cursor names that feed, and the other feeds refuse it with invalid_cursor. So are refused a cursor of the other kind (a list cursor on a feed, or the reverse) and a change-feed cursor from another location_id filter. A list cursor passed to another list is not always refused, and then resumes from a meaningless position.

A feed cursor issued up to 2026-11-04 may name no feed. It keeps working until 2027-02-01, and any feed page you read replaces it with one that does. After that date it gets invalid_cursor: drop it and walk the feed again from no cursor.

GET /v1/contacts with phone or tag_id is a search rather than the feed, and finds only the contacts the feed publishes:

  • phone takes E.164, or a national number read as Italian. Anything else is refused with 400 invalid_parameter.
  • tag_id takes a Tag.id. An unknown or malformed one finds nothing.

With both, a contact must match both. Results are newest contact first, and next_cursor is null on the last page, so a search gives you no position to store. A feed cursor is refused by a search, and a search cursor by the feed, with invalid_cursor. Sync from the unfiltered feed.

GET /v1/documents is a newest-first list, not a change feed: its cursor pages through the list as it stands. Keep a copy current with the document.* events, and walk the whole list to rebuild it or to find deletions you missed; see Manage the knowledge base.

A resource is a finite thing the business books against: tables, rooms, chairs, seats. A shared resource is one fungible pool of capacity units. A units resource is a set of discrete sized units described by unit_classes, and there capacity is the size of the largest single unit rather than a total — read max_party_size for the largest booking it can hold.

A resource belongs to one location, and only services of that location draw on it. A timed service draws on its location’s default resource (is_default), on a resource of its own, or on resources the business built. A resource of its own carries the service’s owner_service_id: it is the real per-service cap, and it is deleted with the service.

Writing is much narrower than reading: name, and capacity on a shared resource, plus creating a shared resource with POST /v1/resources, and deleting a resource with DELETE /v1/resources/{id}. Unit classes and service links are read-only, and a capacity below what the calendar has already committed is refused with 409 capacity_below_committed. A delete is refused for the default resource (409 resource_is_default) and for the only resource of a service that still needs one (409 resource_required_by_service).

When a caller asks something the assistant cannot answer, it records the question, says a person will get back to them, and the business works through the list. The object carries the question, a number to call back, and a status that closes: map it to a task.

A request moves in the feed again when its status or its answer changes. Sync without the status filter, and use that filter only for a point-in-time view of the open list.

Once a request is resolved, the general answer to its question can be taught to the assistant, which then gives it to every caller who asks; see Teach the assistant an answer.

The published question is the assistant’s own one-line record of what was asked. The conversation itself is the call’s transcript, under transcripts:read.

The contact feed publishes owner-curated, imported and API-written names only. An AI-inferred candidate is a pending contact until a person approves it. Names written through the API are trusted and are not overwritten by later transcript extraction.

Every appointment create, reschedule, cancellation and restore, every service, resource, closure, tag, caller-rule or document create, and teaching the assistant (POST /v1/unresolved_requests/{id}/teach) require an Idempotency-Key. Generate one per logical write and reuse it only to retry the same method, path, query and body: a write can commit and still lose its response, and the replay is what tells those two cases apart.

Every other write accepts the header without requiring it. Once the first request has a final answer, a repeated key with the same request replays the original status and body and adds Idempotency-Replayed: true. An answer a retry could change releases the key instead; see Retry rules. Reusing a key for a different request returns 409 idempotency_key_reuse. Keys are at most 255 characters and are kept for 24 hours; after that, replay protection is no longer guaranteed.

The major generation is /v1 and it never changes. The contract you are pinned to is a dated version; the only one is 2026-10-01. See the changelog.

Every credential carries its own pin, and that is what you are served when you send nothing: an API key pins on the key, an OAuth app pins on the app — never per installation, so one integration speaks one version for all of its customers — and a webhook endpoint pins on the endpoint.

Send Talki-Version: 2026-10-01 to override the pin for a single request. That is how you rehearse an upgrade: one request at a time, against the sandbox, before moving the pin for everybody. A response echoes the version it was served at in the same header, so you can confirm it rather than infer it from the payload. It is absent when the request was refused before a version was chosen: 401, the 402/403 account refusals, unsupported_version itself, ip_rate_limited, and every /oauth/* response.

A version the API does not serve is refused with 400 unsupported_version, never downgraded; the message lists the accepted versions.

Fields and event types may be added without a path change, so ignore what you do not recognise. Removals, renames, type changes and meaning changes require a dated contract release and a migration notice.