Documentation / Reference / Calendar API v1 — Endpoint reference

Calendar API v1 — Endpoint reference

ES EN

Manage calendars, availability and appointments from external systems.

Base URL: https://app.dhalinta.com/api/calendar/v1

Authentication: Authorization: Bearer mrcal_...

See the API introduction for authentication, rate limits and conventions.


Required scopes

ScopeEndpoints that require it
calendar:readGET /me, GET /calendars, GET /calendars/{calendar}/services, GET /calendars/{calendar}/resources
calendar:availabilityGET /calendars/{calendar}/availability
calendar:appointments:readGET /calendars/{calendar}/appointments, GET /appointments/{appointment}
calendar:appointments:createPOST /calendars/{calendar}/appointments
calendar:appointments:cancelPOST /appointments/{appointment}/cancel
calendar:appointments:updatePATCH /appointments/{appointment}
calendar:adminSpecial scope: satisfies any required scope

If the token doesn't have the scope:

{ "error": "Insufficient scope" }

Me

GET /me

Returns basic information about the current token.

GET /api/calendar/v1/me
Authorization: Bearer mrcal_...
{
  "token": {
    "id": 10,
    "name": "Retell AI clinic",
    "scopes": ["calendar:read", "calendar:availability", "calendar:appointments:create"],
    "calendar_ids": [12],
    "expires_at": null
  },
  "workspace_id": 3,
  "organization_id": 2
}

Calendars

GET /calendars

Lists the active calendars the token can access.

GET /api/calendar/v1/calendars
Authorization: Bearer mrcal_...
{
  "data": [
    {
      "id": 12,
      "name": "Main calendar",
      "slug": "main-calendar",
      "timezone": "Europe/Madrid",
      "time_format": "24h",
      "is_default": true,
      "brand_color": "#2563eb",
      "public_url": "https://app.dhalinta.com/c/main-calendar"
    }
  ]
}

GET /calendars/{calendar}/services

Lists the calendar's active appointment types.

GET /api/calendar/v1/calendars/12/services
Authorization: Bearer mrcal_...
{
  "data": [
    {
      "id": 5,
      "name": "Haircut",
      "description": "Sample service",
      "duration_minutes": 30,
      "buffer_before_minutes": 0,
      "buffer_after_minutes": 0,
      "requires_approval": false,
      "allows_group_booking": false,
      "max_party_size": 1,
      "price": "25.00",
      "color": "#2563eb"
    }
  ]
}

GET /calendars/{calendar}/resources

Lists the calendar's active resources (people, rooms, equipment or any bookable resource).

GET /api/calendar/v1/calendars/12/resources
Authorization: Bearer mrcal_...
{
  "data": [
    {
      "id": 3,
      "name": "Maria",
      "type": "person",
      "capacity": 1,
      "color": "#2563eb"
    }
  ]
}

Availability

GET /calendars/{calendar}/availability

Returns free slots for a service within a date range.

Parameters:

ParameterTypeRequiredRules
service_idintegerYesmin:1
fromdateYesStart date/time
todateYesMust be after from
resource_idintegerNomin:1
limitintegerNomin:1, max:500, default 120
GET /api/calendar/v1/calendars/12/availability?service_id=5&from=2026-05-12&to=2026-05-16&limit=20
Authorization: Bearer mrcal_...
{
  "data": [
    {
      "start_at": "2026-05-12T08:00:00+00:00",
      "end_at": "2026-05-12T08:30:00+00:00",
      "day": "2026-05-12",
      "start_local": "10:00",
      "end_local": "10:30",
      "timezone": "Europe/Madrid",
      "resource_id": 3,
      "resource_name": "Maria",
      "capacity_total": 1,
      "capacity_available": 1
    }
  ]
}
Note: in availability, start_local and end_local are HH:mm strings in the calendar's time zone (not ISO 8601). In appointment responses they are full ISO 8601 with offset.

If to is not after from:

{ "message": "to_must_be_after_from" }

Appointments

The appointment response uses the same structure in every endpoint:

