QuillstreamQuillstream
ProductAgent APIDocsPricingDemo
Sign In
Sign In

API reference

Quillstream Agent API

The REST surface an external agent calls: read the documents of the organization the token names, read one block with the drafts standing against it, and propose a draft. An agent can never approve its own work — every submission lands pending, awaiting a human verdict. The `webhooks` section is the other direction: the three signed events this product POSTs to a receiver you register.

Version
1.0.0
OpenAPI
3.1.0
Base URL
https://quillstream.shiftclawco.com
Download the OpenAPI document

Authentication

An agent token minted in the agents console. The organization it acts for is a property of the credential — no header, parameter or body field can name another one.

Authorization: Bearer <YOUR_AGENT_TOKEN>

Endpoints

  • get/api/agent/documentsList the documents of your workspace
  • get/api/agent/blocks/{id}Read a block and its pending drafts
  • post/api/agent/draftsPropose a draft on a block
GET/api/agent/documents

List the documents of your workspace

operationId: documents.list

Returns the documents your organization owns, newest first, with the number of drafts each one still has pending. Optional filters: a page size (1-100) and a document status.

Parameters

NameInRequiredType
limitquerynointeger
statusquerynostring

Responses

StatusSchemaDescription
200ListDocumentsResponseList the documents of your workspace
400AgentErrorA query parameter or body field is invalid. The body names the offending field and the reason; it never echoes the value received.
401AgentErrorThe `Authorization: Bearer <token>` header is missing, malformed, unknown or revoked. Answered before the request is looked at, so a refusal never confirms which parameters the endpoint understands.
403AgentErrorThe credential's organization does not own the resource. A foreign id, a missing id and a malformed id are answered IDENTICALLY, so the endpoint can never be swept to learn what exists elsewhere.
GET/api/agent/blocks/{id}

Read a block and its pending drafts

operationId: blocks.get

Returns one block of a document in your workspace together with every draft still awaiting a human verdict. A block id belonging to another workspace is refused exactly as a malformed one is, so the endpoint never confirms what exists elsewhere.

Parameters

NameInRequiredType
idpathyesstring

Responses

StatusSchemaDescription
200GetBlockResponseRead a block and its pending drafts
400AgentErrorA query parameter or body field is invalid. The body names the offending field and the reason; it never echoes the value received.
401AgentErrorThe `Authorization: Bearer <token>` header is missing, malformed, unknown or revoked. Answered before the request is looked at, so a refusal never confirms which parameters the endpoint understands.
403AgentErrorThe credential's organization does not own the resource. A foreign id, a missing id and a malformed id are answered IDENTICALLY, so the endpoint can never be swept to learn what exists elsewhere.
POST/api/agent/drafts

Propose a draft on a block

operationId: drafts.submit

Submits a proposed rewrite of one block. The draft always lands as agent-authored and pending — an agent can never approve its own work — and the reviewer decides in the editor.

Request body SubmitDraftRequest

Responses

StatusSchemaDescription
201SubmitDraftResponsePropose a draft on a block
400AgentErrorA query parameter or body field is invalid. The body names the offending field and the reason; it never echoes the value received.
401AgentErrorThe `Authorization: Bearer <token>` header is missing, malformed, unknown or revoked. Answered before the request is looked at, so a refusal never confirms which parameters the endpoint understands.
403AgentErrorThe credential's organization does not own the resource. A foreign id, a missing id and a malformed id are answered IDENTICALLY, so the endpoint can never be swept to learn what exists elsewhere.

Webhooks

The other direction: register a receiver in the agents console and every event below is POSTed to it, signed, within seconds of happening. Each entry is keyed by its event name rather than by a path — the path is yours.

POSTdraft.created

A draft was proposed against a section

Sent when a proposal is made against a section, by an agent or by a person. `data.authorKind` is which — resolved by the write path, never taken from the caller. The passage itself is never in the payload.

Payload DraftCreatedEvent

2XX — Answer any 2xx to acknowledge. Anything else — or no answer inside the request timeout — is retried on the published schedule and then abandoned.

POSTsection.approved

A human approved a proposed section

Sent when a human accepts a proposal. An agent can never approve its own work, so this event is always the record of a person deciding.

Payload SectionApprovedEvent

2XX — Answer any 2xx to acknowledge. Anything else — or no answer inside the request timeout — is retried on the published schedule and then abandoned.

POSTsection.rejected

A human rejected a proposed section, with the reason

Sent when a human refuses a proposal. `data.reason` carries the rationale the reviewer typed and is always present — an agent that learns only "no" retries blind; the rationale is what lets it revise instead.

Payload SectionRejectedEvent

2XX — Answer any 2xx to acknowledge. Anything else — or no answer inside the request timeout — is retried on the published schedule and then abandoned.

Verifying a delivery

Every attempt carries two headers. Refuse anything outside the tolerance before you compute a signature, and compare in constant time.

