{"openapi":"3.1.0","info":{"title":"Signing Flows — Integration API","version":"1.0.0","description":"To run the whole life of a document from your own systems: upload it, build the flow, dispatch it, follow it and archive it.\n\n**Send `Idempotency-Key` on anything that cannot be undone** — dispatching above all. If you retry with the same key and the same body you get the same response back, without the effect happening twice; the same key with a different body gives you a 409. On the other side of an integration there is almost always a queue that retries on its own, and without a key that sends the document twice.\n\nTo find out about changes without asking, add a webhook under Integrations: you get a signed POST every time something happens. The `circuito.completo` one carries the instance id, which is what you ask the delivery package for."},"servers":[{"url":"/api/v1"}],"tags":[{"name":"Documents","description":"Get a PDF in, and get the results out: the signed document, its certificate of completion, and the delivery package that archives the whole thing somewhere else."},{"name":"Workflows","description":"The life of a flow: build it while it is a draft, add who signs, dispatch it, and act on it afterwards — cancel it, or hold it so retention does not delete it."},{"name":"Evidence","description":"What happened and when, with the verdict on the hash chain. This is the family that answers a lawyer, and its vocabulary is the same one the webhooks speak."},{"name":"Account","description":"Who this token belongs to and what it is allowed to do."},{"name":"Service","description":"The two doors that answer without a credential: what this is, and this description. Point a health check at the first one — asking for a token to explain how tokens work is a circle."}],"components":{"securitySchemes":{"tokenDeApi":{"type":"http","scheme":"bearer","description":"The token you created under Integrations, prefixed `sf_`. It acts on behalf of whoever created it: it cannot do anything that person could not do."}},"schemas":{"Circuito":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string","description":"The name given to it by whoever sent it."},"status":{"type":"string","enum":["draft","sent","completed","cancelled","expired"],"description":"Where it stands. `sent` = waiting for signatures. `completed` = everyone signed, and that is the moment the certificate of completion exists. `expired` = the deadline passed without everyone signing; what was already signed stays signed."},"mode":{"type":"string","enum":["sequential","parallel","copies"],"description":"`sequential` = they sign in turns. `parallel` = everyone at once on the same document. `copies` = the same document to N recipients, each one with THEIR own document and THEIR own evidence record."},"signature_level":{"type":"string","enum":["simple","advanced","qualified"]},"country":{"type":"string","description":"The legal framework it is signed under (ISO 3166-1 alpha-2)."},"language":{"type":"string"},"dispatched_at":{"type":["string","null"],"format":"date-time"},"expires_at":{"type":["string","null"],"format":"date-time"},"documents":{"type":"integer","description":"How many documents it has. In `copies` mode it is one per recipient."}}}}},"security":[{"tokenDeApi":[]}],"paths":{"/":{"get":{"tags":["Service"],"summary":"What this is and what version it speaks","security":[],"responses":{"200":{"description":"The version, and where this description lives."}}}},"/me":{"get":{"tags":["Account"],"summary":"Which account this token belongs to, and with what scopes","description":"The first thing worth trying when somebody hands you a token.","responses":{"200":{"description":"The account and the token."},"401":{"description":"Something did not work. The body carries `message` with the explanation.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}}}}},"/workflows":{"get":{"tags":["Workflows"],"summary":"The flows in this account","description":"Cursor paginated: the response carries `next_before`, and you send it back as `desde` to get the next batch. Absent or null means there are no more.\n\nBy cursor and not by page number because new flows arrive between two requests, and with numbered pages that shifts the rows down and skips some of them past you.","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["draft","sent","completed","cancelled","expired"]}},{"name":"before","in":"query","schema":{"type":"string","format":"date-time"},"description":"The `next_before` from the previous batch."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}}],"responses":{"200":{"description":"The batch and the cursor.","content":{"application/json":{"schema":{"type":"object","properties":{"circuitos":{"type":"array","items":{"$ref":"#/components/schemas/Circuito"}},"next_before":{"type":["string","null"],"format":"date-time"}}}}}},"401":{"description":"Something did not work. The body carries `message` with the explanation.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"403":{"description":"Something did not work. The body carries `message` with the explanation.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}}}}},"/workflows/{id}":{"get":{"tags":["Workflows"],"summary":"One flow, with its signers and where each of them stands","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The flow, its participants and their states."},"404":{"description":"Something did not work. The body carries `message` with the explanation.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}}}},"patch":{"tags":["Workflows"],"summary":"Configure the flow before dispatching it","description":"Draft only. Fields: `title`, `mode`, `signature_level`, `validity_days`, `decline_policy`. What you do not send is not touched.\n\nIt does not need an `Idempotency-Key`: it is an update with the same values, so repeating it produces nothing different.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The flow, configured."},"409":{"description":"Something did not work. The body carries `message` with the explanation.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}}}}},"/documents/{id}/file":{"get":{"tags":["Documents"],"summary":"The signed PDF","description":"By INSTANCE and not by flow: in `copias` mode there is one document per recipient, each with its own signatures. Instance ids come from `/circuitos/{id}`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The PDF.","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"Something did not work. The body carries `message` with the explanation.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}}}}},"/documents/{id}/certificate":{"get":{"tags":["Documents"],"summary":"The certificate of completion","description":"The deliverable: one page with who signed, when, with what, and the root hash of the evidence record. It exists only once the flow is `completo`, and it is issued once — the one you download today and the one you download in a year are the same file.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The certificate PDF.","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"Something did not work. The body carries `message` with the explanation.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}}}}},"/documents/{id}/evidence":{"get":{"tags":["Evidence"],"summary":"The full evidence record, with the verdict on the chain","description":"Every fact around that document, chained by hash. `cadena` says whether the chain closes: if it ever said it does not, the record stopped being proof and somebody has to go and look at it.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The events and the verdict."},"404":{"description":"Something did not work. The body carries `message` with the explanation.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}}}}},"/documents/{id}/package":{"get":{"tags":["Documents"],"summary":"The delivery package: everything needed to archive it in another system","description":"One single call with the flow metadata, the evidence record with its chain verdict, and a MANIFEST of the files — URL, type, size and fingerprint — instead of the bytes: a PDF as base64 inside a JSON triples the size and cannot be streamed. Meant for packaging at the destination (E-ARK/METS/PREMIS, OAIS), which is where it belongs: a SIP is built against each archive own submission agreement.\n\n**It is a contract.** Stable: `esquema`, the field names, the hashes and — above all — `evidencia.eventos[].codigo`, which is what you map to a PREMIS eventType: that vocabulary is ADDED to, never renamed and never removed. Not stable: `descripcion` (human text, changes with the language) and `datos` (free jsonb, different per event type).\n\nIt requires both the document scope AND the evidence scope: combining them into one response does not widen what a token can see.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The package."},"403":{"description":"Something did not work. The body carries `message` with the explanation.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"404":{"description":"Something did not work. The body carries `message` with the explanation.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}}}}},"/documents":{"post":{"tags":["Documents"],"summary":"Upload a PDF and keep the flow in draft","description":"As `multipart/form-data`, not as JSON with base64: base64 inflates by 33% and forces the whole file into memory twice.\n\nForm fields: the file, `folder_id` (optional) and `title` (optional). **`folder_id` is optional here and required in the console**, and the difference is real: a person is standing in a folder when they upload, a program has nowhere to be standing. Without it the document goes to the account drafts folder.\n\nReturns the workflow id, the document id, the `sha256` and whether it was a duplicate. **It does not need an `Idempotency-Key`**: it deduplicates by content within the account, which works even when the client sends no key at all.","responses":{"200":{"description":"The document and the draft flow."},"400":{"description":"Something did not work. The body carries `message` with the explanation.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"409":{"description":"Something did not work. The body carries `message` with the explanation.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}}}}},"/documents/from-url":{"post":{"tags":["Documents"],"summary":"Bring the document from where it already lives","description":"Instead of uploading bytes, hand over a URL and the platform fetches it: a SharePoint or OneDrive item, a Google Drive file, a document manager of your own — anything that can serve a PDF over HTTPS. You get back the same thing an upload gives you: a workflow in draft.\n\nFields: `url` (required), `title`, `folder_id`, and `authorization` — whatever the source needs to hand the file over, for example a Microsoft Graph bearer token.\n\n**The credential is borrowed, not kept.** It is used for this one download and never stored, logged or returned.\n\nRedirects are followed up to three times and **every hop is checked**, so the URL cannot be used to reach anything private. The file is capped at 30 MB while it is being read, not by trusting the `content-length` the other server sends.\n\nSend `Idempotency-Key`: without it, a retry from your queue creates the workflow twice.","responses":{"200":{"description":"The workflow, in draft, with the document in it."},"400":{"description":"Something did not work. The body carries `message` with the explanation.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"413":{"description":"Something did not work. The body carries `message` with the explanation.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"502":{"description":"Something did not work. The body carries `message` with the explanation.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}}}}},"/workflows/{id}/signers":{"post":{"tags":["Workflows"],"summary":"Add somebody to the flow","description":"Draft only. Fields: `email` (required), `name`, `role` (`signer`/`reviewer`/`cc`), `order`, `minimum_assurance`, `capacity`.\n\n**Send an `Idempotency-Key`.** Adding twice looks harmless and is not: the same address repeated is a duplicated signer in the order, and the flow asks the same person to sign twice.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The participation that was created."},"409":{"description":"Something did not work. The body carries `message` with the explanation.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}}}}},"/workflows/{id}/signers/{signerId}/link":{"get":{"tags":["Workflows"],"summary":"The signing link for one person — embedded signing","description":"Your application already has the person authenticated and on screen. Instead of emailing them so they can leave, open their inbox and come back, open the signing page right there, in an iframe or a new tab. The drop-off between *the email went out* and *they opened it* is one of the most expensive things a signing process has.\n\n**The link is a credential**: whoever holds it can sign as that person. It is never cached, it needs the dispatch scope rather than a read scope, and it is refused if the workflow has not been dispatched, or if that person has already signed or declined.\n\nThe `signerId` is the id you got back when you added them.","responses":{"200":{"description":"The URL, and who it belongs to."},"404":{"description":"Something did not work. The body carries `message` with the explanation.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"409":{"description":"Something did not work. The body carries `message` with the explanation.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}}}}},"/workflows/{id}/dispatch":{"post":{"tags":["Workflows"],"summary":"Dispatch: the emails go out and the clock starts","description":"**The least undoable thing in the whole API, and where the `Idempotency-Key` matters most.** A retry without a key sends the document twice to every signer and bills it twice — and on the other side there is almost always a queue retrying on its own.\n\nWith the same key and the same body you get the same response back, without the effect happening twice. With the same key and a **different** body you get a 409: that is not a retry.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Dispatched."},"409":{"description":"It is not a draft, something is missing, or the idempotency key was reused with a different body.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}}}}},"/workflows/{id}/hold":{"post":{"tags":["Workflows"],"summary":"Hold a document so it is not deleted, or release the hold","description":"While it is held, the retention sweep does NOT delete the bytes, whatever deadline passes. This is the *legal hold*: you set it when the document is in litigation or under a production order.\n\nFields: `hold` (boolean, required) and `reason` (required when holding, because a hold with no reason never gets released: nobody lifts something when they do not know why it was put there). Both movements go into the evidence record.\n\nIt is in the API and not only in the console because a hold usually arrives from OUTSIDE — a legal system, a case manager — and asking those people to go into a screen by hand guarantees it arrives late. Requires the `circuito.administrar` scope: **releasing a hold enables deletion**.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The state of the hold."},"400":{"description":"Something did not work. The body carries `message` with the explanation.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"404":{"description":"Something did not work. The body carries `message` with the explanation.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}}}}},"/workflows/{id}/cancel":{"post":{"tags":["Workflows"],"summary":"Cancel a flow that was already dispatched","description":"`reason` is **required**: it goes into the evidence record, and a flow cancelled with no reason is a hole in the proof. What was already signed stays signed.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Cancelled."},"400":{"description":"Something did not work. The body carries `message` with the explanation.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"409":{"description":"Something did not work. The body carries `message` with the explanation.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}}}}},"/event-types":{"get":{"tags":["Evidence"],"summary":"The event vocabulary of the evidence record","description":"The codes that can show up in `evidencia.eventos[].codigo`, with their category and their weight. It is there so you can build your mapping BEFORE the first document, instead of discovering the rare ones — a rejection, an extension — months later and in production.\n\nThe ones marked `contractual` are never renamed and never removed, and their `categoria` does not change: a database trigger enforces that, not a convention. **New codes DO get added**, so a consumer has to IGNORE the ones it does not know instead of failing — if it fails, every new fact we record breaks its integration.","responses":{"200":{"description":"The vocabulary."}}}}}}