# Pactly Public API v2 (in development) V2 is in development. It starts with the same endpoints and behavior as v1. More endpoints and changes to requests and responses are coming before v2 is declared stable. Use v1 for integrations that require a frozen contract. The same API keys work with both versions; select the version in the URL. > Programmatic access to contract lifecycle management: create, retrieve, and manage contracts, templates, and documents. ## Authentication All endpoints require authentication via an API key passed in the `x-api-key` HTTP header. ``` x-api-key: YOUR_API_KEY ``` ## Base URL ``` https://clientapi.pactly.ai ``` ## Contracts Operations related to contract management including creation, retrieval, updates, and text extraction ### GET /v2/contract-statuses ### GET /v2/contracts List contracts allowed by the API-key owner current ACL, newest first. Scopes can narrow this access. Results are cursor-paginated: pass the `cursor` from a response back to fetch the next page and stop when `hasMore` is false. The cursor encodes the sort position and is opaque — do not build one. Filters combine with AND. `party` and `counterparty` both narrow by the parties linked to a contract: `party` takes a party id from `GET /v2/parties`, `counterparty` matches a name and is the convenience form. Property values filter as `property[]=` using a `key` from `GET /v2/properties`; an unknown key is rejected rather than ignored. **Parameters:** - `keyword` *(query)* *string* — Match against contract name and reference - `type` *(query)* *string: template | playbook | external* — Storage origin, not business type. Discover business types with GET /v2/categories. - `status` *(query)* *string* — Comma-separated status codes: 1=Draft, 2=In Negotiation, 3=Pending Approval, 4=Pending Signature, 5=Executed, 6=Terminated. e.g. `status=2,3,4` for everything in flight. - `party` *(query)* *string* — Only contracts linked to this party - `counterparty` *(query)* *string* — Only contracts linked to a party matching this name - `partyRole` *(query)* *string: counterparty | signer | counterparty_representative | our_representative* — Narrow a party or counterparty filter to one role on the contract - `category` *(query)* *string* — Category ID or exact label from GET /v2/categories. Ambiguous labels return 400. - `owner` *(query)* *string* — Company-scoped owner ID or exact email. Ambiguous emails return 400. - `createdAfter` *(query)* *string (date-time)* — Only contracts created after this date (ISO 8601) - `createdBefore` *(query)* *string (date-time)* — Only contracts created before this date (ISO 8601) - `updatedAfter` *(query)* *string (date-time)* — Only contracts updated after this date (ISO 8601) - `updatedBefore` *(query)* *string (date-time)* — Only contracts updated before this date (ISO 8601) - `renewalAfter` *(query)* *string (date-time)* — Renewal anchor on or after this date. This can be an earlier notice deadline; use expirationAfter/Before for expiration. - `renewalBefore` *(query)* *string (date-time)* — Renewal anchor on or before this date - `expirationAfter` *(query)* *string (date-time)* — Stored expiration date on or after this ISO date; unknown dates do not match. - `expirationBefore` *(query)* *string (date-time)* — Stored expiration date on or before this ISO date. - `renewalState` *(query)* *string* — Filter by renewal state - `property` *(query)* *object* — Filter by contract property value: `property[]=`, where `` comes from GET /v2/properties. Values are coerced to the property's declared type; an unknown key or an operator in place of a value is rejected. Party properties take a party ID. - `sort` *(query)* *string: createdAt | updatedAt | renewalAnchorDate, default: createdAt* — Sorting by renewalAnchorDate returns only contracts that have one - `order` *(query)* *string: asc | desc, default: desc* - `cursor` *(query)* *string* — Opaque cursor from a previous response - `pagesize` *(query)* *integer, default: 50* — Page size (capped at 200) **Response (200):** List of contracts - `contracts` *array* - `pagination` *object* — Keyset pagination. Pass `cursor` back to fetch the next page and stop when `hasMore` is false. The cursor is opaque: it encodes the sort position, not just an id, and must not be constructed by hand. - `pageSize` *integer* — Rows in this page - `hasMore` *boolean* — Whether another page follows - `cursor` *string, nullable* — Pass as `cursor` to fetch the next page Example response: ```json { "contracts": [ { "id": "507f1f77bcf86cd799439011", "type": "template", "name": "Software License Agreement - Acme Corp", "url": "https://app.pactly.ai/contracts/template/507f1f77bcf86cd799439011", "expirationDate": "2025-02-01T00:00:00Z", "endDateBasis": "fixed", "noticeType": null, "noticePeriodDays": null, "autorenew": false, "reference": "SLA-2024-001", "createdAt": "2024-01-15T10:30:00Z", "updatedAt": "2024-01-20T14:45:00Z", "status": { "code": 5, "label": "Executed" }, "archived": false, "aborted": false, "renewalAnchorDate": "2025-02-01T00:00:00Z", "renewalState": "linked", "category": { "id": "507f1f77bcf86cd799439031", "label": "Licensing", "abbreviation": "LIC" } }, { "id": "507f1f77bcf86cd799439014", "type": "playbook", "name": "Service Agreement - Beta Inc", "url": "https://app.pactly.ai/contracts/playbook/507f1f77bcf86cd799439014", "expirationDate": null, "endDateBasis": null, "noticeType": null, "noticePeriodDays": null, "autorenew": null, "reference": "SA-2024-002", "createdAt": "2024-01-16T09:00:00Z", "updatedAt": "2024-01-18T11:30:00Z", "status": { "code": 1, "label": "Draft" }, "archived": false, "aborted": false, "renewalAnchorDate": null, "renewalState": null, "category": null } ], "pagination": { "pageSize": 50, "hasMore": true, "cursor": "eyJjIjoxLCJzIjp7ImsiOiJkIiwidiI6IjIwMjQtMDEtMTZUMDk6MDA6MDBaIn19" } } ``` ### POST /v2/contracts/search Supply up to 50 contract IDs from discovery. Returns bounded current-source citations, indexing coverage and continuation. Ranked search is not exhaustive. **Request Body:** - `query` *string* - `contractIds` *array* - `limit` *integer* - `cursor` *string* **Response (200):** Matches, source references, coverage and pagination - `searchMode` *string* - `hits` *array* - `coverage` *object* - `state` *string: available | incomplete | unavailable* - `requestedContracts` *integer* - `missingIndexedText` *array* - `exhaustive` *boolean: false* - `warning` *string* - `pagination` *object* - `hasMore` *boolean* - `cursor` *string, nullable* ### POST /v2/contracts/generate Generate a new contract from a template. `values` is a flat map keyed by the variable, valuemap and party ids from `GET /v2/templates/{id}`: a scalar for a variable, the chosen label (an array of labels for a multi-selection valuemap) for a valuemap, and an object of the declared attributes for a party. An unknown key, a wrong type, an unknown label or a missing required value is a 400 naming what is wrong. A value that is not supplied renders as `[●]`. `options.persist` (default `true`) decides whether Pactly keeps the contract. Persisted contracts are returned as a record you then fetch with `GET /v2/contracts/{id}/download`; ephemeral ones (`"persist": false`) keep nothing — no contract, no file, no audit event — and the document comes back base64-encoded in the response. `options.triggerWorkflows` defaults to the value of `persist` and sends real email to real people, so turn it off for a bulk import. It cannot be enabled without persisting: a workflow has no contract to act on. **Request Body:** - `templateId` *string* — Template to generate from - `contractName` *string* — Name for the generated contract - `values` *object* — Flat map keyed by the ids from GET /v2/templates/{id}. A variable takes a value of its declared valueType — a string for text (numbers are rejected, not coerced), a real boolean for boolean. A valuemap takes the chosen label — an array of labels when allowMultipleSelection is true, a single label when it is not. A party takes an object of its declared attributes, plus "type" ("entity" or "individual") when the template leaves the party type "unknown". Anything omitted renders as [●]. - `options` *object* - `persist` *boolean* — Keep the contract in Pactly. False returns the document and stores nothing. - `triggerWorkflows` *boolean* — Run the templateContractCreated workflows. Defaults to the value of persist; cannot be true when persist is false. - `reference` *string* — Your own identifier, rendered wherever the template prints {{__reference}}. Only accepted with "persist": false — Pactly allocates its own reference otherwise. Omitted, the tag renders [●]. **Response (200):** Contract generated. The shape follows options.persist: a contract record, or the document itself. ### GET /v2/contracts/{id} Retrieve detailed information about a specific contract including all properties, metadata, and status information. **Parameters:** - `id` *(path)* **required** *string* — Route pattern variable `id` **Response (200):** Contract details - `id` *string* — Contract ID - `type` *string: template | playbook | external* — Contract type - `name` *string* — Contract name - `reference` *string* — Contract reference number - `status` — Company-configured status object, normally {"label":"Executed"}. If no label is configured, returns a string such as "STATUS_5". The numeric code is included in contract list responses, not in this detail response. - `createdAt` *string (date-time)* - `updatedAt` *string (date-time)* - `finalizedAt` *string (date-time), nullable* - `executedAt` *string (date-time), nullable* - `executionMethod` *string, nullable* - `archived` *boolean* - `deleted` *boolean* - `aborted` *boolean* - `final` *boolean* - `hubRequestStatus` *string* - `properties` *object* — Contract properties with values and metadata - `value` — Stored JSON value: a string, number, boolean, array, object, or null. Date properties use epoch milliseconds. Party values with an ID are returned as {id, label}; label can be null when the party cannot be resolved. Use GET /v2/properties for property definitions. - `meta` *object* — Property metadata including name, type, and grouping - `metadata` *array* — An entry for each stored property, with its current catalogue label and type when available. This supplements the properties object. Missing values are null. - `url` *string (uri)* — Link to this contract in Pactly - `user` *string, nullable* — ID of the user who owns the contract - `requester` *string, nullable* — ID of the user who requested the contract - `group` *string, nullable* — ID of the group this contract belongs to - `category` *object, nullable* — Contract category, or null when unavailable - `id` *string* - `label` *string, nullable* - `abbreviation` *string, nullable* - `form` *string, nullable* — ID of the associated form - `formentry` *string, nullable* — ID of the form entry - `template` *string, nullable* — ID of the template used (for template contracts) - `playbook` *string, nullable* — ID of the playbook used (for playbook contracts) - `setModuleTo` *string* — Parent contract ID, present when a template contract belongs to a contract set - `parties` *array* — Readable, confirmed parties associated with this contract. Excludes deleted parties and links awaiting review. Empty when none qualify. Example response: ```json { "id": "507f1f77bcf86cd799439011", "type": "template", "name": "Software License Agreement - Acme Corp", "reference": "SLA-2024-001", "status": { "label": "Executed" }, "createdAt": "2024-01-15T10:30:00Z", "updatedAt": "2024-01-20T14:45:00Z", "finalizedAt": "2024-01-20T14:45:00Z", "executedAt": "2024-01-20T15:00:00Z", "executionMethod": "docusign", "archived": false, "deleted": false, "aborted": false, "final": true, "hubRequestStatus": "completed", "properties": { "licenseType": { "value": "Enterprise", "meta": { "name": "License Type", "description": "Type of software license", "type": "select", "group": "License Details" } }, "effectiveDate": { "value": 1706745600000, "meta": { "name": "Effective Date", "description": "When the license becomes active", "type": "date", "group": "License Details" } } }, "metadata": [ { "key": "licenseType", "label": "License Type", "type": "list", "value": "Enterprise" }, { "key": "effectiveDate", "label": "Effective Date", "type": "date", "value": 1706745600000 } ], "url": "https://app.pactly.ai/contracts/template/507f1f77bcf86cd799439011", "category": { "id": "507f1f77bcf86cd799439031", "label": "Licensing", "abbreviation": "LIC" }, "parties": [], "user": "507f1f77bcf86cd799439012", "template": "507f1f77bcf86cd799439013" } ``` ### PUT /v2/contracts/{id}/properties Set up to 20 property values on a contract the key owner can edit, keyed by property key. Keys must exist in the company property catalogue, including global properties (see GET /v2/properties), and can be added to the contract. Conditional list choices use the resulting parent value; blank numeric strings are rejected. Concurrent writes retry or return 409. Dates take YYYY-MM-DD; null clears a value. Runs with the key owner's current permissions and has the same effect as editing the value in Pactly, including the renewal schedule. Requires the contracts:write scope. **Parameters:** - `id` *(path)* **required** *string* — Route pattern variable `id` **Request Body:** - `properties` *object* **Response (200):** Updated values and the resulting renewal state - `id` *string* - `updated` *object* - `expirationDate` *number, nullable* - `renewalAnchorDate` *string, nullable* - `renewalState` *string, nullable* ### PUT /v2/contracts/{id}/status Set the status of a contract the key owner can edit, by code or label from GET /v2/contract-statuses. Same as Change status in Pactly: no workflow transitions run. Requires the contracts:write scope and the owner's permission to change status. **Parameters:** - `id` *(path)* **required** *string* — Route pattern variable `id` **Request Body:** - `status` **Response (200):** The status now set on the contract - `id` *string* - `status` *object* - `code` *integer* - `label` *string* - `changed` *boolean* ### GET /v2/contracts/{id}/text Extract and return the full text content of a contract. Useful for search, analysis, and AI processing. **Parameters:** - `maxCharacters` *(query)* *integer, default: 24000* — Enable bounded evidence pages with this character budget. - `textCursor` *(query)* *string* — Continuation cursor from the previous evidence page. - `id` *(path)* **required** *string* — Contract ID (MongoDB ObjectId format) **Response (200):** Contract text ### GET /v2/contracts/{id}/download Download a contract file by ID **Parameters:** - `id` *(path)* **required** *string* — Contract ID (MongoDB ObjectId format) **Response (200):** Contract file download Content types: `application/vnd.openxmlformats-officedocument.wordprocessingml.document`, `application/pdf` ## Parties ### GET /v2/parties List the parties (counterparties, entities and individuals) in the contact book. Excludes parties still awaiting confirmation — an AI-extracted party a person has not accepted is a suggestion, not a counterparty — and soft-deleted parties. `keyword` matches the normalised search string, so "ASTAR" finds "A*STAR". Results are cursor-paginated: pass the `cursor` from a response back to fetch the next page, and stop when `hasMore` is false. A cursor is opaque and must not be constructed by hand. **Parameters:** - `keyword` *(query)* *string* — Match against party names, entity names and abbreviations - `isEntity` *(query)* *boolean* — true returns only organisations, false only individuals; omit for both - `isInternal` *(query)* *boolean* — true returns only your own entities, false only external counterparties - `parent` *(query)* *string* — Return the children of this party — the way to walk a corporate group - `cursor` *(query)* *string* — Opaque cursor from a previous response - `pagesize` *(query)* *integer, default: 50* — Page size (capped at 200) - `order` *(query)* *string: asc | desc, default: desc* — Sort direction on creation date **Response (200):** List of parties - `parties` *array* - `pagination` *object* — Keyset pagination. Pass `cursor` back to fetch the next page and stop when `hasMore` is false. The cursor is opaque: it encodes the sort position, not just an id, and must not be constructed by hand. - `pageSize` *integer* — Rows in this page - `hasMore` *boolean* — Whether another page follows - `cursor` *string, nullable* — Pass as `cursor` to fetch the next page ### GET /v2/party-attributes The custom attribute definitions for parties. A party carries its values in `customAttributes` keyed by the `key` returned here, the same way a contract pairs its property values with `GET /v2/properties`. **Response (200):** List of party attribute definitions - `partyAttributes` *array* ### GET /v2/parties/{id} Get a single party by id. **Parameters:** - `id` *(path)* **required** *string* — Party ID (MongoDB ObjectId format) **Response (200):** Party - `party` *object* — A party in the contact book. Parties awaiting confirmation and soft-deleted parties are never returned. - `id` *string* — Party ID - `name` *string* — Display name — the entity name for organisations, the full name for individuals - `isEntity` *boolean* — True for an organisation, false for an individual - `isInternal` *boolean* — True for one of your own entities, false for an external counterparty - `entityType` *string, nullable* - `entityRegNo` *string, nullable* — Company registration number, entities only - `email` *string, nullable* - `country` *string, nullable* - `tags` *array* - `abbreviations` *array* — Alternative names this party is also known by - `parent` *string, nullable* — Parent party id, for corporate groups. Use `?parent=` to list children. - `customAttributes` *object* — Values keyed by the `key` of a definition from GET /v2/party-attributes - `sanctionsStatus` *string: clear | sanctioned | unknown* — Sanctions screening result. `unknown` means screening has not run or could not complete — it is NOT a clear result and must not be treated as one. - `createdAt` *string (date-time)* - `updatedAt` *string (date-time)* ## Templates Manage contract templates that serve as the foundation for generating new contracts ### GET /v2/templates Retrieve all available contract templates that can be used to create new contracts. **Response (200):** List of templates - Type: array - Items: - `id` *string* — Unique identifier of the template - `name` *string* — Name of the template - `version` *string* — Version number of the template - `description` *string* — Description of the template purpose and usage - `createdAt` *string (date-time)* — Template creation timestamp - `variables` *array* — Variables this template takes. Post them by id in the "values" map when generating a contract. - `valuemaps` *array* — Valuemaps this template takes. Post the chosen label by id in the "values" map when generating a contract. - `parties` *array* — Parties this template prints details for. Post an object of the listed attributes by id in the "values" map. ### GET /v2/templates/{id} Retrieve detailed information about a specific template including variables and value mappings. **Parameters:** - `id` *(path)* **required** *string* — Template ID (MongoDB ObjectId format) **Response (200):** Template - `id` *string* — Unique identifier of the template - `name` *string* — Name of the template - `version` *string* — Version number of the template - `description` *string* — Description of the template purpose and usage - `createdAt` *string (date-time)* — Template creation timestamp - `variables` *array* — Variables this template takes. Post them by id in the "values" map when generating a contract. - `valuemaps` *array* — Valuemaps this template takes. Post the chosen label by id in the "values" map when generating a contract. - `parties` *array* — Parties this template prints details for. Post an object of the listed attributes by id in the "values" map. Example response: ```json { "id": "507f1f77bcf86cd799439013", "name": "Software License Agreement Template", "version": "2.1.0", "description": "Standard template for software licensing agreements with customizable terms", "createdAt": "2023-06-15T08:00:00Z", "variables": [ { "id": "companyName", "question": "Name of the licensing company", "valueType": "string", "required": true, "default": null, "conditional": false }, { "id": "seatCount", "question": "How many seats are licensed?", "valueType": "number", "required": true, "default": 25, "conditional": false } ], "valuemaps": [ { "id": "licenseType", "question": "Which tier applies?", "allowMultipleSelection": false, "choices": [ "Basic", "Professional", "Enterprise" ] } ], "parties": [ { "id": "party1", "label": "Licensee", "type": "entity", "attributes": [ "entityName", "entityRegNo", "country", "address" ] } ] } ``` ## Playbooks ### GET /v2/playbooks List the playbooks in force — your negotiation positions as structured data. Returns your own playbooks and any Pactly-provided default you have not switched off; `isDefault` tells them apart. Only production versions are returned, because a draft is not policy. Positions are omitted here; fetch one playbook to get them. **Parameters:** - `cursor` *(query)* *string* — Opaque cursor from a previous response - `pagesize` *(query)* *integer, default: 50* - `order` *(query)* *string: asc | desc, default: desc* **Response (200):** List of playbooks - `playbooks` *array* - `pagination` *object* — Keyset pagination. Pass `cursor` back to fetch the next page and stop when `hasMore` is false. The cursor is opaque: it encodes the sort position, not just an id, and must not be constructed by hand. - `pageSize` *integer* — Rows in this page - `hasMore` *boolean* — Whether another page follows - `cursor` *string, nullable* — Pass as `cursor` to fetch the next page ### GET /v2/playbooks/{id} A playbook with its positions: keywords, fallback language, and what a missing clause means for each position (`ifNotFound`). Positions still awaiting human confirmation are excluded — an AI-suggested position is not policy until someone accepts it — and counted in `unconfirmedPositionCount`. Position notes are limited to drafting guidance; internal rationale and escalation routing are not published. **Parameters:** - `id` *(path)* **required** *string* — Playbook ID (MongoDB ObjectId format) **Response (200):** Playbook with positions - `playbook` *object* — A playbook: the negotiation positions in force. Only production versions are returned. - `id` *string* - `name` *string, nullable* - `description` *string, nullable* - `version` *integer* - `versionNote` *string, nullable* - `status` *string* - `isDefault` *boolean* — True for a Pactly-provided standard playbook, false for your own - `positionCount` *integer* - `unconfirmedPositionCount` *integer* — Positions suggested but not yet accepted by a person. They are excluded from `positions`; this is how many were withheld. - `createdAt` *string (date-time)* - `updatedAt` *string (date-time)* - `groups` *array* - `positions` *array* ### POST /v2/playbooks/{id}/match Report which playbook position keywords appear in a document, and where. **This is keyword matching, not a compliance review.** It returns no verdict on whether a position is met — deciding that is a separate AI evaluation. Use the matches, each position's fallback language and its `ifNotFound` setting to reach your own conclusion. Send `paragraphs` as an array of strings, exactly the shape `GET /v2/contracts/{id}/text` returns. Nothing is persisted: no review, no round, no change to the contract. **Parameters:** - `id` *(path)* **required** *string* — Playbook ID (MongoDB ObjectId format) **Request Body:** - `paragraphs` *array* — The document text, one entry per paragraph **Response (200):** Keyword matches per position - `playbook` *object* - `id` *string* - `name` *string, nullable* - `isDefault` *boolean* - `positions` *array* ## Clauses ### GET /v2/clauses List the clause library — the pre-approved language an agent should propose instead of drafting its own. Visibility follows the key owner: an admin key sees the whole library, a grouped user sees their groups, and a key owned by a lite user is refused, because lite users have no Clause Bank. **Parameters:** - `tag` *(query)* *string* — Filter by tag - `cursor` *(query)* *string* — Opaque cursor from a previous response - `pagesize` *(query)* *integer, default: 50* - `order` *(query)* *string: asc | desc, default: desc* **Response (200):** List of clauses - `clauses` *array* - `pagination` *object* — Keyset pagination. Pass `cursor` back to fetch the next page and stop when `hasMore` is false. The cursor is opaque: it encodes the sort position, not just an id, and must not be constructed by hand. - `pageSize` *integer* — Rows in this page - `hasMore` *boolean* — Whether another page follows - `cursor` *string, nullable* — Pass as `cursor` to fetch the next page ## Properties Contract property definitions that define the metadata fields available on contracts ### GET /v2/properties Retrieve all contract property definitions available to the company, including system default properties. **Response (200):** List of property definitions - `properties` *array* ### GET /v2/properties/{idOrKey} Retrieve a single property definition by its ID or unique key. **Parameters:** - `idOrKey` *(path)* **required** *string* — Property ID (MongoDB ObjectId) or property key **Response (200):** Property definition - `id` *string* — Property ID - `key` *string* — Unique property key identifier - `label` *string* — Display label - `description` *string, nullable* - `type` *string, nullable* — Property data type (string, number, boolean, date, list, multi-option, etc.) - `options` *object, nullable* — Available options for list/select type properties - `group` *string, nullable* — Property group name - `subGroup` *string, nullable* - `sortOrder` *integer* — Display sort order - `isDefault` *boolean* — Whether this is a system default property - `manualOnly` *boolean* — Whether this property can only be set manually (not via AI extraction) ## Categories Contract categories used to classify and organize contracts ### GET /v2/categories Retrieve all contract categories available to the company, including system default categories. **Response (200):** List of categories - `categories` *array* ## Emails Email-related operations ### GET /v2/emails List all emails associated with contracts (for AI training) **Parameters:** - `pagesize` *(query)* *integer, default: 100* — Number of emails to return - `cursor` *(query)* *string* — Cursor for pagination - `contractType` *(query)* *string: template | playbook | external* — Filter by contract type - `contractId` *(query)* *string* — Filter by specific contract ID - `sender` *(query)* *string* — Filter by sender email - `dateFrom` *(query)* *string (date)* — Filter emails from this date - `dateTo` *(query)* *string (date)* — Filter emails until this date **Response (200):** List of emails with contract context - `emails` *array* - `pagination` *object* - `pageSize` *integer* - `hasMore` *boolean* - `cursor` *string* - `total` *integer* — Number of readable emails matching the requested filters, across all pages. The cursor does not affect this count. ## General ### GET /v2/test Test endpoint to test API connectivity and authentication **Response (200):** Test successful - `message` *string* ## Error Responses All endpoints return errors in a consistent format: ```json { "error": "Bad Request", "message": "Description of what went wrong" } ``` Common HTTP status codes: - `400` — Bad request (invalid parameters or body) - `403` — Missing, invalid, expired, revoked, or insufficiently scoped API key; access may also be denied by the key owner permissions - `404` — Resource not found - `500` — Internal server error ## Rate Limiting API requests are rate-limited. If you exceed the limit, you will receive a `429 Too Many Requests` response. Implement exponential backoff in your integration.