Overview
The Dental Odontogram application uses Airtable as the database to store patient records, odontogram images, and formatted dental notes. Airtable provides a flexible, spreadsheet-like interface with powerful API capabilities.
Environment Variables
To integrate with Airtable, configure the following environment variables:
Your Airtable personal access token or API key for authentication
The ID of your Airtable base (starts with “app”)
Setting Up Environment Variables
Add these to your .env.local file:
AIRTABLE_API_KEY=patAbCdEfGhIjKlMnOp.1234567890abcdef
AIRTABLE_BASE_ID=appXYZ123456789
Keep your Airtable API key secure. Never commit it to version control or expose it in client-side code.
Base Structure
The application expects an Airtable base with a table named “Pacientes” (Patients).
Required Table
Table Name: Pacientes
This table stores all patient information and odontogram data.
Record Structure and Fields
Each patient record in Airtable contains the following fields:
Image Attachment Field
Array of image attachments containing odontogram PNG files from Cloudinary
The field name is configurable via the fieldName parameter in the upload request.
Structure:
[
{
"url": "https://res.cloudinary.com/...",
"filename": "odontogram.png"
}
]
Notes Fields
notas-odontograma-paciente
Cumulative history of all odontogram notes with timestamps (appended with each upload)
ultima-nota-odontograma-paciente
Most recent odontogram note (replaced with each upload)
=============================
PACIENTE: Juan Pérez
ODONTOGRAMA GENERADO: 03/03/2026, 14:30:45
=============================
TRATAMIENTOS Y OBSERVACIONES:
PIEZA 11:
• Condiciones:
- Caries
• Prestaciones Requeridas:
- Endodoncia
• Notas: Requiere atención urgente
PIEZA 21:
• Prestaciones Preexistentes:
- Corona
• Condiciones:
- Desgaste
How Data Is Stored
1. Fetch Existing Data
Before updating, the endpoint retrieves current data:
const getRecordUrl = `https://api.airtable.com/v0/${process.env.AIRTABLE_BASE_ID}/Pacientes/${recordId}`
const getResponse = await fetch(getRecordUrl, {
method: 'GET',
headers: {
Authorization: `Bearer ${process.env.AIRTABLE_API_KEY}`,
},
})
let existingAttachments = []
let existingNotes = ''
if (getResponse.ok) {
const currentRecord = await getResponse.json()
existingAttachments = currentRecord.fields[fieldName] || []
existingNotes = currentRecord.fields[jsonFieldName] || ''
}
2. Append New Attachment
The new Cloudinary URL is added to existing attachments:
const newAttachment = {
url: uploadResult.secure_url,
filename: file.originalFilename || 'odontogram.png',
}
const allAttachments = [...existingAttachments, newAttachment]
JSON data is formatted into readable text:
const parsedData = JSON.parse(jsonData)
const timestamp = now.toLocaleString('es-AR', {
timeZone: 'America/Argentina/Buenos_Aires',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
})
let formattedText = `\n\n=============================\nPACIENTE: ${parsedData.nombre}\nODONTOGRAMA GENERADO: ${timestamp}\n=============================\n\n`
if (parsedData.piezas && parsedData.piezas.length > 0) {
formattedText += `TRATAMIENTOS Y OBSERVACIONES:\n\n`
parsedData.piezas.forEach((pieza) => {
formattedText += `PIEZA ${pieza.pieza}:\n`
if (pieza.condiciones && pieza.condiciones.length > 0) {
formattedText += ` • Condiciones:\n`
pieza.condiciones.forEach((condicion) => {
formattedText += ` - ${condicion}\n`
})
}
// ... more formatting
})
}
finalNotesText = existingNotes + formattedText
4. Update Record
All data is updated in a single PATCH request:
const airtableUrl = `https://api.airtable.com/v0/${process.env.AIRTABLE_BASE_ID}/Pacientes/${recordId}`
const updateFields = {
[fieldName]: allAttachments, // Image attachments
[jsonFieldName]: finalNotesText, // Cumulative notes
'ultima-nota-odontograma-paciente': latestNoteText // Latest note only
}
const attachmentData = {
fields: updateFields,
}
const airtableResponse = await fetch(airtableUrl, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.AIRTABLE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(attachmentData),
})
Data Fields Breakdown
{
"nombre": "Patient Name",
"piezas": [
{
"pieza": "11",
"condiciones": ["Caries", "Fractura"],
"prestacion_preexistente": ["Corona", "Amalgama"],
"prestacion_requerida": ["Endodoncia", "Corona"],
"notas": "Additional notes about this tooth"
}
]
}
Field Descriptions
Patient name (displayed in formatted notes)
Array of dental pieces (teeth) with findings
Tooth number (e.g., “11”, “21”, “36”)
Existing conditions (e.g., “Caries”, “Fractura”)
piezas[].prestacion_preexistente
Pre-existing treatments (e.g., “Corona”, “Amalgama”)
piezas[].prestacion_requerida
Required treatments (e.g., “Endodoncia”, “Extracción”)
Additional notes specific to this tooth
Setup Instructions
1. Create an Airtable Account
Sign up at airtable.com (free tier available).
2. Create a Base
- Click “Add a base” → “Start from scratch”
- Name your base (e.g., “Dental Patients”)
- Note your base ID from the URL:
https://airtable.com/appXYZ123456789/...
3. Create the Pacientes Table
- Rename the default table to “Pacientes”
- Add the following fields:
- Name (Single line text) - Auto-created
- odontograma-imagenes (Attachment)
- notas-odontograma-paciente (Long text)
- ultima-nota-odontograma-paciente (Long text)
4. Get Your API Key
- Go to airtable.com/create/tokens
- Click “Create token”
- Give it a name (e.g., “Dental Odontogram API”)
- Add scopes:
data.records:read
data.records:write
- Add access to your base
- Create token and copy it
Add to .env.local:
AIRTABLE_API_KEY=your-token-here
AIRTABLE_BASE_ID=appXYZ123456789
Error Handling
Credential Validation
if (!process.env.AIRTABLE_API_KEY || !process.env.AIRTABLE_BASE_ID) {
return res
.status(500)
.json({ error: 'Airtable credentials not configured' })
}
Update Errors
if (!airtableResponse.ok) {
const errorText = await airtableResponse.text()
throw new Error(
`Airtable error: ${airtableResponse.status} - ${errorText}`
)
}
Common errors:
- 401 Unauthorized - Invalid API key
- 404 Not Found - Base ID or record ID not found
- 422 Unprocessable Entity - Invalid field names or data format
All timestamps use Argentina timezone (America/Argentina/Buenos_Aires):
const timestamp = now.toLocaleString('es-AR', {
timeZone: 'America/Argentina/Buenos_Aires',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
})
// Output: 03/03/2026, 14:30:45
Data Retention
The application appends data rather than replacing it:
- Image attachments - All historical images are preserved
- Cumulative notes - All previous notes remain with new notes appended
- Latest note - Only the most recent entry (replaced with each upload)
This provides a complete audit trail of all odontogram changes over time.
Next Steps