TocDoc

A Ruby gem for interacting with the (unofficial) Doctolib API. A thin, Faraday-based client with configurable defaults, model-driven resource querying, and a clean error hierarchy.

Gem Version CI Coverage Status License: GPL v3 YARD Docs

Heads-up: Doctolib™ does not publish a public API. This gem reverse-engineers the endpoints used by the Doctolib™ website. Behaviour may change at any time without notice. This project is for entertainment purposes only. Doctolib is a trademark of Doctolib. This project is not affiliated with, endorsed by, or sponsored by Doctolib.


Contents

  1. Installation
  2. Quick start
  3. Configuration
  4. Endpoints
  5. Response objects
  6. Pagination
  7. Error handling
  8. Development
  9. Contributing
  10. Code of Conduct
  11. License

Installation

Add the gem to your Gemfile:

gem 'toc_doc'

then run:

bundle install

or install it directly:

gem install toc_doc

Alternative: gem.coop

If you prefer to install from gem.coop, toc_doc is also available there :

source 'https://gem.coop' do
  gem 'toc_doc'
end

Quick start

require 'toc_doc'

collection = TocDoc::Availability.where(
  visit_motive_ids: 7_767_829,
  agenda_ids:       1_101_600,
  practice_ids:     377_272,
  telehealth:       false
)

collection.total      # => 5
collection.next_slot  # => "2026-02-28T10:00:00.000+01:00"

collection.each do |avail|
  puts "#{avail.date}: #{avail.slots.map { |s| s.strftime('%H:%M') }.join(', ')}"
end

Configuration

Module-level configuration

Set options once at startup and every subsequent call will share them:

TocDoc.configure do |config|
  config.api_endpoint = 'https://www.doctolib.de'   # target country
  config.per_page     = 10
end

TocDoc::Availability.where(visit_motive_ids: 123, agenda_ids: 456)

Calling TocDoc.reset! restores all options to their defaults.
Use TocDoc.options to inspect the current configuration hash.

Per-client configuration

Instantiate independent clients with different options and query via TocDoc::Availability.where:

# Germany
TocDoc.configure { |c| c.api_endpoint = 'https://www.doctolib.de'; c.per_page = 3 }
TocDoc::Availability.where(visit_motive_ids: 123, agenda_ids: 456)

# Reset and switch to Italy
TocDoc.reset!
TocDoc.configure { |c| c.api_endpoint = 'https://www.doctolib.it' }
TocDoc::Availability.where(visit_motive_ids: 789, agenda_ids: 101)

Alternatively, use TocDoc::Client directly for lower-level access ().

client = TocDoc::Client.new(api_endpoint: 'https://www.doctolib.de', per_page: 5)
client.get('/availabilities.json', query: { visit_motive_ids: '123', agenda_ids: '456', start_date: Date.today.to_s, limit: 5 })

All configuration options

Option Default Description
api_endpoint https://www.doctolib.fr Base URL. Change to .de / .it for other countries.
user_agent TocDoc Ruby Gem 1.10.0 User-Agent header sent with every request.
default_media_type application/json Accept and Content-Type headers.
per_page 15 Default number of availability dates per request (capped at 15). Emits a warning if the value exceeds the cap.
connect_timeout 5 TCP connect timeout in seconds, passed to Faraday as open_timeout. Override via TOCDOC_CONNECT_TIMEOUT.
read_timeout 10 Response read timeout in seconds, passed to Faraday as timeout. Override via TOCDOC_READ_TIMEOUT.
logger nil Logger-compatible object (e.g. Logger.new($stdout)). When set, TocDoc::Middleware::Logging logs each request URL and response status. nil disables logging.
pagination_depth 1 Number of next_slot hops Availability.where follows automatically. 0 disables automatic pagination. Override via TOCDOC_PAGINATION_DEPTH. Negative values are clamped to 0 with a warning.
rate_limit nil Client-side rate limit. Pass a Numeric for requests-per-second (e.g. 5), a Hash of TokenBucket kwargs (e.g. { rate: 5, interval: 2.0 }), or nil to disable. Values below 1 are clamped.
cache nil Response cache for GET requests. :memory uses a built-in MemoryStore (TTL 300s); a Hash with :store and :ttl keys accepts any AS-compatible store; nil disables caching.
middleware Retry + RaiseError + JSON + adapter Full Faraday middleware stack. Override to customise completely.
connection_options {} Options passed directly to Faraday.new.

