Signing Integration Guide
How to store a document signed outside Medevio through the public REST API: create the patient request, upload the signed PDF and read it back.
Signing Integration Guide
This guide is for systems that let a patient sign a document on their own side — Signi, DocuSign, an in-house signing portal — and then want the result to land in Medevio: a patient request of the right type, the signed PDF stored on it, and the patient tagged.
Medevio does not sign anything in this flow and never calls your signing provider. You sign, you upload the finished PDF. Everything below uses the public REST API — no custom endpoint is involved.
Basics
-
Base URL:
https://api.medevio.cz/external/v1/clinics/{clinicSlug} -
Clinic identification: the clinic slug — the stable identifier of the clinic in Medevio — is part of the URL path, same as in the rest of the Medevio API.
-
Content type:
application/jsonrequests and responses, except the direct upload to storage (step 5), which sends the raw file. -
Response envelope: every success response wraps the payload in
data; errors return{ "error": "...", "code": "..." }. -
Machine-readable reference: all endpoints below are documented in the Medevio API reference under the Patient Requests, Patients, Clinics and Tags tags.
Authentication
All endpoints require an API key (Personal Access Token) sent with every request:
Authorization: Bearer <token>
Generate one in Medevio: clinic picture → Propojky (Integrations) → Správa API klíčů (API key management). This flow needs both the Read scope (lookups, request detail) and the Write scope (create, upload, register). A missing or invalid token returns 401; a token without the required scope or without access to the clinic in the URL returns 403.
Treat the key as a server-side secret:
-
Keep it on your backend. Step 5 uploads straight to cloud storage, which may tempt you to move the flow into a browser or mobile client — do not ship the key there.
-
Scopes are coarse.
Writecovers the whole external API for the clinics the key is bound to, not just this flow. Issue the key under a dedicated service account rather than a doctor's account: every write is attributed to the account the key belongs to. -
Set an expiry and rotate. Deactivate the key immediately if it may have leaked.
Prerequisites
Configure the request type
The single most important setup step happens in Medevio, not over the API: the clinic creates (or reuses) a patient request type for signed documents and assigns it default patient tags — in the request type settings, field Štítky pacienta ("Patient tags": adds tags to the patient automatically when a request is created).
That field is part of the request automations, which are only available on selected plans — if it is greyed out, the clinic needs the Automatizace požadavků ("Request automations") feature enabled by Medevio support. The API side is unaffected: whatever tags the type carries are applied on every create.
Every request created from that type then tags the patient automatically — you never call a tagging endpoint. Two consequences worth knowing:
-
The tags land on the patient (their patient record in this clinic), not on the request.
-
Tagging is best-effort: tags deleted in the meantime are skipped silently, and any other tagging failure is logged without failing the create. A successful create is therefore not proof that tags were applied — read the patient back with
POST …/patients/search(data[].tags) if you need certainty.
Make sure the patient exists
A request can only be created for a patient who already has a patient record in the clinic; otherwise create returns 403. Patients that only exist in your system have to be imported first — see POST …/patients/import (single) or POST …/patients/import-bulk in the API reference.
Note the emphasis on this clinic: if the clinic belongs to an organization with patient sharing enabled, patient search can also return patients whose record lives in a sibling clinic. Those ids look ordinary but are not usable here. Step 2 explains how to tell them apart.
The flow at a glance
Patient signs the document in your system
│
├─ 1. POST …/patientRequestTypes/search → find the request type id (userECRFId)
├─ 2. POST …/patients/search → find the patientId
├─ 3. POST …/patientRequests/create → request is created, default tags land on the patient
│
├─ 4. POST …/patientRequests/{id}/attachments/upload-link → presigned URL + fileHash
├─ 5. PUT <presigned URL> → the signed PDF goes straight to storage
├─ 6. POST …/patientRequests/{id}/attachments → register it as a medical record
│
└─ 7. GET …/patientRequests/{id} → optional: read the document back
Steps 1 and 2 are lookups you can cache or replace with ids you already store. Steps 3–6 are the write path; step 7 is only needed when you want to read the stored document back — step 6 already returns the attachment id.
1. Find the request type
The request type id is what Medevio calls userECRFId when creating a request.
// POST https://api.medevio.cz/external/v1/clinics/my-clinic/patientRequestTypes/search
{}
// Response (200), abbreviated — only active types are returned
{
"data": [
{
"id": "1b7c9f42-3f6a-4c58-9a1e-2d4f6b8c0e11",
"ecrfId": "MISCELLANEOUS",
"name": "Signed consent",
"description": "Consent signed outside Medevio",
"requiresReservation": false,
"requiresPayment": false,
"requiresSignature": false,
"reservationLength": 15,
"remindDaysBefore": 0
}
]
}
requiresReservation tells you what the clinic expects from the type; this endpoint does not enforce it. Creating a request without timeSlotInput for a type flagged requiresReservation succeeds and simply produces a request with no appointment. For a signing flow, pick a type that does not need one.
requiresSignature refers to Medevio's own in-app signing and is unrelated to this guide — leave it false for documents you signed yourself.
2. Find the patient
If you already store Medevio patient UUIDs, skip this step. Otherwise look the patient up — most integrations match on their own identifier, which Medevio stores as an external id:
// POST https://api.medevio.cz/external/v1/clinics/my-clinic/patients/search
{
"filter": {
"externalId": "EXT-12345",
"externalIdSource": "my-system"
},
"pagination": { "limit": 1 }
}
The patient UUID is data[].id in the response.
Processing a backlog of signed documents? Resolve the ids in batches instead of one call per document: filter.externalIds accepts up to 100 identifiers at a time (combined with externalId as a union).
Not every search result can be used in step 3
Patient search is scoped to the organization, while creating a request is scoped to the single clinic in the URL. In a clinic that shares patients with its siblings, search therefore also returns patients who have no record here — and POST …/patientRequests/create answers 403 for them.
The lookup above is safe: matching on externalId/externalIds only ever considers external ids registered in the clinic you are addressing, so those results always belong to it. The mismatch appears once you search some other way — free-text filter.query, or no filter at all when reconciling a whole list.
Two ways to stay on the safe side:
-
Ask for the usable ones only: add
"inClinic": trueto the filter. -
Or read
data[].isInClinicon each result and skip thefalseones (importing them first, if they should become patients of this clinic).
The same distinction explains empty-looking data: for a patient shared from a sibling clinic, note, tags and externalIds come back empty and status reads WAITING, no matter what the other clinic holds. That means "no record in this clinic", not "no tags".
3. Create the patient request
// POST https://api.medevio.cz/external/v1/clinics/my-clinic/patientRequests/create
{
"patientId": "6f2e1a90-77bd-4c1e-b2d3-9c0a5e7f1234",
"userECRFId": "1b7c9f42-3f6a-4c58-9a1e-2d4f6b8c0e11",
"userNote": "Consent v1.4 signed on 2026-07-29 via our portal"
}
// Response (200), abbreviated
{
"data": {
"id": "0a9d3c77-51b2-4a6e-8f10-6b2c7d9e4455",
"title": "Signed consent",
"createdAt": "2026-07-29T09:12:44.000Z",
"patientId": "6f2e1a90-77bd-4c1e-b2d3-9c0a5e7f1234",
"tags": [],
"medicalRecords": [],
"reservations": []
}
}
Keep data.id — the next three steps hang off it.
Three things that surprise integrators:
-
medicalRecordsis empty here, always. Create and search responses never list attachments; only the detail endpoint (step 7) does. -
tagsis about tags on the request, not the patient. Default tags from the request type went to the patient, so this array stays empty unless you assign request tags yourself (see Tags below). -
userNotebecomes the clinic note shown on the request inside Medevio. No REST response returns it, and it is a different field fromuserNoteinPATCH …/patientRequests/{id}. For a marker your own system can read back, use the attachmentdescriptionin step 6.
Add timeSlotInput (start, end, calendarId) only if the signed document should also book an appointment — that variant creates a reservation and closes the request immediately, which is not what a signing flow usually wants.
4. Ask for an upload link
Uploading goes directly to cloud storage, so the file never passes through the API server. Start by asking for a presigned URL:
// POST …/patientRequests/0a9d3c77-51b2-4a6e-8f10-6b2c7d9e4455/attachments/upload-link
{ "contentType": "application/pdf" }
// Response (200)
{
"data": {
"url": "https://<bucket>.s3.<region>.amazonaws.com/medical-record/…?X-Amz-Signature=…",
"fileHash": "5e0b6c1a-8d3f-4a72-9c55-1e2f3a4b5c6d.pdf"
}
}
-
The URL is valid for 10 minutes. Request it right before you upload, not at the start of a batch. If it expires, simply call this endpoint again.
-
Treat the URL as a credential. For those 10 minutes it grants write access to that exact object to anyone holding it, and it can be used more than once. Do not log it, do not persist it, do not hand it to an untrusted client. Never hardcode the host or bucket either — both may change.
-
This endpoint accepts any
contentType. The allowed-types check happens at registration in step 6, so validate against the list in Error handling & limits before you upload, or you will waste the upload.
fileHash is derived from the contentType you sent (that is where the .pdf suffix comes from). Send the same contentType in step 6: the file is linked by fileHash, but the content type you register is what Medevio serves the file with, so a mismatch means the document is delivered with the wrong MIME type and file name.
5. Upload the file
PUT <url from step 4>
Content-Type: application/pdf
<raw bytes of the signed PDF>
A plain HTTP PUT with the file as the body. A 200 from storage means the file is there. Nothing in Medevio references it yet — that is the next step.
6. Register the attachment
// POST …/patientRequests/0a9d3c77-51b2-4a6e-8f10-6b2c7d9e4455/attachments
{
"contentType": "application/pdf",
"fileHash": "5e0b6c1a-8d3f-4a72-9c55-1e2f3a4b5c6d.pdf",
"description": "Consent v1.4 signed.pdf",
"categoryType": "CONSENT",
"visibleToPatient": false
}
// Response (200)
{
"data": {
"id": "b4c8e2d1-9a35-4f60-8712-3d5e6f7a8b9c",
"description": "Consent v1.4 signed.pdf",
"contentType": "application/pdf",
"createdAt": "2026-07-29T09:13:02.000Z"
}
}
-
descriptiondoubles as the file name shown to the clinic and used when the document is downloaded. It is normalized to ASCII for the download (diacritics dropped, spaces replaced with underscores), and it ends up inside the download URL — so keep patient identifiers and clinical detail out of it:Consent 1.4.pdf, notNovak Jan - HIV consent.pdf. -
categoryTypeis optional;CONSENTfits signed consents, other values areANAMNESIS,MEDICAL_REPORT,LAYOFF_REPORT,TEST_RESULTS,LABORATORY_RESULTS,OPINION,REFERRAL,OTHER. -
visibleToPatientdefaults tofalse. Set it totrueonly for documents the patient is meant to see in the Medevio app. There is no API call to hide or delete an already registered document — correcting a mistake requires the clinic to act in the app.
There is no way to attach a document while creating the request — the request has to exist first, because the attachment is registered against its id.
7. Read the document back (optional)
Step 6 already gave you the attachment id, so this call is for verification or for fetching the file later.
GET https://api.medevio.cz/external/v1/clinics/my-clinic/patientRequests/0a9d3c77-51b2-4a6e-8f10-6b2c7d9e4455
// Response (200), abbreviated
{
"data": {
"id": "0a9d3c77-51b2-4a6e-8f10-6b2c7d9e4455",
"doneAt": null,
"medicalRecords": [
{
"id": "b4c8e2d1-9a35-4f60-8712-3d5e6f7a8b9c",
"contentType": "application/pdf",
"description": "Consent v1.4 signed.pdf",
"categoryType": "CONSENT",
"createdAt": "2026-07-29T09:13:02.000Z",
"visibleToPatient": false,
"url": "https://…?X-Amz-Signature=…",
"downloadUrl": "https://…&response-content-disposition=attachment"
}
]
}
}
url opens the file inline, downloadUrl forces a download. Both are presigned for up to 8 hours. Medevio caches them for less than that, so a link handed back from cache always has at least 30 minutes of life left — enough to download it, but not enough to store it or schedule the download for later. Fetch the detail again when you need a fresh link.
This is also the most expensive call in the flow (it loads the patient, tags, reservations and every attachment), so do not call it per document out of habit.
Tags
Nothing to call — the default tags configured on the request type were applied to the patient in step 3.
If you also want a tag on the request itself (e.g. a "Signed" marker in the clinic's inbox), list the clinic's request tags with GET …/tags?type=patient_request and send their ids to PATCH …/patientRequests/{id} as addTagIds.
Evidence of the signature
The signed PDF is the evidence. It carries the signature, the signed version of the document and whatever your provider embeds in it; Medevio adds the upload timestamp and keeps the file retrievable from the request detail. There is no separate "signature version" field to fill in — put the version in the attachment description (readable back through the API) or in userNote (visible to the clinic in the app).
Error handling & limits
Errors use standard status codes with { "error": "...", "code": "..." }:
-
400— validation failure (VALIDATION_FAILED), e.g. acontentTypeoutside the allowed list, or a request detail with more than 1000 attachments. -
401— missing, unknown, expired or deactivated token. -
403— token lacks the scope, has no access to the clinic, or the patient has no record in this clinic (FORBIDDEN). If the id came out of patient search, see step 2: search reaches across the organization, create does not. -
404— unknown patient (PATIENT_NOT_FOUND), unknown request type (USER_ECRF_NOT_FOUND), or a patient request that does not belong to this clinic (PATIENT_REQUEST_NOT_FOUND). -
410— the patient was merged into another record (PATIENT_MERGED). Permanent: the response carriesmergedIntowith the surviving patient id — store that id and retry. -
5xx— internal error. Lookups andGETdetail are safe to retry. Create and attachment registration are not idempotent: a5xxor a timeout may still have persisted the request or the document, and a blind retry produces a duplicate. Check first withPOST …/patientRequests/search(filter bypatientId) or with the request detail, then retry only if nothing was written.
Limits to design around:
-
Allowed attachment types:
application/pdf,image/jpeg,image/png,image/gif,image/webp. The check runs when you register the attachment (step 6), not when you ask for the upload link. -
File size: keep documents under 5 MB. Larger ones are not rejected on upload, but if the clinic forwards the request through eZpráva, oversized attachments are dropped from that message — the encrypted mail has a 25 MB ceiling and attachments inflate roughly 4× on the way.
-
Attachments per request: the detail endpoint returns
400for requests holding more than 1000 attachments. -
Upload URL lifetime: 10 minutes. Download URL lifetime: up to 8 hours, see step 7.
-
Concurrency: keep parallel write flows modest (a handful at a time) — a signed document is a small, sequential five-call flow, and hammering the API gains nothing.
Testing checklist
Before going live, run the whole flow against your test clinic:
-
The request type you intend to use has default patient tags configured, and
requiresReservationisfalse. -
Creating a request returns
data.id, and reading the patient back (POST …/patients/search→data[].tags) shows the expected tags. -
upload-link→PUT→attachmentscompletes, and the registereddescriptionis what the clinic should see. -
The request detail lists the attachment, and
downloadUrlreturns byte-for-byte the PDF you uploaded. -
Your error paths are exercised at least once: an unsupported
contentTypeat registration (400), a request id from another clinic (404), and a rejected upload URL — your client should re-request it (step 4) rather than failing the whole batch. -
If the clinic belongs to an organization: a patient search that is not filtered by external id returns some results with
isInClinic: false, and your client skips them instead of sending them to create and collecting a403.