202 with an operation.
Reads need drafting:read. Writing, generating and revising all need drafting:run, and exporting needs exports:create.
Generating a draft
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
A202 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:
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 assections, an ordered array of heading, level and text:
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.
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:
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.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.
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.
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.
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
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 behindDELETE 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.