Environment variable overrides

All primary options can be set via environment variables before the gem is loaded:

Variable Option
TOCDOC_API_ENDPOINT api_endpoint
TOCDOC_USER_AGENT user_agent
TOCDOC_MEDIA_TYPE default_media_type
TOCDOC_PER_PAGE per_page
TOCDOC_CONNECT_TIMEOUT connect_timeout (default 5)
TOCDOC_READ_TIMEOUT read_timeout (default 10)
TOCDOC_RETRY_MAX Maximum Faraday retry attempts (default 3)
TOCDOC_PAGINATION_DEPTH pagination_depth (default 1)

Endpoints

Availabilities

Retrieve open appointment slots for a given visit motive and agenda.

TocDoc::Availability.where(
  visit_motive_ids: visit_motive_id,   # Integer, String, or Array
  agenda_ids:       agenda_id,         # Integer, String, or Array
  start_date:       Date.today,        # Date or String (default: today)
  limit:            5,                 # override per_page for this call
  # any extra keyword args are forwarded verbatim as query params:
  practice_ids:     377_272,
  telehealth:       false
)

TocDoc.availabilities(...) is a module-level shortcut with the same signature.

Multiple IDs are accepted as arrays; the gem serialises them with the dash-separated format Doctolib expects:

TocDoc::Availability.where(
  visit_motive_ids: [7_767_829, 7_767_830],
  agenda_ids:       [1_101_600, 1_101_601]
)
# → GET /availabilities.json?visit_motive_ids=7767829-7767830&agenda_ids=1101600-1101601&…

Return value: a TocDoc::Availability::Collection (see Response objects).

Query the Doctolib autocomplete endpoint to look up practitioners, organizations, and specialities.

result = TocDoc::Search.where(query: 'dentiste')
result.profiles      # => [#<TocDoc::Profile::Practitioner ...>, ...]
result.specialities  # => [#<TocDoc::Speciality ...>, ...]

Pass type: to receive a filtered array directly:

# Only specialities
TocDoc::Search.where(query: 'cardio', type: 'speciality')
# => [#<TocDoc::Speciality name="Cardiologue">, ...]

# Only practitioners
TocDoc::Search.where(query: 'dupont', type: 'practitioner')
# => [#<TocDoc::Profile::Practitioner ...>, ...]

Valid type: values: 'profile' (all profiles), 'practitioner', 'organization', 'speciality'.

TocDoc.search(...) is a module-level shortcut with the same signature.

Return value: a TocDoc::Search::Result when type: is omitted, or a filtered Array otherwise (see Response objects).

Profile

Fetch a full practitioner or organization profile page by slug or numeric ID.

# by slug
profile = TocDoc::Profile.find('jane-doe-bordeaux')

# by numeric ID
profile = TocDoc::Profile.find(1_542_899)

# module-level shortcut
profile = TocDoc.profile('jane-doe-bordeaux')

Profile.find returns a typed TocDoc::Profile::Practitioner or TocDoc::Profile::Organization instance with partial: false (i.e. full profile data).

profile.name            # => "Dr. Jane Doe"
profile.partial         # => false
profile.practitioner?   # => true

profile.skills          # => [#<TocDoc::Resource ...>, ...]
profile.skills_for(377_272)  # => skills for a specific practice

profile.places.first.city              # => "Bordeaux"
profile.places.first.coordinates       # => [44.8386722, -0.5780466]

