API v1

REST API for connecting your system to Zuboklik

Complete partner API documentation: authentication, patient and appointment endpoints, webhook registration, and HMAC signature verification.

verifiedAPI key + Bearer
lockHMAC-SHA256 signature
boltJSON over HTTPS
languageVersion v1
curl
# Create a new patient
curl -X POST https://app.zuboklik.cz/api/v1/patients \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "patient": {
      "first_name": "John",
      "last_name": "Doe",
      "phone_number": "+420777111222",
      "pin": "9001011234"
    }
  }'
Step 1

Office authentication

Every office that enables the API has a single master API key. You receive the key directly from the doctor — the doctor generates it in the office settings and passes it into your application.

How to obtain the API key

  1. 1

    The doctor generates the key in office settings

    In Zuboklik, go to Office settings → Connections → API key. The doctor generates or reveals the key and copies it.

  2. 2

    The doctor passes the key to your application

    Paste the key into your system's settings (practice management software or custom app). Zuboklik does not send the key automatically — the doctor passes it manually.

  3. 3

    Store the API key securely

    The key is shared once. Keep it in a secrets manager (Vault, AWS Secrets Manager, Keychain). Never store it in a git repository.

Authorization: ApiKey
# The office API key is sent in a header
# Authorization: ApiKey <office_api_key>
# alternative: X-Api-Key: <office_api_key>

# It is used in only two scenarios:
# 1) registering a new ConnectedSystem
# 2) disconnecting (DELETE) a ConnectedSystem
#
# Everywhere else (read/write patients, appointments,
# webhook management) use your ConnectedSystem's
# Bearer token, see the next section.
warning

Two types of keys, do not confuse them

The office API key (ApiKey) proves ownership of the office. Your ConnectedSystem's Bearer token proves you are already connected. They are different things — in production traffic you will typically use only the Bearer token; the ApiKey is only for bootstrap and disconnect.

Step 2

Connecting your system

After obtaining the office API key, register your system with a single HTTP request. In the response you receive a Bearer token that you will use for all further communication.

POST/api/v1/connected_systems

Register a new system

The name of your system must be unique within the office. It is used for identification in the audit log and for visualisation in the Zuboklik admin.

Request headers

Authorization: ApiKey <office_api_key>
Content-Type: application/json

Request body

{ "name": "nazev-vaseho-systemu" }

Response 201 Created

{
"connected_system": {
"id": 42,
"name": "nazev-vaseho-systemu",
"status": "active",
"office_code": "ABC123",
"connected_at": "2026-07-16T14:00:00.000+02:00"
},
"api_key": "f7c3a1b9e2...",
"message": "Napojený systém byl zaregistrován."
}

save the api_key: The API key in the response is shown only once. Store it in your secrets manager immediately — only its SHA-256 digest is kept in the database. If you lose the key, you must create a new ConnectedSystem.

DELETE/api/v1/connected_systems

Disconnect the system

The system is disconnected with a single request. After disconnection, its Bearer token no longer works and cannot be restored — to reconnect, you must create a new ConnectedSystem.

Request headers

Authorization: Bearer <system_api_key>
X-Api-Key: <office_api_key>

Optionally, you can add a reason as a query parameter: ?reason=migration-to-v2. The reason is stored in the office's audit log.

Security check: DELETE requires both your system's Bearer token and the office API key. This ensures that an attacker with a stolen Bearer token cannot disconnect a different office's system.

Endpoints

Patient management

CRUD endpoints for patients. All operations are automatically scoped to the office that owns the Bearer token. Patients from other offices are not accessible.

GET/api/v1/patients
List patients

Returns active patients of the office (deactivated ones are excluded), sorted by last name and first name. Default pagination is 25 patients per page, maximum 100.

Example request
GET /api/v1/patients?page=1&per_page=50 HTTP/1.1 Host: app.zuboklik.cz Authorization: Bearer <system_api_key>
Example 200 OK response
{ "patients": [ { "id": 4711, "office_code": "ABC123", "zubo_cislo": null, "pin": "9001011234", "first_name": "John", "last_name": "Doe", "degree": null, "phone_number": "+420777111222", "email": "john@example.com", "gender": "male", "number": null, "booking_disabled": false, "deactivated": false, "deactivated_at": null, "created_at": "2026-07-01T10:00:00.000+02:00", "updated_at": "2026-07-15T14:00:00.000+02:00" } ], "meta": { "current_page": 1, "per_page": 50, "total_entries": 1, "total_pages": 1 } }
GET/api/v1/patients/:id
Patient detail

Returns a single active patient by their relative ID in the office.

Example request
GET /api/v1/patients/4711 HTTP/1.1 Host: app.zuboklik.cz Authorization: Bearer <system_api_key>
Example 200 OK response
{ "patient": { "id": 4711, "first_name": "John", "last_name": "Doe", "pin": "9001011234", "phone_number": "+420777111222", "deactivated": false } }
POST/api/v1/patients
Create patient