{
  "id": 4821,
  "calendar_id": 12,
  "service_id": 5,
  "service_name": "Haircut",
  "resource_id": 3,
  "resource_name": "Maria",
  "contact_id": 88,
  "customer": {
    "id": 88,
    "name": "Jane Smith",
    "email": "jane@example.com",
    "phone": "+34600111222"
  },
  "start_at": "2026-05-12T10:30:00+02:00",
  "end_at": "2026-05-12T11:00:00+02:00",
  "start_local": "2026-05-12T10:30:00+02:00",
  "end_local": "2026-05-12T11:00:00+02:00",
  "timezone": "Europe/Madrid",
  "status": "confirmed",
  "appointment_kind": "booking",
  "created_via": "api",
  "party_size": 1,
  "notes": "Booked from Retell AI",
  "created_at": "2026-05-11T18:42:00+02:00"
}
Note: start_local and end_local are the same dates as start_at/end_at converted to the calendar's time zone (ISO 8601 with local offset).

POST /calendars/{calendar}/appointments

Creates an appointment. Use Idempotency-Key to avoid duplicates on retries.

HeaderRequiredDescription
AuthorizationYesBearer mrcal_...
Content-TypeYesapplication/json
Idempotency-KeyNoIdempotency key cached for 24 hours

Body:

FieldTypeRequiredRules
service_idintegerYesmin:1
start_atdateYesStart date/time
customerobjectYesRequired object
customer.namestringNomax:255
customer.first_namestringNomax:120
customer.last_namestringNomax:120
customer.emailemailNomax:255
customer.phonestringNomax:64
resource_idintegerNomin:1
any_resourcebooleanNoSelects the first free resource
statusstringNopending or confirmed
party_sizeintegerNomin:1, max:999
notesstringNomax:4000
sourcestringNomax:64
external_idstringNomax:191
POST /api/calendar/v1/calendars/12/appointments
Authorization: Bearer mrcal_...
Content-Type: application/json
Idempotency-Key: retell-call-abc123-slot-20260512-1030

{
  "service_id": 5,
  "start_at": "2026-05-12T10:30:00+02:00",
  "customer": {
    "name": "Jane Smith",
    "phone": "+34600111222",
    "email": "jane@example.com"
  },
  "any_resource": true,
  "notes": "Booked from Retell AI",
  "source": "retell_ai"
}

Response 201 Created: full appointment object wrapped in { "data": { ... } }.

If the same Idempotency-Key is repeated within 24h: 200 with X-Idempotent-Replay: true.

If no resource is available: 422 { "message": "slot_unavailable" }


GET /calendars/{calendar}/appointments

Lists the calendar's appointments.

ParameterTypeRules
fromdateOptional
todateOptional
statusstringpending, confirmed, cancelled, completed, no_show
limitintegermin:1, max:200, default 100
GET /api/calendar/v1/calendars/12/appointments?from=2026-05-12&to=2026-05-19&status=confirmed&limit=50
Authorization: Bearer mrcal_...

Responds with { "data": [ ... ] } — an array of appointment objects.


GET /appointments/{appointment}

Returns an appointment by its ID.

GET /api/calendar/v1/appointments/4821
Authorization: Bearer mrcal_...

Responds with { "data": { ... } }.


POST /appointments/{appointment}/cancel

Cancels an appointment. If it is already cancelled, returns the appointment unchanged.

FieldTypeRules
reasonstringmax:1000, default cancelled_via_calendar_api
notify_customerbooleanDefault false
POST /api/calendar/v1/appointments/4821/cancel
Authorization: Bearer mrcal_...
Content-Type: application/json

{
  "reason": "The customer requested cancellation by phone",
  "notify_customer": true
}

Responds 200 with the appointment object with "status": "cancelled".


PATCH /appointments/{appointment}

Modifies or reschedules an appointment. All fields are optional.

FieldTypeRules
service_idintegermin:1
resource_idintegermin:1
start_atdateNew start date/time
statusstringpending, confirmed, cancelled, completed, no_show
notesstringmax:4000
notify_customerbooleanDefault false