Return value: a TocDoc::Profile::Practitioner or TocDoc::Profile::Organization (see Response objects).

BookingInfo

Fetch the slot-selection funnel context for a practitioner or organization by slug or numeric ID.

# by slug
info = TocDoc::BookingInfo.find('jane-doe-bordeaux')

# by numeric ID
info = TocDoc::BookingInfo.find(1_542_899)

# module-level shortcut
info = TocDoc.booking_info('jane-doe-bordeaux')

BookingInfo.find hits /online_booking/api/slot_selection_funnel/v1/info.json and returns a TocDoc::BookingInfo instance containing the full booking context needed to drive the appointment-booking funnel.

info.profile        # => #<TocDoc::Profile::Practitioner ...>
info.specialities   # => [#<TocDoc::Speciality ...>, ...]
info.visit_motives  # => [#<TocDoc::VisitMotive id=..., name="...">, ...]
info.agendas        # => [#<TocDoc::Agenda id=..., practice_id=...>, ...]
info.places         # => [#<TocDoc::Place ...>, ...]
info.practitioners  # => [#<TocDoc::Profile::Practitioner partial=true>, ...]
info.organization?  # => false

Return value: a TocDoc::BookingInfo (see Response objects).


Response objects

All API responses are wrapped in lightweight Ruby objects that provide dot-notation access and a #to_h round-trip helper.

TocDoc::Availability::Collection

Returned by TocDoc::Availability.where; also accessible via the TocDoc.availabilities module-level shortcut. Implements Enumerable, yielding TocDoc::Availability instances that have at least one slot.

Method Type Description
#total Integer Total number of available slots across all dates.
#next_slot `String \ nil`
#availabilities Array<TocDoc::Availability> All entries that have at least one slot. Aliased as #all. Memoized; invalidated by #merge_page!.
#slots Array<DateTime> All individual slots across every availability in the collection.
#each Yields each TocDoc::Availability that has at least one slot (excludes empty-slot dates).
#raw_availabilities Array<TocDoc::Availability> All date entries, including those with no slots.
#more? Boolean true when the API has indicated further pages exist (next_slot is present).
#load_next! self Fetches the next window and merges it into this collection. Raises StopIteration when #more? is false. Requires a client (i.e. built via Availability.where).
#booking_url(availability) String Returns the Doctolib booking URL for a given TocDoc::Availability instance. Delegates URL construction to TocDoc::UriUtils.
#to_h Hash Plain-hash representation (only dates with slots in the availabilities key).

TocDoc::Availability

Represents a single availability date entry. Each element yielded by the collection.

Method Type Description
#date Date Parsed date object.
#slots Array<DateTime> Parsed datetime objects for each bookable slot on that date.
#to_h Hash Plain-hash representation.

Example:

collection = TocDoc::Availability.where(visit_motive_ids: 123, agenda_ids: 456)

collection.total      # => 5
collection.next_slot  # => "2026-02-28T10:00:00.000+01:00"

collection.first.date   # => #<Date: 2026-02-28>
collection.first.slots  # => [#<DateTime: 2026-02-28T10:00:00+01:00>, ...]

collection.to_h
# => {
#      "total"          => 5,
#      "next_slot"      => "2026-02-28T10:00:00.000+01:00",
#      "availabilities" => [{ "date" => "2026-02-28", "slots" => [...] }, ...]
#    }

TocDoc::Search::Result

Returned by TocDoc::Search.where when type: is omitted.

Method Type Description
#profiles Array<TocDoc::Profile::Practitioner, TocDoc::Profile::Organization> All profile results, typed via Profile.build.
#specialities Array<TocDoc::Speciality> All speciality results.
#filter_by_type(type) Array Narrows results to 'profile', 'practitioner', 'organization', or 'speciality'.

TocDoc::Profile

Represents a practitioner or organization profile. Can be a lightweight search result (partial: true) or a full profile page (partial: false).