FactValue
Signature headerX-Quillstream-Signature
Timestamp headerX-Quillstream-Timestamp
SignatureHMAC-SHA256(secret, `${timestamp}.${rawBody}`) — compare in constant time against the v1= value
Timestamp tolerance300s
Retries, then abandoned30s, 120s, 600s, 3600s (5 attempts)
import { createHmac, timingSafeEqual } from 'node:crypto';

const SIGNATURE_HEADER = 'x-quillstream-signature';
const TIMESTAMP_HEADER = 'x-quillstream-timestamp';
const VERSION_PREFIX = 'v1=';
const TOLERANCE_SECONDS = 300;

// `rawBody` MUST be the exact bytes received, before any JSON parsing:
// re-serialising the object changes the bytes and the signature will not match.
export function verifyQuillstreamDelivery(headers, rawBody, secret) {
  const signature = headers[SIGNATURE_HEADER];
  if (typeof signature !== 'string' || !signature.startsWith(VERSION_PREFIX)) return false;

  const timestamp = Number(headers[TIMESTAMP_HEADER]);
  if (!Number.isFinite(timestamp)) return false;
  // Refuse a stale delivery BEFORE computing an HMAC: a valid signature never
  // expires on its own, so this window is what bounds a replay.
  if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > TOLERANCE_SECONDS) return false;

  const expected =
    VERSION_PREFIX + createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');

  const received = Buffer.from(signature);
  const computed = Buffer.from(expected);
  // Constant time, and length-checked first: timingSafeEqual throws on a
  // length mismatch.
  return received.length === computed.length && timingSafeEqual(received, computed);
}

Destination requirements

A receiver URL that breaks any of these is refused at registration, naming the rule it failed. Build against them and the first attempt succeeds.

  • https_onlyThe URL must use https.
  • public_hostThe host must be publicly resolvable — never loopback, a private range, link-local or *.local.
  • no_credentialsThe URL must not embed a username or password.
  • no_fragmentThe URL must not carry a fragment.

Schemas

JSON Schema (draft 2020-12), emitted from the same contract the handlers validate every request and every response against.

AgentError

{
  "type": "object",
  "properties": {
    "error": {
      "type": "string"
    },
    "code": {
      "type": "string"
    },
    "status": {
      "type": "integer",
      "minimum": -9007199254740991,
      "maximum": 9007199254740991
    }
  },
  "required": [
    "error",
    "code",
    "status"
  ],
  "additionalProperties": false
}

WebhookAck

{
  "type": "object",
  "properties": {},
  "additionalProperties": false,
  "description": "No body is required. Any 2xx acknowledges the delivery; the response body is never read."
}

DraftCreatedEvent

{
  "type": "object",
  "properties": {
    "eventId": {
      "type": "string",
      "minLength": 1
    },
    "deliveryId": {
      "type": "string",
      "minLength": 1
    },
    "occurredAt": {
      "type": "string",
      "minLength": 1
    },
    "event": {
      "type": "string",
      "const": "draft.created"
    },
    "data": {
      "type": "object",
      "properties": {
        "documentId": {
          "type": "string",
          "minLength": 1
        },
        "blockId": {
          "type": "string",
          "minLength": 1
        },
        "draftId": {
          "type": "string",
          "minLength": 1
        },
        "authorKind": {
          "type": "string",
          "enum": [
            "human",
            "agent"
          ]
        },
        "authorLabel": {
          "type": "string",
          "minLength": 1
        }
      },
      "required": [
        "documentId",
        "blockId",
        "draftId",
        "authorKind",
        "authorLabel"
      ],
      "additionalProperties": false
    }
  },
  "required": [
    "eventId",
    "deliveryId",
    "occurredAt",
    "event",
    "data"
  ],
  "additionalProperties": false
}

SectionApprovedEvent

{
  "type": "object",
  "properties": {
    "eventId": {
      "type": "string",
      "minLength": 1
    },
    "deliveryId": {
      "type": "string",
      "minLength": 1
    },
    "occurredAt": {
      "type": "string",
      "minLength": 1
    },
    "event": {
      "type": "string",
      "const": "section.approved"
    },
    "data": {
      "type": "object",
      "properties": {
        "documentId": {
          "type": "string",
          "minLength": 1
        },
        "blockId": {
          "type": "string",
          "minLength": 1
        },
        "draftId": {
          "type": "string",
          "minLength": 1
        },
        "reviewerLabel": {
          "type": "string",
          "minLength": 1
        }
      },
      "required": [
        "documentId",
        "blockId",
        "draftId",
        "reviewerLabel"
      ],
      "additionalProperties": false
    }
  },
  "required": [
    "eventId",
    "deliveryId",
    "occurredAt",
    "event",
    "data"
  ],
  "additionalProperties": false
}

SectionRejectedEvent

