Skip to main content
Drafting is four things: generating a document from a category and a set of instructions, reading it back as structure, replacing its body, and rendering it to a file. Everything lives under a matter, and everything that costs an LLM minutes answers 202 with an operation. Reads need drafting:read. Writing, generating and revising all need drafting:run, and exporting needs exports:create.

Generating a draft

Exactly three fields are required: category, title and governingLawState. Everything else is optional, and unknown fields are refused rather than ignored, so a typo is a 422 and not a silently dropped value. There is no jurisdiction field. Vaquill drafts US documents only, so a field whose one accepted value is also its default would be something to discover before you could ignore it. governingLawState is required for the opposite reason: a US draft with no pinned governing law cites nothing. governingLawState accepts three forms: a two-letter code such as ca, a full state name such as California, or the sentinel federal. It is normalized on the way in, and an unrecognized value is refused.
category is a slug rather than an open string, and it is not published as an enum in the schema: the underlying vocabulary still carries categories from an earlier, non-US product this API does not sell, and publishing the enum would publish those as accepted values. Read GET /v1/draft-categories instead, which lists every slug we advertise with the product’s own name for each. An unknown slug comes back as 422 invalid-request naming body.category, before anything is queued.
variables is free-form on purpose: which keys mean anything is a property of the category, and a closed set would need a release to add one. Anything the generator leaves unfilled is counted for you at export time as unfilledPlaceholders.

What comes back

A 202 with an operation of type draft.generate, plus Location and Retry-After headers. Poll GET /v1/operations/{operationId} until it is terminal. On succeeded, resource names the draft:
The draft row exists from the moment the launch is accepted, but it is an empty placeholder until the pipeline fills it. Read the operation, not the draft, to know whether it is finished.
This is the launch where Idempotency-Key matters most. The generation pipeline is not idempotent and its task is configured with no automatic retries, so a client timeout without a key is a coin flip between losing the work and paying for it twice. See Idempotency.

Reading drafts

GET /v1/matters/{matterId}/drafts lists every draft in the matter, newest edit first, without bodies. It pages with limit (default 50, maximum 200) and offset, and returns data alongside a pagination object carrying total and hasMore. GET /v1/matters/{matterId}/drafts/{draftId} returns the same record plus the body. Four fields on that record are worth understanding before you build on them. The two status fields are not synonyms, and confusing them is the easiest mistake here. A finished contract someone has marked final still reads generationStatus: "succeeded", and a draft the pipeline never touched also reads succeeded, which is the honest answer for a document that was uploaded rather than generated.

Where the body lives

The body comes back as sections, an ordered array of heading, level and text:
This is not plain text, and it is not the editor’s document. Vaquill stores a draft as the internal rich-text model its editor uses, and publishing that would make a third-party library’s schema part of an API contract we cannot change without your release cycle. So structured content goes out, markdown comes in, and bytes are available on request. The one representation you never see, in either direction, is the stored editor document. The practical consequence is that sections is the shape your backend acts on: find the indemnity clause, check whether a limitation of liability exists, diff one heading across two revisions. If you want a flat document, join the sections yourself. If you want something a human reads, use the export.
The first entry often has heading: null. That is not a bug. Body text preceding the first heading is where a preamble and a parties block live, and dropping it because it carries no heading would lose the first paragraph of most contracts.

Editing a draft

PUT /v1/matters/{matterId}/drafts/{draftId} replaces the body. Only contentMarkdown is required.
It answers 200 with the updated draft, at its new version number. The body you overwrote is kept as a version.
This is a PUT, not a PATCH. contentMarkdown is the whole document, so sending one paragraph replaces the contract with that paragraph. Markdown cannot express a partial body, and pretending otherwise would invite exactly that mistake. Whitespace alone is refused rather than accepted as an empty document.

Optimistic concurrency

expectedVersion is how a headless read-modify-write avoids a lost update. Read the draft, take its version, send that number back with your edit. If the draft has moved since you read it, the write is refused with 409 draft-version-conflict and nothing is changed:
Both numbers are on the problem document, so a retry can be automatic: re-read the draft, reapply your change to the current text, send it again with the new version. Omitting expectedVersion means “overwrite whatever is there”. That is a legitimate thing to want, which is why it is allowed, and a bad thing to get by accident, which is why the field exists. A headless integration has no screen on which to notice that someone edited the draft in the browser between your two calls, so without the guard a lost update is invisible on both sides.
The version predicate is applied to the write itself as well as being checked first, so two callers that pass the check in the same instant still cannot both land. The loser gets the same 409.
A body that renders to more than 5 MB of stored document is refused with 413 file-too-large, carrying limit and sizeBytes.

Version history

GET /v1/matters/{matterId}/drafts/{draftId}/versions lists the draft’s history, newest first, paged the same way as the drafts list. Each entry carries three fields: version, changeSummary (whatever was supplied on the replace that created it, or null) and createdAt. Old bodies are deliberately not published. A version body would be a second copy of the same representation problem, and no customer has asked to read one. Treat this list as an audit trail, not as a way to restore text. If you need the ability to roll back, keep your own copy of the markdown you sent.