Sending start_at triggers appointment.rescheduled. Other fields trigger appointment.updated.

PATCH /api/calendar/v1/appointments/4821
Authorization: Bearer mrcal_...
Content-Type: application/json

{
  "start_at": "2026-05-14T16:00:00+02:00",
  "notify_customer": true
}

Responds 200 with the updated appointment object.


Integration guides

Retell AI

Typical flow for booking during a call:

1. Agent detects booking intent
2. GET /calendars/{calendar}/availability with service_id
3. Caller picks a time
4. POST /calendars/{calendar}/appointments with Idempotency-Key
5. Appointment created with created_via = api

Minimum scopes: calendar:read, calendar:availability, calendar:appointments:create

Use an Idempotency-Key made of call ID + slot to avoid double bookings on retries.


Webhooks

Dhalinta AI Automation sends HTTP POST notifications when appointment events occur.

Available events

EventWhen it fires
appointment.createdWhen an appointment is created
appointment.updatedWhen fields are updated
appointment.cancelledWhen cancelled
appointment.rescheduledWhen moved to another time

Configuration

Panel → Settings → API & Integrations → Webhooks.

FieldDescription
urlDestination URL
eventsList of events; empty = all
calendar_idsList of calendars; empty = all
is_activeEnables or disables the endpoint

The generated secret uses the mrcal_whsec_ prefix.

Signature headers

HeaderValue
X-Dhalinta AI Automation-EventEvent name, e.g. appointment.created
X-Dhalinta AI Automation-DeliveryDelivery ID with the mrcal_evt_ prefix
X-Dhalinta AI Automation-TimestampUnix timestamp in seconds
X-Dhalinta AI Automation-Signaturesha256= + HMAC-SHA256

Verify the signature

The signature is HMAC-SHA256 over {X-Dhalinta AI Automation-Timestamp}.{raw_json_body}.

PHP:

$secret    = 'mrcal_whsec_...';
$timestamp = $_SERVER['HTTP_X_MUSERELAY_TIMESTAMP'] ?? '';
$rawBody   = file_get_contents('php://input');
$expected  = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
$received  = $_SERVER['HTTP_X_MUSERELAY_SIGNATURE'] ?? '';

if (!hash_equals($expected, $received)) {
    http_response_code(401);
    exit('Invalid signature');
}

Node.js:

const crypto = require('crypto');
const secret = 'mrcal_whsec_...';
const timestamp = req.headers['x-muserelay-timestamp'] ?? '';
const signed = `${timestamp}.${req.rawBody}`;
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(signed).digest('hex');
const received = req.headers['x-muserelay-signature'] ?? '';

if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) {
  res.status(401).send('Invalid signature');
}
Important: keep the raw body before parsing it. It is signed with JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE.

Sample payload — appointment.created

{
  "id": "mrcal_evt_550e8400-e29b-41d4-a716-446655440000",
  "event": "appointment.created",
  "created_at": "2026-05-11T18:42:00+00:00",
  "data": {
    "id": 4821,
    "appointment_id": 4821,
    "calendar_id": 12,
    "workspace_id": 3,
    "organization_id": 2,
    "service_id": 5,
    "service_name": "Haircut",
    "resource_id": 3,
    "resource_name": "Maria",
    "contact_id": 88,
    "customer": {
      "id": 88,
      "name": "Jane Smith",
      "email": "jane@example.com",
      "phone": "+34600111222"
    },
    "start_at": "2026-05-12T10:30:00+02:00",
    "end_at": "2026-05-12T11:00:00+02:00",
    "timezone": "Europe/Madrid",
    "status": "confirmed",
    "appointment_kind": "booking",
    "created_via": "api",
    "party_size": 1,
    "notes": "Booked from Retell AI"
  }
}

Retry policy

ItemValue
Maximum attempts3
Backofffixed 30 seconds
Timeout HTTP10 seconds
SuccessAny HTTP 2xx status