Skip to content

Dental API

import { Badge } from ‘@astrojs/starlight/components’;

All dental routes require:

  • JWT in Authorization: Bearer <token>
  • Active subscription on a plan that includes dental features (pro, clinica, trial, or forever_free)
  • Dental clinic type — the clinic’s clinic_type must be dental

All routes are prefixed /api/dental.


A treatment plan groups one or more clinical procedures into a named sequence for a patient. Each procedure is a “step” on the plan.

GET /api/dental/treatment-plans

Optional query param: ?patient_id=<uuid> — filter to one patient.

Response 200:

{
"plans": [
{
"id": "uuid",
"clinic_id": "uuid",
"patient_id": "uuid",
"title": "Ortodoncia completa",
"status": "active",
"estimated_total": 1200.00,
"notes": null,
"created_at": "2026-01-15T10:00:00Z",
"updated_at": "2026-01-15T10:00:00Z"
}
]
}

GET /api/dental/treatment-plans/:id

Returns the plan with its full steps array.

Response 200:

{
"id": "uuid",
"title": "Ortodoncia completa",
"status": "active",
"estimated_total": 1200.00,
"notes": null,
"steps": [
{
"id": "uuid",
"treatment_plan_id": "uuid",
"procedure": "Colocación de brackets",
"status": "completed",
"estimated_cost": 300.00,
"notes": null,
"appointment_id": null,
"step_order": 1
}
]
}

Response 404: { "error": "Treatment plan not found" }


POST /api/dental/treatment-plans

Body:

{
"patient_id": "uuid",
"title": "Ortodoncia completa",
"estimated_total": 1200.00,
"notes": "Tratamiento de 18 meses"
}
Field Required Type Notes
patient_id Yes UUID Must belong to this clinic
title Yes string Min 1 char
estimated_total No number Positive
notes No string

Response 201: Created plan object.


PATCH /api/dental/treatment-plans/:id

Body: Any subset of updatable fields.

{
"title": "Ortodoncia + blanqueamiento",
"status": "completed",
"estimated_total": 1400.00,
"notes": "Blanqueamiento añadido al finalizar"
}

status values: active, completed, paused, cancelled

Response 200: Updated plan object. Response 404: { "error": "Treatment plan not found" }


DELETE /api/dental/treatment-plans/:id

Soft delete — sets status = 'cancelled'. The plan remains in the database.

Response 200: Updated (cancelled) plan object. Response 404: { "error": "Treatment plan not found" }


POST /api/dental/treatment-plans/:id/steps

Body:

{
"procedure": "Colocación de brackets",
"estimated_cost": 300.00,
"notes": "Primera sesión",
"appointment_id": "uuid"
}
Field Required Type Notes
procedure Yes string Procedure name
estimated_cost No number Positive
notes No string
appointment_id No UUID Link to existing appointment

Response 201: Created step object. Response 404: { "error": "Treatment plan not found" }


PATCH /api/dental/treatment-steps/:id

Updates a step’s status, notes, cost, or appointment link. Tenant-isolated — only steps belonging to your clinic can be updated.

Body: Any subset of updatable fields.

{
"status": "completed",
"notes": "Completado sin complicaciones",
"estimated_cost": 320.00,
"appointment_id": "uuid"
}

status values: pending, scheduled, completed, skipped

Response 200: Updated step object. Response 404: { "error": "Treatment step not found" }


DELETE /api/dental/treatment-steps/:id

Deletes a step and renumbers remaining steps so step_order stays sequential. Tenant-isolated.

Response 200: { "ok": true } Response 404: { "error": "Treatment step not found" }


A payment plan tracks how a patient pays for their treatment over time. Payments are recorded individually and can be voided if entered incorrectly.

GET /api/dental/payment-plans/summary

Clinic-wide aggregate. This route must be called before GET /payment-plans/:id (it’s registered first to avoid route conflict with :id).

Response 200:

{
"total_collected": 4500.00,
"total_pending": 1200.00,
"active_count": 8
}

GET /api/dental/payment-plans

Optional query param: ?patient_id=<uuid> — filter to one patient.

Response 200:

{
"plans": [
{
"id": "uuid",
"clinic_id": "uuid",
"patient_id": "uuid",
"description": "Ortodoncia — pago mensual",
"total_amount": 1200.00,
"amount_paid": 400.00,
"status": "active",
"reminder_enabled": true,
"reminder_day_of_month": 5,
"notes": null
}
]
}

GET /api/dental/payment-plans/:id

Returns the plan with its full payment history (non-voided payments only).

Response 200: Plan object with payments array. Response 404: { "error": "Payment plan not found" }


POST /api/dental/payment-plans

Body:

{
"patient_id": "uuid",
"description": "Ortodoncia — pago mensual",
"total_amount": 1200.00,
"treatment_plan_id": "uuid",
"reminder_enabled": true,
"reminder_day_of_month": 5,
"notes": null
}
Field Required Type Notes
patient_id Yes UUID Must belong to this clinic
description Yes string Min 1 char
total_amount Yes number Positive
treatment_plan_id No UUID Link to a treatment plan
reminder_enabled No boolean Default false
reminder_day_of_month No integer 1–28; day of month to send reminder
notes No string

Response 201: Created payment plan object.


PATCH /api/dental/payment-plans/:id

Body: Any subset of updatable fields.

{
"description": "Ortodoncia + retención",
"status": "paid",
"reminder_enabled": false,
"reminder_day_of_month": 10,
"notes": "Tratamiento finalizado"
}

status values: active, paid, overdue, cancelled

Response 200: Updated plan object. Response 404: { "error": "Payment plan not found" }


DELETE /api/dental/payment-plans/:id

Soft delete — sets status = 'cancelled'.

Response 200: Updated (cancelled) plan object. Response 404: { "error": "Payment plan not found" }


POST /api/dental/payment-plans/:id/payments

Records a payment against an active plan. Updates amount_paid and recalculates status.

Body:

{
"amount": 100.00,
"payment_method": "cash",
"notes": "Pago enero"
}
Field Required Type Notes
amount Yes number Positive
payment_method No string cash, transfer, card, other
notes No string

Response 201: Updated plan object with new payment reflected in amount_paid. Response 404: { "error": "Payment plan not found" }


DELETE /api/dental/payments/:id

Voids a specific payment record. Requires a reason. Updates amount_paid on the parent plan.

Body:

{
"reason": "Error de captura — monto incorrecto"
}

reason is required.

Response 200: Voided payment object. Response 404: { "error": "Payment not found or already voided" }


A recall campaign automatically contacts patients who are due for a follow-up visit. The nightly job evaluates eligibility and queues WhatsApp messages for each campaign.

GET /api/dental/recall-campaigns

Response 200:

{
"campaigns": [
{
"id": "uuid",
"clinic_id": "uuid",
"name": "Revisión 6 meses",
"procedure_type": "limpieza dental",
"recall_interval_days": 180,
"message_template": "Hola {nombre}, han pasado 6 meses...",
"send_time": "09:00:00",
"max_attempts": 2,
"is_active": true,
"created_at": "2026-01-10T00:00:00Z"
}
]
}

GET /api/dental/recall-campaigns/:id

Returns the campaign with recent contact records.

Response 200: Campaign object with recent_contacts array. Response 404: { "error": "Campaign not found" }


POST /api/dental/recall-campaigns

Body:

{
"name": "Revisión 6 meses",
"message_template": "Hola {nombre}, han pasado 6 meses desde tu última visita a {clinica}. ¿Te gustaría agendar tu próxima {procedimiento}?",
"procedure_type": "limpieza dental",
"recall_interval_days": 180,
"send_time": "09:00:00",
"max_attempts": 2
}
Field Required Type Notes
name Yes string Campaign display name
message_template Yes string Supports variables (see below)
procedure_type No string Used to match patient history
recall_interval_days No integer Days since last visit; default 180
send_time No string HH:MM:SS in CST; default “09:00:00”
max_attempts No integer 1–5; default 2

Template variables: {nombre}, {clinica}, {procedimiento}, {ultima_visita}

Invalid variables return 400 with INVALID_TEMPLATE_VARS.

Response 201: Created campaign object.


PATCH /api/dental/recall-campaigns/:id

Body: Any subset of campaign fields (at least one required).

{
"name": "Revisión semestral",
"is_active": false,
"recall_interval_days": 182
}

Same validation rules as create. At least one field required.

Response 200: Updated campaign object. Response 404: { "error": "Campaign not found" }


DELETE /api/dental/recall-campaigns/:id

Soft delete — sets is_active = false. Patients in flight still receive their queued message.

Response 200: Updated (deactivated) campaign object. Response 404: { "error": "Campaign not found" }


GET /api/dental/recall-campaigns/:id/contacts

Paginated list of patients who have been contacted by this campaign.

Query params:

  • ?page=1 — page number (default 1)
  • ?limit=20 — results per page (1–100, default 20)

Response 200:

{
"contacts": [
{
"id": "uuid",
"patient_id": "uuid",
"patient_name": "Ana García",
"patient_phone": "+50312345678",
"status": "sent",
"sent_at": "2026-06-01T15:00:00Z",
"responded_at": null
}
],
"total": 45,
"page": 1,
"limit": 20
}

GET /api/dental/recall-stats

Aggregate stats across all campaigns for the clinic in the current month.

Response 200:

{
"sent_this_month": 45,
"responded": 12,
"opted_out": 3,
"response_rate": 0.27
}

PATCH /api/dental/patients/:id/recall-opt-out

Sets recall_opted_out = true on the patient record. The patient will not appear in any campaign’s eligible list going forward.

No request body required.

Response 200: Updated patient object. Response 404: { "error": "Patient not found" }