Skip to contentSaltar al contenidoAle nan kontni anПерейти к содержимомуדלג לתוכן
EstateDraftFL

Developers · Reference

The firm API

Base address https://estatedraftfl.com/api/v1. JSON in, JSON out; every answer is uncached. The machine-readable contract is the OpenAPI 3.1 document.

Reviewed 2026-10-01 · contract version 2026-10-01

Authentication

Send the firm’s key as a bearer token. A missing, unknown, revoked or expired key gets the same 401; a key without the operation’s scope gets a 403 that names the scope. Keys are minted by the firm’s admin (Firm settings → API access) and shown once.

curl https://estatedraftfl.com/api/v1/matters?limit=50 \
  -H "Authorization: Bearer $EDFL_FIRM_KEY"

Scopes

A client-safe key reads only what a client may see; it can never hold the agenda, the deadlines, a proposal or the webhooks.

  • matters:readattorney or client-safe

    Each matter's id, status, stage and dates — never a name, a document or a person's words.

  • deadlines:readattorney keys only

    The dates the firm derived and confirmed, with the rule and citation each rests on.

  • receipts:readattorney or client-safe

    The tamper-evident receipts of filed and delivered work: type, hash and chain link.

  • documents:metaattorney or client-safe

    On a matter: each document's id, SHA-256 and scan status — never its name or its contents.

  • agenda:readattorney keys only

    What waits on the firm, by kind and register — ids and dates, never the names.

  • capabilities:readattorney or client-safe

    The platform's published catalogue: what each capability does, its limits and refusals.

  • record:proposeattorney keys only

    File a proposal on a matter for the firm to decide. Nothing is executed. Never carried by a rotation.

  • webhooks:manageattorney keys only

    Register, test and disable the firm's signed webhook endpoints. Whole-firm keys only.

Operations

  • GET/api/v1/mattersmatters:read · read lane

    The firm's matters

    Newest first; ids, states and dates only. A matter with a wall is never listed or counted. Default page 200 (the v1 contract), maximum 200.

    Query: limit, cursor

    Answers 200 · refuses 400, 401, 403, 429, 503

  • GET/api/v1/matters/{id}matters:read · read lane

    One matter

    The same 404 for an unknown id and one the key may not read. Richer for a key whose allowlist names the matter; documents only with documents:meta.

    Answers 200 · refuses 401, 403, 404, 429, 503

  • POST/api/v1/matters/{id}/proposalsrecord:propose · write lane · Idempotency-Key required

    Propose an action for the firm to decide

    Files a proposal; nothing is executed (executed: false). Fields beyond title, detail and requested are stripped and named on the firm's card. Three undecided per matter.

    Answers 201 · refuses 400, 401, 403, 404, 409, 429, 503

  • GET/api/v1/deadlinesdeadlines:read · read lane

    The firm's derived deadlines

    Each with its rule key, its citation, its status and whether it was confirmed.

    Query: limit, cursor

    Answers 200 · refuses 400, 401, 403, 429, 503

  • GET/api/v1/receiptsreceipts:read · read lane

    The firm's receipts

    Each receipt's type, content hash and the previous receipt's hash — verifiable at https://estatedraftfl.com/verify.

    Query: limit, cursor

    Answers 200 · refuses 400, 401, 403, 429, 503

  • GET/api/v1/agendaagenda:read · read lane

    What waits on the firm

    The firm's agenda by kind and register — never a name. A lane that could not be read lowers the status to unknown and is named in the notes.

    Answers 200 · refuses 401, 403, 429, 503

  • GET/api/v1/capabilitiescapabilities:read · read lane

    The platform's capabilities

    The published capability catalogue.

    Query: limit, cursor

    Answers 200 · refuses 400, 401, 403, 429, 503

  • GET/api/v1/webhookswebhooks:manage · read lane

    The firm's webhook endpoints

    Each endpoint with its ten latest delivery attempts (status code, duration, the class of a failure — never a response body).

    Answers 200 · refuses 401, 403, 429, 503

  • POST/api/v1/webhookswebhooks:manage · write lane · Idempotency-Key required

    Register a webhook endpoint

    An https address on the public internet and its events. The signing secret is in this answer once and never again (a replay says so).

    Answers 201 · refuses 400, 401, 403, 409, 429, 503

  • DELETE/api/v1/webhooks/{id}webhooks:manage · write lane · Idempotency-Key required

    Disable a webhook endpoint

    With the reason ({ "reason": "…" }). Its secret is destroyed; the endpoint is kept as a record.

    Answers 200 · refuses 400, 401, 403, 404, 409, 429, 503

  • POST/api/v1/webhooks/{id}/testwebhooks:manage · write lane · Idempotency-Key required

    Send a test event

    Fictional data (data.test: true), signed like any other and delivered at once: the answer says whether the receiver took it.

    Answers 200 · refuses 401, 403, 404, 409, 429, 503

  • POST/api/v1/webhooks/{id}/rotatewebhooks:manage · write lane · Idempotency-Key required

    Rotate an endpoint's signing secret

    The old secret is destroyed at once; the new one is in this answer once.

    Answers 200 · refuses 401, 403, 404, 409, 429, 503

  • GET/api/v1/openapi.jsonno key needed

    This document

    The OpenAPI 3.1 description of the firm API v1. No key needed.

    Answers 200

The envelope

Every list answers in the same shape; one item carries the same fields around its data.