Creates a new patient. The PIN is used for login in Zuboklik. If a patient with the same PIN already exists in the office, the API returns a 422 error.

Example request
POST /api/v1/patients HTTP/1.1 Host: app.zuboklik.cz Authorization: Bearer <system_api_key> Content-Type: application/json { "patient": { "first_name": "John", "last_name": "Doe", "pin": "9001011234", "phone_number": "+420777111222", "email": "john@example.com", "gender": "male" } }
Example 201 Created response
{ "patient": { "id": 4712, "first_name": "John", "last_name": "Doe", "pin": "9001011234", "phone_number": "+420777111222", "deactivated": false, "created_at": "2026-07-16T14:00:00.000+02:00" } }
PATCH/api/v1/patients/:id
Update patient

Updates selected attributes of a patient. Attributes that are not sent remain unchanged.

Example request
PATCH /api/v1/patients/4711 HTTP/1.1 Host: app.zuboklik.cz Authorization: Bearer <system_api_key> Content-Type: application/json { "patient": { "phone_number": "+420777333444", "email": "john.doe@example.com" } }
Example 200 OK response
{ "patient": { "id": 4711, "first_name": "John", "last_name": "Doe", "phone_number": "+420777333444", "email": "john.doe@example.com" } }
DELETE/api/v1/patients/:id
Deactivate patient

The patient is not physically deleted (deletion would violate statutory retention obligations) — only deactivated_at and deactivate_reason are set. The patient no longer appears in lists and cannot be used for new appointments.

Example request
DELETE /api/v1/patients/4711?reason=duplicate-record HTTP/1.1 Host: app.zuboklik.cz Authorization: Bearer <system_api_key>
Response
HTTP/1.1 204 No Content
Endpoints

Appointment management (Entry)

Appointments in Zuboklik are called Entry. Each entry has a type (EntryType), a state (EntryState) and optionally a link to a patient via patient_symptoms.

GET/api/v1/entries
List entries

Returns the office's entries, newest first. Vacation entries are skipped — those only serve to internally block the calendar.

Example request
GET /api/v1/entries?page=1 HTTP/1.1 Host: app.zuboklik.cz Authorization: Bearer <system_api_key>
Paginated 200 OK response
{ "entries": [ /* … */ ], "meta": { "current_page": 1, "per_page": 25, "total_entries": 1, "total_pages": 1 } }
GET/api/v1/entries/:id
Entry detail

Returns a single entry including patient_symptoms.

Example request
GET /api/v1/entries/8123 HTTP/1.1 Host: app.zuboklik.cz Authorization: Bearer <system_api_key>
200 OK response
{ "entry": { /* see the sample below */ } }
POST/api/v1/entries
Create entry

Creates a new entry for the office. If the entry requires a patient link, the request must include patient_symptoms_attributes with patient_id and symptom_id. The patient must exist in the same office.

Example request
POST /api/v1/entries HTTP/1.1 Host: app.zuboklik.cz Authorization: Bearer <system_api_key> Content-Type: application/json { "entry": { "entry_type_id": 1, "entry_state_id": 1, "booked_for": "2026-07-20", "start_time": "2026-07-20T09:00:00.000+02:00", "end_time": "2026-07-20T09:30:00.000+02:00", "note": "Post-treatment check", "patient_symptoms_attributes": [ { "patient_id": 4711, "symptom_id": 12 } ] } }
201 Created response
{ "entry": { /* … entry.create! */ } }
PATCH/api/v1/entries/:id
Update entry

Updates selected attributes. If patient_symptoms_attributes is in the body, existing links are replaced by the new ones.

Example request
PATCH /api/v1/entries/8123 HTTP/1.1 Host: app.zuboklik.cz Authorization: Bearer <system_api_key> Content-Type: application/json { "entry": { "start_time": "2026-07-20T09:30:00.000+02:00", "end_time": "2026-07-20T10:00:00.000+02:00" } }
200 OK response
{ "entry": { /* updated entry */ } }
DELETE/api/v1/entries/:id
Delete entry

Physically removes the entry from the database. This operation is irreversible — if you only want to cancel the appointment, change entry_state_id to the appropriate state (e.g. canceled).

Example request
DELETE /api/v1/entries/8123 HTTP/1.1 Host: app.zuboklik.cz Authorization: Bearer <system_api_key>
Response
HTTP/1.1 204 No Content

Full Entry object example

This is the full response structure returned by GET /api/v1/entries/:id and POST /api/v1/entries.

{ "entry": { "id": 8123, "office_id": 42, "entry_type_id": 1, "entry_state_id": 1, "booked_for": "2026-07-20", "start_time": "2026-07-20T09:00:00.000+02:00", "end_time": "2026-07-20T09:30:00.000+02:00", "to_be_paid": false, "note": "Post-treatment check", "patient_symptoms": [ { "id": 998, "patient_id": 4711, "symptom_id": 12 } ], "created_at": "2026-07-16T14:00:00.000+02:00", "updated_at": "2026-07-16T14:00:00.000+02:00" } }
Endpoints

