Form — Server-to-server
Create and manage practices directly from your backend, authenticating with the api_token (Bearer). Base URL: https://app.dokicasa.it.
Main endpoints
| Method | Path | What it does |
|---|---|---|
GET | /api/v3/catalog-forms | All available forms (services, contracts, bundles) with slug and form_url — public |
GET | /api/list-form/canone-concordato-{city} | List the steps of the flow for a city |
GET | /api/v3/form/{slug} | Schema (fields) of a form |
POST | /api/v3/form/{slug} | Submit the form → creates a practice |
PUT | /api/v3/form/{bundleUserServiceId} | Update an already submitted form |
GET | /api/v3/user/{id}/services | List practices (filterable by external_id) |
GET | /api/v3/practices/{practice} | Detail + status of a practice |
GET | /api/v3/practices/{practice}/tasks | Tasks linked to the practice |
POST | /api/v3/tasks/{task}/comments | Reply to a task (comment + attachments) |
GET | /api/v3/contract/{id}/pdf | Download the PDF of a contract practice (bundle step too) |
GET | /api/v3/contract/{id}/doc | Download the Word (.docx) of a contract practice (requires Word download enabled on the account) |
POST | /api/v3/practices/{practice}/duplicate-partner | Duplicate a practice (tenant change): clone the chosen steps into a new practice |
0. Discover the available forms
A single public call returns every form, sorted by name:
curl https://app.dokicasa.it/api/v3/catalog-forms -H "Accept: application/json"Each entry has kind (service / contract / bundle), name, slug and form_url. Bundles include steps (one form per step):
[
{ "kind": "service", "name": "Cadastral visura", "slug": "visura-catastale",
"form_url": "/api/v3/form/visura-catastale", "steps": [] },
{ "kind": "bundle", "name": "Canone Concordato", "slug": "canone-concordato",
"form_url": null, "steps": [
{ "step": 1, "name": "Contract", "type": "Contract",
"slug": "contratto-locazione", "form_url": "/api/v3/form/contratto-locazione" }
] }
]The interactive list (with copy-slug) is also in JavaScript SDK → Available forms.
1. Create a practice
$res = Http::withToken(env('DOKICASA_TOKEN'))
->acceptJson()
->post('https://app.dokicasa.it/api/v3/form/locazione-ad-uso-abitativo-4-4', [
'form' => [ /* ...field answers... */ ],
'metadata' => [
'external_id' => 'order_8842', // YOUR id, to look it up later
],
])
->json();
// $res['id'], $res['external_id']const res = await fetch(
'https://app.dokicasa.it/api/v3/form/locazione-ad-uso-abitativo-4-4',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.DOKICASA_TOKEN}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
form: { /* ...answers... */ },
metadata: { external_id: 'order_8842' },
}),
}
).then((r) => r.json())curl -X POST \
https://app.dokicasa.it/api/v3/form/locazione-ad-uso-abitativo-4-4 \
-H "Authorization: Bearer $DOKICASA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"form":{},"metadata":{"external_id":"order_8842"}}'WARNING
POST /api/v3/form/{slug} may charge credit from the partner wallet. Check the balance before automating bulk creations.
How to fill form
form is an object keyed by field slug; each value is { "type": "<type>", "value": <value> }. Fetch the fields (with type, is_required, depends_from, selectable_options) from GET /api/v3/form/{slug}.
{
"form": {
// selectable: just send the option label as a string
"tipologia_ape_5": { "type": "selectable", "value": "Locazione" },
"numero_di_telefono_per_contatto_5": { "type": "varchar", "value": "3331234567" },
"dati_immobile_6_6": { "type": "address", "value": "Via Cavour 1, 50100 Firenze (FI)" }
},
"metadata": { "external_id": "order_8842" }
}Watch out for some types:
selectable— send the optionlabelas a string (it is normalized automatically):{ "type": "selectable", "value": "Locazione" }. If the field is multi-select (is_multiple: truein the schema), send an array of labels:{ "type": "selectable", "value": ["Option A", "Option B"] }. In all casesvaluemust contain strings (the label): do not send a nested object or an array insidevalue(e.g.{ "value": ["..."] }as a single object is not valid).customer_data— an array of objects, one per person/entity:[{ "type": "Persona Fisica", "name": "...", "address": "...", "fiscal_code": "...", ... }]. Required keys depend ontype(seefieldsin the schema). These blocks must be always filled in even when the schema reportsis_required: 0.immobile(e.g.blocco_immobile) — a list of property objects, each withindirizzo,categoria_catastale,foglio_immobile,parcella_immobile,subalterno_immobile,rendita_immobile. A single object (not in a list) is invalid.file—valueis a list of objects, one per attachment, each withfilenameandcontent. Do not usemultipart/form-data, and there is no separate upload endpoint: the content travels inside the field (see Attaching a file). Manyfilefields are required only on certain branches of aselectable(depends_from): pick the other branch and they are not needed.- A field is required only when active: if its
depends_fromis not satisfied it is ignored.
Fields you can provide later
Some required fields can be deferred: in the schema they carry has_skip_button: true and a skip_button_text with the wording the end user sees (for example the APE on the certification form: "Ne sono in possesso, lo fornirò successivamente" — I have it, I'll provide it later).
To defer one, just leave it out: omit the key from form, or send it with an empty value (null, "", []). There is no extra field to set and no special value to use.
{
"form": {
// ape_1_1_7 is simply absent: it will be requested later
"tipologia_ape_5": { "type": "selectable", "value": "Locazione" }
}
}The practice is accepted and the field is marked as deferred, exactly as if the user had pressed the button in our web interface: our back office sees the document was not available and keeps asking for it. Provide the document later through the usual channels — there is no dedicated endpoint.
WARNING
This applies only to fields with has_skip_button: true. Any other active required field, if missing, still returns 422 with "Campo obbligatorio mancante.".
Attaching a file (file type fields)
file fields are sent inside the same form JSON, not as multipart/form-data. value is a list of objects, one per attachment:
{
"form": {
"documenti_roma_1_1_7": {
"type": "file",
"value": [
{
"filename": "signed-documents.pdf",
"content": "JVBERi0xLjQKJeLjz9MK..." // base64 of the file
}
]
}
},
"metadata": { "external_id": "order_8842" }
}content accepts two formats, detected automatically:
| Format | Example | Behaviour |
|---|---|---|
| base64 | "JVBERi0xLjQK..." | the content is decoded and stored |
| Public URL | "https://your-domain.com/doc.pdf" | the file is downloaded by our server |
filename is optional but recommended: we take the extension from it. If it is missing we try to infer it from the URL. content is mandatory: without it the call returns 400 with File "<name>" has no content.
Full curl example (base64 of a local PDF):
CONTENT=$(base64 -w0 signed-documents.pdf) # on macOS: base64 -i signed-documents.pdf
curl -X POST "https://app.dokicasa.it/api/v3/form/attestazione-contratto-locazione-roma" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"form\": {
\"documenti_roma_1_1_7\": {
\"type\": \"file\",
\"value\": [{ \"filename\": \"signed-documents.pdf\", \"content\": \"$CONTENT\" }]
}
},
\"metadata\": { \"external_id\": \"order_8842\" }
}"WARNING
value must be a list of objects. Passing just the file name as a string ("value": "contract.pdf") returns 201 but attaches nothing: the practice is created with attachments: []. Likewise multipart/form-data does not work, nor does the attachments[] part — that one is only for task reply attachments (see Reply to a task).
NOTE
If the field is required and active, sending it empty ("", [], [""]) returns 422 Campo obbligatorio mancante. A required file field stays required even when the web form shows a "skip" button.
Single practice vs bundle
external_id is your id (from your platform) and also drives grouping:
- First call with a new
external_id→ creates the practice. If the slug is a single service/contract a standalone practice is created; if it is step 1 of a bundle the bundle container is created. - Later call with the same
external_id→ the step is attached to the bundle already started (step 2, 3, …).
external_id stays bound to the practice and makes it findable via filter[external_id].
Bundles must always be started from step 1
If the first call with a new external_id uses the slug of an inner step (step 2, 3, …), the container cannot be created and the practice would end up isolated, with no previous steps. In that case the response is 422:
{
"error": "BUNDLE_NOT_STARTED",
"message": "The service \"Creazione Documenti Canone Concordato Roma\" is step 4 of the bundle \"Canone Concordato Roma\" and cannot be the first call for a new external_id. Start the bundle by calling the first step (\"foglio-caratteristiche-immobile-roma\") with the same external_id, then call the remaining steps.",
"first_step_slug": "foglio-caratteristiche-immobile-roma"
}first_step_slug tells you which slug to start from. You can discover a bundle's steps with GET /api/v3/catalog-forms (see Discover the available forms).
Creating a step as a standalone practice: metadata.standalone
Some services belong to a bundle but also make sense on their own. To create one without a bundle, say so explicitly:
{
"form": { },
"metadata": {
"external_id": "order_8842",
"standalone": true
}
}With standalone: true the practice is created on its own: no container is looked up or created, and the check above does not apply. Omit the parameter for the normal bundle behaviour.
An external_id cannot be shared between a standalone practice and a bundle
If an external_id has already been used for a standalone practice, it cannot later act as the container for bundle steps. In that case the response is 409:
{
"error": "EXTERNAL_ID_NOT_A_BUNDLE",
"message": "The external_id \"order_8842\" is already used by a standalone practice and cannot be used to add bundle steps. Use a different external_id for the bundle."
}Use a different external_id for the bundle.
2. Look up your practices
$practices = Http::withToken(env('DOKICASA_TOKEN'))
->acceptJson()
->get('https://app.dokicasa.it/api/v3/user/me/services', [
'filter[external_id]' => 'order_8842',
])
->json();3. Practice status and tasks
$id = 84213; // practice id or public uuid
$practice = Http::withToken(env('DOKICASA_TOKEN'))->acceptJson()
->get("https://app.dokicasa.it/api/v3/practices/{$id}")->json();
// $practice['status'] → e.g. WAITING / DOING / ...
$tasks = Http::withToken(env('DOKICASA_TOKEN'))->acceptJson()
->get("https://app.dokicasa.it/api/v3/practices/{$id}/tasks")->json();
// $tasks['tasks'] → Activity (type=TASK) with status TO_DO / DOING / DONETIP
With the uuid instead of the numeric id, GET /practices/{practice} and .../tasks are public (no Bearer): handy for a read-only link. With the numeric id you need Bearer and ownership of the practice.
4. Reply to a task
While a practice is being processed the back office can assign you tasks (Activity with type = TASK, readable via GET /practices/{practice}/tasks): e.g. upload a document or provide some missing information.
To reply you send a comment (text and/or attachments) to the task. The call:
- always requires the user's Bearer
api_token; - only accepts tasks assigned to you (
assigned_to) and linked to a practice you own; otherwise403; - moves the task back to
DOINGand reassigns it to the back office, which gets notified.
WARNING
{task} is the id of the single task, i.e. the id field of an item in the tasks[] array returned by GET /practices/{practice}/tasks — not the practice id (practice_id / the container id). Passing the practice id returns 404 (or 403 if that id happens to match a task that isn't yours).
// GET /api/v3/practices/84213/tasks
{
"practice_id": 84213, // ❌ NOT this one
"tasks": [
{ "id": 99812, ... } // ✅ this one: {task} = 99812
]
}$taskId = 99812;
// Text only
$comment = Http::withToken(env('DOKICASA_TOKEN'))->acceptJson()
->post("https://app.dokicasa.it/api/v3/tasks/{$taskId}/comments", [
'comment' => 'I uploaded the requested document.',
])
->json();
// With attachments (multipart)
$comment = Http::withToken(env('DOKICASA_TOKEN'))->acceptJson()
->attach('attachments[]', file_get_contents('/path/id.pdf'), 'id.pdf')
->post("https://app.dokicasa.it/api/v3/tasks/{$taskId}/comments", [
'comment' => 'ID card attached.',
])
->json();const taskId = 99812
const form = new FormData()
form.append('comment', 'ID card attached.')
form.append('attachments[]', fileBlob, 'id.pdf') // optional
const comment = await fetch(
`https://app.dokicasa.it/api/v3/tasks/${taskId}/comments`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.DOKICASA_TOKEN}`,
Accept: 'application/json',
},
body: form, // no manual Content-Type: FormData sets it
}
).then((r) => r.json())curl -X POST \
https://app.dokicasa.it/api/v3/tasks/99812/comments \
-H "Authorization: Bearer $DOKICASA_TOKEN" \
-H "Accept: application/json" \
-F "comment=ID card attached." \
-F "attachments[]=@/path/id.pdf"NOTE
You must provide at least one of comment and attachments[]. The 201 response contains the created comment; the task moves to DOING awaiting the back office review.
5. Download the contract PDF
Practices of type contract (type: CONTRACT) expose the generated PDF, downloadable on-demand with the Bearer api_token:
GET /api/v3/contract/{id}/pdf{id}is the contract practice id: the sameidreturned byPOST /api/v3/form/{slug}when the slug is a contract, or — if the contract is a step of a bundle — theidof that step (user_service_id). So it works for both single contracts and contract steps of a bundle.- Requires the Bearer and ownership of the practice (practices created via the API belong to the partner). An
idthat isn't yours →401. - By default it responds with the PDF inline (
Content-Type: application/pdf); with?download_mode=1it forces an attachment download (Content-Disposition: attachment; filename="<contract-name>.pdf").
$id = 84213; // contract practice id (from the POST form, or step user_service_id)
$pdf = Http::withToken(env('DOKICASA_TOKEN'))
->get("https://app.dokicasa.it/api/v3/contract/{$id}/pdf", [
'download_mode' => 1, // force attachment; omit for the inline PDF
])
->body();
file_put_contents("contract_{$id}.pdf", $pdf);curl -L -X GET \
"https://app.dokicasa.it/api/v3/contract/84213/pdf?download_mode=1" \
-H "Authorization: Bearer $DOKICASA_TOKEN" \
-o contract_84213.pdfTIP
The contract PDF also reaches you as an attachment of the CLOSED webhook (attachments[].content_base64, see below). This GET is for when you want it on-demand — e.g. to download or regenerate it before closing.
NOTE
Optional query params: download_mode=1 (attachment), send_mail=1 (emails the PDF to the practice owner), preview.
Word (.docx) version
The same contract is also downloadable as the editable Word version, with the same Bearer api_token:
GET /api/v3/contract/{id}/doc{id}is the same id used for the PDF: the contract-practice id (fromPOST /api/v3/form/{slug}) or theuser_service_idof the contract step of a bundle.- Requires the Bearer and ownership of the practice.
- Extra gating: Word download must be enabled on your partner account. If it isn't, it responds
403with{ "error": "..." }— contact support to have it enabled. - On success it returns the
.docxfile; errors (missing permission, generation failure) are returned as JSON with the appropriate HTTP status (403,502).
$id = 84213; // same contract-practice id used for the PDF
$docx = Http::withToken(env('DOKICASA_TOKEN'))
->get("https://app.dokicasa.it/api/v3/contract/{$id}/doc")
->body();
file_put_contents("contract_{$id}.docx", $docx);curl -L -X GET \
"https://app.dokicasa.it/api/v3/contract/84213/doc" \
-H "Authorization: Bearer $DOKICASA_TOKEN" \
-o contract_84213.docx6. Webhooks (events to your backend)
Configure a webhook_url on your partner account: Dokicasa POSTs a JSON event at every relevant step of the practice and task lifecycle. The same webhook serves both API-created and SDK-created practices.
Configuring the webhook_url (self-service)
You can read and update your account's webhook_url directly via API, without going through support.
| Method | Path | Description |
|---|---|---|
GET | /api/v3/user/me | Logged-in account details (includes the current webhook_url) |
PATCH | /api/v3/user/me/webhook | Set / update the webhook_url |
curl https://app.dokicasa.it/api/v3/user/me \
-H "Authorization: Bearer $DOKICASA_TOKEN" \
-H "Accept: application/json"curl -X PATCH \
https://app.dokicasa.it/api/v3/user/me/webhook \
-H "Authorization: Bearer $DOKICASA_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"webhook_url":"https://your-server.com/webhooks/dokicasa"}'The body accepts webhook_url (a valid URL, required as a key). The response is { "webhook_url": "..." }. To disable webhooks pass null or an empty string:
curl -X PATCH https://app.dokicasa.it/api/v3/user/me/webhook \
-H "Authorization: Bearer $DOKICASA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"webhook_url":null}'IMPORTANT
The webhook_url is per-environment: staging (testing.dokicasa.it) and production (app.dokicasa.it) have separate databases, so the value you set applies only to the environment whose API you call. Configure your staging receiver by calling PATCH on testing.dokicasa.it and your production one on app.dokicasa.it: this way test events reach your staging endpoint and only real events reach production.
Events
event_type | When |
|---|---|
CREATED | Practice created |
UPDATED | Practice updated |
CLOSED | Practice completed (closed by the back office) |
NEW_TASK | The back office opens a task on the practice |
UPDATE_TASK | A task is updated or closed (task.status = "DONE") |
SERVICE_NOT_AVAILABLE | Service not available for that practice |
NOTE
There is no dedicated event for closing a task: closing a task is an UPDATE_TASK with task.status = "DONE".
Payload
All events share the same envelope. Example for CLOSED:
{
"event_type": "CLOSED",
"status": "CLOSED",
"external_id": "order_8842", // YOUR id (null if not set)
"name": "Legal consultancy",
"service_name": "Legal consultancy",
"user_service_id": 99076,
"type": "SERVICE", // SERVICE | CONTRACT
"attachments": [
{ "name": "document.pdf", "mime_type": "application/pdf",
"url": null, "content_base64": "JVBERi0xLjQK..." }
],
"notes": null,
"details": [],
"metadata": {},
// present only if the practice is a step of a bundle:
"bundle_id": 146025,
"bundle_name": "Canone Concordato Roma",
"step": "2"
}Attachments may be inline (content_base64) — e.g. the contract PDF — or a public url: check which of the two is set.
Task events (NEW_TASK / UPDATE_TASK) add the task and notify fields:
{
"event_type": "UPDATE_TASK",
"status": "UPDATE_TASK",
"external_id": "order_8842",
"user_service_id": 99078,
"type": "SERVICE",
"task": {
"id": 41852,
"type": "TASK",
"status": "DONE", // DONE = task closed
"description": "Upload the ID card",
"assigned_to": 1
},
"notify": null // set on NEW_TASK, null on updates
}Delivery and idempotency
- Asynchronous delivery (with retries),
POST, JSON body. - The outgoing webhook is not signed: protect your receiving endpoint with an IP allowlist of Dokicasa IPs and/or a secret URL path.
- The payload has no dedicated delivery id: deduplicate on
user_service_id+event_type(andtask.idfor task events) and always reply2xx, even to duplicates.
7. Staging environment (sandbox)
To integrate and test without touching production there is a staging environment with an isolated database:
- Base URL:
https://testing.dokicasa.it - Same API, same Bearer
api_token(issued on staging).
To exercise the webhooks on your own, staging exposes endpoints that simulate the actions the back office normally performs. They are available only on staging (in production they return 403), require the Bearer, and that the practice/task is owned by you.
| Method | Path | Webhook fired |
|---|---|---|
POST | /api/v3/sandbox/practices/{userService}/simulate-close | CLOSED (with a sample PDF attached) |
POST | /api/v3/sandbox/practices/{userService}/simulate-task | NEW_TASK (opens a task assigned to the practice user) |
POST | /api/v3/sandbox/tasks/{task}/simulate-update | UPDATE_TASK (updates status/assignee/description) |
POST | /api/v3/sandbox/tasks/{task}/simulate-close | UPDATE_TASK with task.status = "DONE" |
{userService} is the practice (user service) id; {task} is the single task id (tasks[].id from GET /practices/{practice}/tasks).
curl -X POST \
https://testing.dokicasa.it/api/v3/sandbox/practices/99076/simulate-close \
-H "Authorization: Bearer $DOKICASA_TOKEN" \
-H "Accept: application/json"curl -X POST \
https://testing.dokicasa.it/api/v3/sandbox/practices/99078/simulate-task \
-H "Authorization: Bearer $DOKICASA_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"description":"Upload the ID card"}'curl -X POST \
https://testing.dokicasa.it/api/v3/sandbox/tasks/41852/simulate-close \
-H "Authorization: Bearer $DOKICASA_TOKEN" \
-H "Accept: application/json"Optional JSON body params: simulate-task accepts description and status (default TO_DO); simulate-update accepts status, assigned_to, description and user_service_status. The response includes webhook_sent (false if you have no webhook_url configured).
8. Duplicate a practice (tenant change)
When the tenant changes on a property that already has a completed practice, instead of re-running the Calculation by hand you can duplicate the practice: all duplicable steps (property and calculation) are cloned into a new practice with a new external_id, ready to register the new tenant.
POST /api/v3/practices/{practice}/duplicate-partner
Authorization: Bearer <token>{practice} is the bundle practice id you get from GET /api/v3/user/{id}/services (field practice_id).
All duplicable steps of the practice (property and calculation) are always cloned: you do not pass the list of steps.
Body (all optional):
| Field | Type | Default | Description |
|---|---|---|---|
external_id | string | — | New external_id of the duplicated practice (for later PUTs) |
bundle_name | string | "<name> (copia)" | Name of the new practice |
Billing: for each cloned step the same logic as the submit (§1) applies: if the step is free for your account nothing is charged, otherwise a compatible credit is consumed first and, if none, the castelletto (wallet) is charged. If a paid step has neither a credit nor enough wallet → 401 with the failing step, and nothing is created (atomic: all or nothing).
curl -X POST "https://app.dokicasa.it/api/v3/practices/45210/duplicate-partner" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"external_id":"pratica-2026-00987"}'201 response:
{
"practice_id": 45999,
"external_id": "pratica-2026-00987",
"steps": [
{ "step": 1, "bundle_user_service_id": 88123, "name": "Creazione Documenti Canone Concordato del 15/07/2026" },
{ "step": 2, "bundle_user_service_id": 88124, "name": "Certificazione Contratto Locazione del 15/07/2026" }
]
}With the returned bundle_user_service_id you then update the new tenant's data via PUT /api/v3/form/{bundle_user_service_id} (see §1).
Errors:
403practice not yours ·422practice not duplicable or no duplicable step ·401insufficient castelletto (with thestep) ·403API plan not enabled for a service.
OpenAPI reference
The full spec (parameters, schemas, responses) is below, generated from the same spec also published at api.dokicasa.it/api/documentation.