Method Type Description
Profile.find(identifier) `Profile::Practitioner \ Profile::Organization`
Profile.build(attrs) `Profile::Practitioner \ Profile::Organization`
#id `String \ Integer`
#partial Boolean true when built from a search result, false when fetched via Profile.find.
#practitioner? Boolean true when this is a Profile::Practitioner.
#organization? Boolean true when this is a Profile::Organization.
#places Array<TocDoc::Place> Practice locations (available on full profiles).
#skills Array<TocDoc::Resource> All skills across every practice (available on full profiles).
#skills_for(practice_id) Array<TocDoc::Resource> Skills for a single practice by its ID.

TocDoc::Profile::Practitioner and TocDoc::Profile::Organization are typed subclasses that inherit dot-notation attribute access from TocDoc::Resource.

TocDoc::Place

Represents a practice location returned inside a full profile response. Inherits dot-notation attribute access from TocDoc::Resource.

Method Type Description
#id String Practice identifier (e.g. "practice-125055").
#address String Street address.
#zipcode String Postal code.
#city String City name.
#full_address String Combined address string.
#landline_number `String \ nil`
#latitude Float Latitude.
#longitude Float Longitude.
#elevator Boolean Whether the practice has elevator access.
#handicap Boolean Whether the practice is handicap-accessible.
#formal_name `String \ nil`
#coordinates Array<Float> Convenience method returning [latitude, longitude].

TocDoc::BookingInfo

Returned by TocDoc::BookingInfo.find; also accessible via the TocDoc.booking_info module-level shortcut.

Method Type Description
#profile `Profile::Practitioner \ Profile::Organization`
#specialities Array<TocDoc::Speciality> Specialities associated with the booking context.
#visit_motives Array<TocDoc::VisitMotive> Available visit motives (reasons for consultation).
#agendas Array<TocDoc::Agenda> Agendas, each pre-resolved with their matching VisitMotive objects.
#places Array<TocDoc::Place> Practice locations.
#practitioners Array<TocDoc::Profile::Practitioner> Practitioners associated with this booking context (partial: true).
#organization? Boolean Delegates to the inner profile.

TocDoc::VisitMotive

Represents a visit motive (reason for consultation) returned inside a BookingInfo response. Inherits dot-notation attribute access from TocDoc::Resource.

Method Type Description
#id Integer Visit motive identifier.
#name String Human-readable name of the visit motive.

TocDoc::Agenda

Represents an agenda (calendar) returned inside a BookingInfo response. Inherits dot-notation attribute access from TocDoc::Resource.

Method Type Description
#id Integer Agenda identifier.
#practice_id Integer ID of the associated practice.
#visit_motives Array<TocDoc::VisitMotive> Visit motives pre-resolved via visit_motive_ids when built through BookingInfo.

TocDoc::Speciality

Represents a speciality returned by the autocomplete endpoint. Inherits dot-notation attribute access from TocDoc::Resource.

Method Type Description
#value Integer Numeric speciality identifier.
#slug String URL-friendly identifier.
#name String Human-readable speciality name.

Example:

result = TocDoc::Search.where(query: 'dermato')

result.profiles.first.class          # => TocDoc::Profile::Practitioner
result.profiles.first.practitioner?  # => true
result.profiles.first.name           # => "Dr. Jane Smith"

result.specialities.first.slug   # => "dermatologue"
result.specialities.first.name   # => "Dermatologue"

Pagination

The Doctolib availability endpoint is window-based: each request returns up to limit dates starting from start_date.

Automatic next-slot resolution

TocDoc::Availability.where automatically follows next_slot up to pagination_depth times (default: 1). Each hop issues a follow-up request from the next_slot date returned by the previous response, transparently merging the pages before returning the collection. Set pagination_depth: 0 to disable automatic pagination entirely.