Symptom list

Symptoms are a global catalogue. Each symptom has an ID, code and localized name. When creating an entry via the API, use the `symptom_id` from this list inside `patient_symptoms_attributes`.

GET/api/v1/symptoms
List symptoms

Returns all available symptoms ordered by type (acute primary, acute, chronic, hygiene, no complaints) and ID. Names are localized according to the request language.

Example request
GET /api/v1/symptoms HTTP/1.1 Host: app.zuboklik.cz Authorization: Bearer <system_api_key>
Example 200 OK response
{ "symptoms": [ { "id": 1, "code": "big_pain", "name": "Strong pain", "symptom_type_code": "primary" }, { "id": 2, "code": "small_pain", "name": "Mild pain", "symptom_type_code": "secondary" } ] }
Webhooks

Real-time change notifications

Webhooks let you react to changes in Zuboklik in real time — no periodic polling. Every CRUD on an appointment or patient of your office POSTs to your endpoint with an HMAC signature.

add_link

1. Register your URL

POST to /api/v1/webhooks with your endpoint URL. You receive a one-time HMAC secret in the response.

verified_user

2. Verify the signature

Always verify the X-Zuboklik-Signature header against the request body and X-Zuboklik-Timestamp.

sync

3. Respond quickly

Your endpoint must respond 2xx within 10 seconds. If the response is slow, delivery is retried.

POST/api/v1/webhooks

Register a new webhook

The URL must be https and must not point to a private IP address or a cloud metadata service (SSRF protection). Maximum URL length is 2048 characters.

Request
POST /api/v1/webhooks HTTP/1.1 Host: app.zuboklik.cz Authorization: Bearer <system_api_key> Content-Type: application/json { "webhook": { "url": "https://..." } }
201 Created response
{
"webhook": {
"id": 7,
"url": "https://...",
"status": "active",
"failure_count": 0
},
"secret": "a1b2c3d4e5...",
"message": "Webhook byl zaregistrován."
}

save the secret: The HMAC secret is returned only once. Store it in your secrets manager immediately. Zuboklik only keeps its SHA-256 digest. If you lose it, you must create a new webhook (with a new secret).

Payload format

The payload is intentionally minimal: only the ID and type of the event. To get details, call back into the relevant endpoint.

{ "event": "patient_created", "action": "create", "resource_type": "Patient", "resource_id": 4711, "office_code": "ABC123", "occurred_at": "2026-07-16T14:00:00.000+02:00" }

Why is the payload thin? If the webhook URL is compromised, only IDs and types leak. Full data can be retrieved only if the attacker also knows your Bearer token.

Delivery HTTP headers

HeaderExampleMeaning
X-Zuboklik-Signaturea1b2c3d4…(64 hex chars)HMAC-SHA256(payload, secret) computed over the request body and X-Zuboklik-Timestamp.
X-Zuboklik-Timestamp1752670800Unix timestamp in seconds at delivery time. Used to protect against replay attacks.
X-Zuboklik-Deliveryf1e2d3c4-…Unique delivery ID, suitable for deduplication on your side.
X-Zuboklik-Eventpatient_createdEvent type (same value as event in the payload body).
Security

HMAC signature verification

Every webhook is signed with HMAC-SHA256. The signature is computed from the request body and the timestamp. The goal is two-factor verification: (1) you share a secret with us, (2) every message is signed uniquely.

Algorithm

The signature is computed over a string composed of the request body and the timestamp. Both sides must therefore compute from the same inputs — otherwise the signature does not match.

  1. Take the raw request body (not the parsed JSON).
  2. Append X-Zuboklik-Timestamp separated by a dot: {body}.{timestamp}.
  3. Compute HMAC-SHA256 with your webhook secret.
  4. Compare the result (hex) to the X-Zuboklik-Signature header — they must match bit-for-bit.
  5. Optionally check that X-Zuboklik-Timestamp is no older than 5 minutes (replay protection).
Ruby
require 'openssl'

def verify(body, timestamp, signature, secret)
  expected = OpenSSL::HMAC.hexdigest(
    'SHA256',
    secret,
    "#{body}.#{timestamp}"
  )
  ActiveSupport::SecurityUtils.secure_compare(expected, signature)
end

# In your controller (Rails):
# def webhook
#   body = request.raw_post
#   ts   = request.headers['X-Zuboklik-Timestamp']
#   sig  = request.headers['X-Zuboklik-Signature']
#   ok   = verify(body, ts, sig, ENV['ZUBOKLIK_WEBHOOK_SECRET'])
#   head :unauthorized unless ok
#   # ... process payload ...
# end

Replay attack protection

When verifying the signature, always check that X-Zuboklik-Timestamp is no older than 5 minutes (or another reasonable limit for your use case). This prevents an attacker from re-sending a captured message.

Frequently asked questions

Practical answers to the questions that partners typically have when connecting for the first time.

Want to connect your system?

If you are interested in the partner API, get in touch. We will discuss your needs, help you set up a test office, and help with the integration.

Book Appointment