{
  "type": "object",
  "properties": {
    "eventId": {
      "type": "string",
      "minLength": 1
    },
    "deliveryId": {
      "type": "string",
      "minLength": 1
    },
    "occurredAt": {
      "type": "string",
      "minLength": 1
    },
    "event": {
      "type": "string",
      "const": "section.rejected"
    },
    "data": {
      "type": "object",
      "properties": {
        "documentId": {
          "type": "string",
          "minLength": 1
        },
        "blockId": {
          "type": "string",
          "minLength": 1
        },
        "draftId": {
          "type": "string",
          "minLength": 1
        },
        "reviewerLabel": {
          "type": "string",
          "minLength": 1
        },
        "reason": {
          "type": "string",
          "minLength": 1
        }
      },
      "required": [
        "documentId",
        "blockId",
        "draftId",
        "reviewerLabel",
        "reason"
      ],
      "additionalProperties": false
    }
  },
  "required": [
    "eventId",
    "deliveryId",
    "occurredAt",
    "event",
    "data"
  ],
  "additionalProperties": false
}

ListDocumentsResponse

{
  "type": "object",
  "properties": {
    "op": {
      "type": "string",
      "const": "documents.list"
    },
    "documents": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "documentId": {
            "type": "string",
            "minLength": 1
          },
          "path": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "in_review",
              "approved",
              "archived"
            ]
          },
          "pendingDraftCount": {
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "updatedAt": {
            "type": "number"
          },
          "createdAt": {
            "type": "number"
          }
        },
        "required": [
          "documentId",
          "path",
          "title",
          "status",
          "pendingDraftCount",
          "updatedAt",
          "createdAt"
        ],
        "additionalProperties": false
      }
    }
  },
  "required": [
    "op",
    "documents"
  ],
  "additionalProperties": false
}

GetBlockResponse

{
  "type": "object",
  "properties": {
    "op": {
      "type": "string",
      "const": "blocks.get"
    },
    "block": {
      "type": "object",
      "properties": {
        "blockId": {
          "type": "string",
          "minLength": 1
        },
        "documentId": {
          "type": "string",
          "minLength": 1
        },
        "order": {
          "type": "number"
        },
        "ownerKind": {
          "type": "string",
          "enum": [
            "human",
            "agent",
            "review"
          ]
        },
        "ownerLabel": {
          "type": "string"
        },
        "status": {
          "type": "string",
          "enum": [
            "clean",
            "drafting",
            "review",
            "conflict"
          ]
        },
        "body": {
          "type": "string"
        }
      },
      "required": [
        "blockId",
        "documentId",
        "order",
        "ownerKind",
        "ownerLabel",
        "status",
        "body"
      ],
      "additionalProperties": false
    },
    "pendingDrafts": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "draftId": {
            "type": "string",
            "minLength": 1
          },
          "blockId": {
            "type": "string",
            "minLength": 1
          },
          "authorKind": {
            "type": "string",
            "enum": [
              "human",
              "agent"
            ]
          },
          "authorLabel": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "rejected",
              "superseded"
            ]
          },
          "body": {
            "type": "string"
          },
          "createdAt": {
            "type": "number"
          }
        },
        "required": [
          "draftId",
          "blockId",
          "authorKind",
          "authorLabel",
          "status",
          "body",
          "createdAt"
        ],
        "additionalProperties": false
      }
    }
  },
  "required": [
    "op",
    "block",
    "pendingDrafts"
  ],
  "additionalProperties": false
}

SubmitDraftResponse

{
  "type": "object",
  "properties": {
    "op": {
      "type": "string",
      "const": "drafts.submit"
    },
    "draft": {
      "type": "object",
      "properties": {
        "draftId": {
          "type": "string",
          "minLength": 1
        },
        "blockId": {
          "type": "string",
          "minLength": 1
        },
        "documentId": {
          "type": "string",
          "minLength": 1
        },
        "authorKind": {
          "type": "string",
          "const": "agent"
        },
        "status": {
          "type": "string",
          "const": "pending"
        },
        "blockStatus": {
          "type": "string",
          "enum": [
            "clean",
            "drafting",
            "review",
            "conflict"
          ]
        },
        "pendingDraftCount": {
          "type": "integer",
          "minimum": 0,
          "maximum": 9007199254740991
        }
      },
      "required": [
        "draftId",
        "blockId",
        "documentId",
        "authorKind",
        "status",
        "blockStatus",
        "pendingDraftCount"
      ],
      "additionalProperties": false
    }
  },
  "required": [
    "op",
    "draft"
  ],
  "additionalProperties": false
}

SubmitDraftRequest

{
  "type": "object",
  "properties": {
    "blockId": {
      "type": "string",
      "minLength": 1
    },
    "body": {
      "type": "string",
      "minLength": 1,
      "maxLength": 20000
    },
    "authorLabel": {
      "type": "string",
      "minLength": 1,
      "maxLength": 80
    }
  },
  "required": [
    "blockId",
    "body"
  ],
  "additionalProperties": false
}

Docs your agents write. Verdicts your team keeps.

Agents propose Markdown straight into the repository your documentation already lives in. Every block keeps its owner and its provenance, and nothing reaches your default branch until a reviewer accepts it.

See pricing

Navigate

  • Product
  • Agent API
  • Docs
  • Pricing
  • Demo

Account

  • Sign In
  • Dashboard

Company

  • About
  • Contact

Legal

  • Privacy Policy
  • Terms
  • Support

© 2026 Quillstream. All rights reserved.

Privacy PolicyTerms