TocDoc.configure { |c| c.pagination_depth = 3 }
# → up to 3 next_slot hops per Availability.where call

On-demand pagination with #load_next!

After the initial call, check #more? and call #load_next! to load additional windows one at a time:

collection = TocDoc::Availability.where(
  visit_motive_ids: 7_767_829,
  agenda_ids:       1_101_600,
  start_date:       Date.today
)

while collection.more?
  collection.load_next!
end

collection.total  # slots across all fetched pages

Manual window advancement

To fetch a completely independent next window, call TocDoc::Availability.where again with a later start_date:

first_page = TocDoc::Availability.where(
  visit_motive_ids: 7_767_829,
  agenda_ids:       1_101_600,
  start_date:       Date.today
)

if first_page.any?
  next_start = first_page.raw_availabilities.last.date + 1
  next_page  = TocDoc::Availability.where(
    visit_motive_ids: 7_767_829,
    agenda_ids:       1_101_600,
    start_date:       next_start
  )
end

Error handling

All errors raised by TocDoc inherit from TocDoc::Error < StandardError, so you can rescue the whole hierarchy with a single clause:

begin
  TocDoc::Availability.where(visit_motive_ids: 0, agenda_ids: 0)
rescue TocDoc::Error => e
  puts "Doctolib error: #{e.message}"
end

Error classes

Class Raised when
TocDoc::Error Base class for all TocDoc errors.
TocDoc::ConnectionError Network/transport failure before a response is received (DNS, timeout, SSL). The original cause is available via #cause.
TocDoc::ResponseError HTTP response received but indicates an error. Carries #status, #body, and #headers.
TocDoc::ClientError 4xx response not matched by a more specific class.
TocDoc::BadRequest HTTP 400.
TocDoc::NotFound HTTP 404.
TocDoc::UnprocessableEntity HTTP 422.
TocDoc::TooManyRequests HTTP 429.
TocDoc::ServerError 5xx response.
begin
  TocDoc::Profile.find('unknown-slug')
rescue TocDoc::NotFound => e
  puts e.status    # => 404
  puts e.body      # => '{"error":"not found"}'
rescue TocDoc::ConnectionError => e
  puts e.cause     # original Faraday exception
rescue TocDoc::ServerError => e
  puts "Server error: #{e.status}"
end

The default middleware stack includes a Faraday::Retry::Middleware that automatically retries (up to 3 times, with exponential back-off) on:

  • 429 Too Many Requests
  • 500 Internal Server Error
  • 502 Bad Gateway
  • 503 Service Unavailable
  • 504 Gateway Timeout
  • network timeouts

Development

Clone the repository and install dependencies:

git clone https://github.com/01max/toc_doc.git
cd toc_doc
bin/setup

Run the test suite:

bundle exec rake spec
# or
bundle exec rspec

Run the linter:

bundle exec rubocop

Open an interactive console with the gem loaded:

bin/console

Install the gem locally:

bundle exec rake install

Adding new endpoints

  1. Create lib/toc_doc/models/<resource>.rb with a model class inheriting from TocDoc::Resource. Add a class-level .where (or equivalent) query method that calls TocDoc.client.get / .post to issue requests.
  2. If the endpoint is paginated, create lib/toc_doc/models/<resource>/collection.rb with an Enumerable collection class (see TocDoc::Availability::Collection for the pattern).
  3. Require the new files from lib/toc_doc/models.rb.
  4. Add specs under spec/toc_doc/models/.

Generating documentation

The codebase uses YARD for API documentation. All public methods are annotated with @param, @return, and @example tags.

Generate the HTML docs:

bundle exec yard doc

The output is written to doc/. To browse it locally:

bundle exec yard server
# → http://localhost:8808

To check documentation coverage without generating files:

bundle exec yard stats

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/01max/toc_doc. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.


Code of Conduct

Everyone interacting in the TocDoc project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.


License

The gem is available as open source under the terms of the GNU General Public v3 License.