Sign and revoke consents
Draft the template, collect the signature and revoke it against the exact text.
ClinicsOnly in clinic workspaces.
Informed consent has two halves with opposite rules. The template is the text the clinic drafts. It names no patient, it's versioned like code, and any integration with the configuration scope can maintain it. The consent is what one particular person signed, against which exact version, when and for what. That's health data, it asks for a scope of its own, and no connected app reaches it.
This chapter covers both, in that order: first what belongs to everyone, then what belongs to somebody.
Templates
A consent template has versions and only one is live. The draft is edited, then published. Publishing freezes that text for good: consents signed against it keep pointing at that version even if another is drafted tomorrow.
curl -X POST https://api.vitrinadev.com/api/v1/clinic/consent-templates \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Consentimiento de ortodoncia",
"description": "Tratamiento de ortodoncia fija: alcance, riesgos y duración estimada.",
"signature_methods": ["tablet", "public_link"],
"body_html": "<p>Autorizo el tratamiento de ortodoncia fija descrito por el profesional tratante…</p>",
"allows_photos": true,
"allows_marketing_use": false
}'The cycle is always the same. POST …/{id}/draft saves the draft, POST …/{id}/publish makes it live, POST …/{id}/versions starts the next one from the live one and POST …/{id}/retire retires it without touching anything already signed. signature_methods bounds how it may be signed, on a tablet at the front desk or through a link the patient opens. allows_photos and allows_marketing_use are the two clauses that enable different things further down.
Form templates
Intake forms work the same way, at /clinic/form-templates, with schema instead of body_html. They aren't sensitive either, for the same reason: a question is nobody's answer. What one person answered lives in Clinical record.
Retention
GET /clinic/retention says how many years each kind of document is kept, and PUT /clinic/retention changes it for one:
{
"data": {
"data": [
{ "kind": "examen", "retention_years": 15, "legal_minimum_years": 15, "is_default": true },
{ "kind": "boleta", "retention_years": 6, "legal_minimum_years": 6, "is_default": true }
]
}
}legal_minimum_years is the floor and can't be lowered: a clinic may decide to keep more, never less. It's the number that decides the outcome when somebody asks for their record to be erased (Record documents).
Templates are not sensitive
They name no patient and write nothing into the record's access log. An integration can maintain the texts without access to the records, and a connected app does read them.
Signed consents
From here on, everything is sensitive
It demands clinic_record:read, or clinic_record:write to write, on top of
the clinic scope. Every read lands in the record's access log, and a
connected app receives 403 CONNECTED_APP_SENSITIVE_DATA whatever scopes
it holds (Personal and health data).
Requesting a consent creates it pending and pins it to the live version of the template at that instant:
curl -X POST "https://api.vitrinadev.com/api/v1/clinic/patients/$PACIENTE/consents" \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{ "template_id": "66666666-0000-4000-8000-000000000001", "encounter_id": "44444444-0000-4000-8000-000000000001" }'From there two paths. POST /clinic/consents/{id}/link mints the tokenised link the patient opens and signs on their own. POST /clinic/consents/{id}/sign signs it at the front desk, assisted by somebody from the clinic. This is a signed one:
{
"data": {
"id": "77777777-0000-4000-8000-000000000001",
"template_id": "66666666-0000-4000-8000-000000000001",
"template_version_id": "88888888-0000-4000-8000-000000000001",
"template_version": 1,
"clinic_patient_id": "12121212-0000-4000-8000-000000000001",
"encounter_id": "44444444-0000-4000-8000-000000000001",
"status": "signed",
"signed_by_name": "María José Fuentes Lagos",
"signed_by_document": "12.345.678-5",
"signed_by_role": "titular",
"signature_method": "tablet",
"signed_at": "2026-09-15T21:53:03.507Z",
"photo_consent": true,
"marketing_use_consent": false,
"revoked_at": null,
"expires_at": null
}
}What's worth knowing about each field:
template_version_idandtemplate_versionare the whole point of this route. A consent is worth the text the person read, andGET /clinic/consents/{id}returns that text, not the one in force today.signed_by_rolesays who signed: the patient themselves (titular) or whoever answers for them, from the family bond (Patient register).photo_consentandmarketing_use_consentare two different permissions. The first enables the clinical gallery; the second, using those images outside the record. Signing for the treatment signs for neither.expires_atexpires the consent on its own, without anybody having to remember.
GET /clinic/consents lists the whole clinic's, signed, pending and revoked, filterable by status. GET /clinic/patients/{id}/consents returns one person's.
Revoking
POST /clinic/consents/{id}/revoke revokes it with a mandatory reason. It is not a delete: the row stays, with revoked_at and the reason. It has to be possible to say that on one date there was consent and from another there wasn't. What revoking does do is close whatever that consent enabled.
The clinical gallery
Before-and-after images hang off the patient and only go in with consent. POST /clinic/patients/{id}/photos fails if the person has no signed, current consent whose template declares allows_photos.
Each image carries its zone, its phase (before / after), the date it was taken and a pair_id joining the before to the after. GET /clinic/patients/{id}/photos returns them grouped by zone and date. Deleting one is POST /clinic/photos/{id}/delete with a reason, and it leaves a tombstone: what was there and why it no longer is.
Events
| Event | When |
|---|---|
clinic_consent.signed | It was signed; carries the version it was signed against |
clinic_consent.revoked | It was revoked, with its reason |
Both arrive as a notice, with data_omitted: "sensitive": the signed text and the name of whoever signed don't travel in a webhook. The notice is enough to react, to close a flow or tell the front desk, and whoever holds the scope reads the detail with their credential.
The rendered consent hangs off the record as one more document, with its retention deadline, in Record documents.