Revising a draft

POST /v1/matters/{matterId}/drafts/{draftId}/improvements runs the draft back through the pipeline with new instructions. It answers 202 with an operation and accepts Idempotency-Key.
Every field is optional. instructions is free text up to 40,000 characters, category overrides the source draft’s category, and tone is the same three-value enum, defaulting to balanced. Governing law and practice area are carried over from the source. If the source draft is in a document type we no longer write, the revision falls back to custom rather than refusing. That case is real: categories get retired, and a draft this API published a month ago should still be revisable today. The result is a new draft. The source is never overwritten. The operation is again of type draft.generate and its resource.id is a different drf_ id from the one in the path. That is deliberate: the run takes minutes and costs money, and a customer who dislikes the result still holds the document they started from. Your integration has to decide which of the two is now current. A revision is refused with 409 draft-not-revisable in two states: the source draft is still being generated, or it has essentially no body to revise (under 50 characters of text). Neither is a malformed request, which is why it is a 409 rather than a 422. The same call succeeds once the generation finishes or once the draft has content.

Exporting

POST /v1/matters/{matterId}/drafts/{draftId}/exports renders the draft and returns the file.
format is required and is one of docx or pdf. It answers 200, not 201, and the response is the file itself:
content is base64. Decode it and write it to disk; there is no URL to fetch. The filename is derived from the draft’s own title, lowercased and hyphenated, so two exports of the same draft produce the same name and overwriting is an update rather than a second copy.
Every other artifact on this API is a signed URL, and a draft export is the exception. A comparison export is signed over a file a worker already wrote, whereas a draft is rendered per call, so vending a URL would mean writing your client’s work product to storage first. With encryption on, that is a choice between a URL you cannot decrypt and plaintext sitting in a bucket. A bounded response body is better than both.
The ceiling is 10 MB. A larger render is refused with 413 export-too-large carrying limit and sizeBytes, and the file is discarded rather than truncated. unfilledPlaceholders counts the [Party Name]-style spans that survived into the finished file. The web app warns a human about these before they send a contract; an integration that files an export unread needs the same signal, so it rides along as a number rather than as a header nobody parses. Treat anything above zero as a document not ready to leave the building. Exporting requires exports:create, which is separate from drafting:read. Reading a draft’s structure and taking the finished contract out of the workspace are different risks, the same way documents:download is separate from documents:read.

Templates

A template is a document your organization uploaded with its fillable spans marked. Running one is not the same thing as generating a draft: The point of a template run is that your clause language is bit-identical between the template and the output. Only the marked variables are replaced.

Listing templates

GET /v1/templates returns every active template the organization owns, paged with limit and offset. Each entry carries id (a tpl_ identifier), title, category, an optional description and sourceFilename, and timestamps.
Vaquill’s own starter templates are not listed. They belong to us rather than to you, and a template you did not author is not one your automation should run as if you had. Everything in this list is your organization’s own.
The list is a picker: id, title, category, description, sourceFilename and timestamps, sorted by title so two reads agree. For the body and the variable ids, fetch one: GET /v1/templates/{templateId} returns both. See Authoring templates for creating and editing them.

Running one

Both fields are optional. prompt is free text up to 8,000 characters that the extractor reads values out of, and slotOverrides is a map keyed by each variable’s varId with values up to 4,000 characters. Those ids come from GET /v1/templates/{templateId}. A template whose every variable you supply needs no prose at all, and values you supply are never overwritten by the extractor. The response is a 202 with an operation of type template.run. Its resource is the run, with kind templateRun and a dtr_ identifier, not the draft.

Reading the run

GET /v1/matters/{matterId}/template-runs/{runId} is where the answer lives:
draftId is null until the run has finished rendering. status uses the same five public values as an operation, so the internal planning and extracting stages both read as running. Read slots[].source before you trust a value. A source of user was handed over by you. Anything else, prompt, context, computed or default, was inferred by a model and is worth checking before the document goes out.

Errors worth handling

Two of these are worth a second look. draft-content-unprocessable is not invalid-request. The field is a string and it passed the schema; what failed is our conversion of it into a draft body. The slug says which half broke so you are not hunting for a malformed field that is not there. Send headings, paragraphs and lists. Tables and fenced code blocks are not part of a draft. draft-not-revisable is a 409, not a 422. Nothing about your request is wrong, and changing the payload is the one thing that will not help. Poll the source draft’s generation operation, or give it a body, and send exactly the same request again.

What is on the other pages

Copying a draft, importing one from a document, filing one into its matter as a searchable document, and the 30-day trash behind DELETE are all on The draft lifecycle. Creating and editing the templates a run uses is on Authoring templates. A 404 on a draft is deliberately uninformative for the same reason every not-found on this API is: if a missing draft and someone else’s draft returned different statuses, the status code would become a way to probe another organization. See Errors.
Last modified on August 24, 2026