Dental API
import { Badge } from ‘@astrojs/starlight/components’;
Dental API
Section titled “Dental API ”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_typemust bedental
All routes are prefixed /api/dental.
Treatment Plans
Section titled “Treatment Plans”A treatment plan groups one or more clinical procedures into a named sequence for a patient. Each procedure is a “step” on the plan.
List Treatment Plans
Section titled “List Treatment Plans”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 Treatment Plan
Section titled “Get Treatment Plan”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" }
Create Treatment Plan
Section titled “Create Treatment Plan”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.
Update Treatment Plan
Section titled “Update Treatment Plan”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 Treatment Plan
Section titled “Delete Treatment Plan”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" }
Add Step to Plan
Section titled “Add Step to Plan”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" }
Update Step
Section titled “Update Step”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 Step
Section titled “Delete Step”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" }
Payment Plans
Section titled “Payment Plans”A payment plan tracks how a patient pays for their treatment over time. Payments are recorded individually and can be voided if entered incorrectly.
Summary
Section titled “Summary”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}List Payment Plans
Section titled “List Payment Plans”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 Payment Plan
Section titled “Get Payment Plan”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" }
Create Payment Plan
Section titled “Create Payment Plan”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.
Update Payment Plan
Section titled “Update Payment Plan”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 Payment Plan
Section titled “Delete Payment Plan”DELETE /api/dental/payment-plans/:id
Soft delete — sets status = 'cancelled'.
Response 200: Updated (cancelled) plan object.
Response 404: { "error": "Payment plan not found" }
Record Payment
Section titled “Record Payment”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" }
Void Payment
Section titled “Void Payment”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" }
Recall Campaigns
Section titled “Recall Campaigns”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.
List Campaigns
Section titled “List Campaigns”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 Campaign
Section titled “Get Campaign”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" }
Create Campaign
Section titled “Create Campaign”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.
Update Campaign
Section titled “Update Campaign”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" }
Deactivate Campaign
Section titled “Deactivate Campaign”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 Campaign Contacts
Section titled “Get Campaign Contacts”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}Clinic-Wide Recall Stats
Section titled “Clinic-Wide Recall Stats”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}Opt Out Patient
Section titled “Opt Out Patient”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" }