schema_version
The contract's version (2026-10-01).
generated_at · as_of
When the answer was composed, and as of when its data stands.
data
The rows — an empty list when there are none; null when they could not be read.
page
limit, has_more (true only when a further row exists) and next_cursor (a position only).
verification
status (unknown < ungrounded < grounded < verified — the weakest of the answer's parts), method, counts, notes.
provenance_ids · provenance_count
The ids the answer rests on, and how many.

Pagination

Ask for limit 1–200 (anything else is refused with 400, never clamped). When page.has_more is true, send page.next_cursor back unchanged as cursor. The cursor holds a position only: each page is authorized again, so a matter walled or removed from the key’s allowlist between pages is simply absent from the next one. Exactly 200 matters is one full page with nothing more.

Rate lanes

Each key has four lanes, counted in one-minute windows aligned to the clock and shared by every server — the limit is the same wherever a request lands (X-RateLimit-Enforcement: global). Every answer carries RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset (seconds until the window turns over) and RateLimit-Policy; a spent lane answers 429 with Retry-After.

  • 120

    REST reads a minute

  • 30

    REST writes a minute

  • 120

    MCP reads a minute

  • 30

    MCP writes a minute

Errors

Each refusal is { "ok": false, "error": "…", "message": "…" }, with the field or the scope named where there is one.

400
Refused: a parameter, the body or the Idempotency-Key could not be read (the field is named).
401
The key is missing, unknown, revoked or expired — the same answer for each.
403
The key does not hold the scope (named), or its sensitivity may never hold it.
404
Not found — the same answer for an unknown id and one the key may not read.
409
A conflict: an Idempotency-Key reused with a different request, a write still running (retry), or the record's state refuses it.
429
The key's rate lane is spent — Retry-After and RateLimit-Reset say when it refills.
503
The read or write could not be completed — retryable; nothing is known about the result (never an empty list).

Writes happen once

Every POST and DELETE carries an Idempotency-Key (1–255 visible characters, kept 24 hours). The same key with the same request returns the first answer again, marked Idempotent-Replayed: true; the same key with a different request is refused (409 idempotency_key_reused); a request still running answers 409 with Retry-After. A write that failed keeps no claim on its key. A signing secret is never kept for a replay — the replay says it was shown once.

Proposals — the one write about a matter

A key holding record:propose files a proposal for the firm’s attorneys to decide: a title, an optional description and a requested object of data. Nothing is executed (executed: false). Anything else sent — a status, a clearance, an approval, an override — is stripped and named on the firm’s card. Three undecided proposals per matter at most; a rotation never carries the scope.

curl -X POST https://estatedraftfl.com/api/v1/matters/$MATTER_ID/proposals \
  -H "Authorization: Bearer $EDFL_FIRM_KEY" \
  -H "Idempotency-Key: 2026-10-02-inventory-1" \
  -H "Content-Type: application/json" \
  -d '{"title":"Confirm the inventory date","requested":{"due_on":"2026-10-20"}}'

Signed webhooks

Register an https address on the public internet (Firm settings → API access, or POST /api/v1/webhooks with webhooks:manage) and choose its events. Each event carries ids and states only — never a name, a document or a person’s words — and nothing about a matter with a wall, at the moment it happens or the moment it is delivered. A firm keeps ten live endpoints.

  • matter.status_changed

    A matter's status or stage changed

    data: matter_id, status, lifecycle

  • receipt.minted

    A receipt was minted

    data: receipt_id, matter_id, artifact_type, content_hash

  • deadline.confirmed

    A deadline was confirmed

    data: derivation_id, matter_id, task_key, rule_key, due_on, status

  • agenda.item_opened

    An item awaiting the firm's decision opened

    data: request_id, matter_id, action_type, severity

  • transfer.requested

    A client's transfer was addressed to the firm

    data: transfer_id, state, preview_only

  • consult.received

    A consult request arrived

    data: consult_id, lane

  • approved_delivery.completed

    An approved delivery reached its client

    data: delivery_id, matter_id, artifact_kind, recipient_kind

  • source.changed

    A source the firm's work rests on changed

    data: change_set_id, matter_id, trigger_kind, scope

  • request.status_changed

    A client request opened or closed

    data: request_id, matter_id, status, closed_as

A delivery

POST /your/endpoint HTTP/1.1
Content-Type: application/json
EstateDraftFL-Signature: t=1759363200,v1=5f0c…
EstateDraftFL-Event-Id: 6a1e7c52-…
EstateDraftFL-Event-Type: deadline.confirmed

{"id":"6a1e7c52-…","type":"deadline.confirmed","created_at":"2026-10-02T00:00:00.000Z","data":{"derivation_id":"…","matter_id":"…","task_key":"…","rule_key":"…","due_on":"2026-11-02","status":"calendared"}}
  • EstateDraftFL-Signature is t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of <t>.<the raw body> under the endpoint’s whole signing secret. Verify it before trusting the body, and refuse a timestamp more than 5 minutes from your clock.
  • Delivery is at least once: de-duplicate by EstateDraftFL-Event-Id; order by the timestamp.
  • Answer 2xx within ten seconds. Anything else is retried with a growing delay, up to ten attempts; redirects are never followed.
  • Answer 410 Gone and the endpoint is disabled. Revoking the key that registered an endpoint disables it too.
  • A test event (webhook.test, data.test: true) is signed like any other and delivered at once.
  • The secret is shown when the endpoint is registered and when it is rotated — never again; rotation destroys the old one.

Verifying a signature — Node

import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the request body exactly as received, as text — never JSON that was parsed and serialized again.
export function verifyDelivery(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(signatureHeader.split(",").map((p) => p.split("=", 2)));
  const t = Number(parts.t);
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`, "utf8").digest();
  const given = Buffer.from(parts.v1 ?? "", "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}

Verifying a signature — Python

import hashlib, hmac, time

# raw_body: the request body exactly as received, as bytes.
def verify_delivery(raw_body: bytes, signature_header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    t = int(parts.get("t", "0"))
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), str(t).encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

The same reads are offered to AI assistants through the MCP server.