{
  "openapi": "3.1.0",
  "info": {
    "title": "pact — the contract layer",
    "version": "2.1.0",
    "description": "One versioned contract per pact: the terms as written, the version a signature is bound to, and an evidence bundle a stranger can check. Every row is scoped to the account that owns it.\n\n## This surface lists as well as reads\n\n`GET /_/api/v1/pacts` answers the pacts this account holds, newest first: id, kind, status, the minute it was written, and the seats that can still sign it with whether each has. It is the same list `/pact/` draws, read through the same function, and it carries no terms — the wording of one agreement is `GET /_/api/v1/pacts/{id}`, which stays the addressed read. Both reads are scoped to the account the credential names, so a counterparty sees the pacts of their own account and never the other side's."
  },
  "servers": [
    {
      "url": "/pact",
      "description": "pact on zpe.vc"
    }
  ],
  "paths": {
    "/_/api/v1/pacts": {
      "post": {
        "operationId": "postPacts",
        "summary": "Write a pact and freeze its first version, once per idempotency key.",
        "tags": [
          "pacts"
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "description": "The caller's own retry handle. The same key written twice returns the pact the first call made and writes nothing; the body's `idempotencyKey` is the same field for a client that cannot set a header.",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The terms, the parties, and the id the pact is written under. The account and the creator come from the credential, never from the body.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "kind",
                  "bodyMd",
                  "parties"
                ],
                "properties": {
                  "pactId": {
                    "type": "string",
                    "pattern": "^[a-z0-9][a-z0-9._:-]{0,127}$",
                    "description": "The id this pact is written under, for a consumer republishing a pact it already holds. Omitted, the service mints a uuid. It is the id every later call names, and the last segment of the address a link signs at."
                  },
                  "idempotencyKey": {
                    "type": "string",
                    "minLength": 1,
                    "description": "The same retry handle as the `Idempotency-Key` header, for a client that cannot set one. The header wins when both are sent. Absent is a new pact, and the same key twice replays the first answer."
                  },
                  "kind": {
                    "type": "string",
                    "minLength": 1,
                    "description": "The consumer's own word for what sort of agreement this is. Stored as given, never interpreted."
                  },
                  "bodyMd": {
                    "type": "string",
                    "minLength": 1,
                    "description": "The terms, as markdown, frozen as version 1 in the same write as the header."
                  },
                  "profileRef": {
                    "type": "string",
                    "description": "The jurisdiction and form profile, or `none`. Defaults to `none`."
                  },
                  "timeZone": {
                    "type": "string",
                    "description": "The IANA zone this pact's dates are read in, such as `Asia/Bangkok`. Defaults to `UTC`."
                  },
                  "parties": {
                    "type": "array",
                    "minItems": 1,
                    "description": "The people and companies on this pact. At least one row must carry the `counterparty` role: a promise with nobody on the other side of it is not an agreement, and that is the seat a link is sent to.",
                    "items": {
                      "type": "object",
                      "required": [
                        "role",
                        "displayName"
                      ],
                      "properties": {
                        "role": {
                          "type": "string",
                          "enum": [
                            "issuer",
                            "counterparty",
                            "arbiter",
                            "witness",
                            "notary",
                            "custodian"
                          ],
                          "description": "Which side of this pact the party is on. A witness or a notary is a role here and not yet a mechanism."
                        },
                        "partyId": {
                          "type": "string",
                          "pattern": "^[a-z0-9][a-z0-9_-]{0,63}$",
                          "description": "This pact's own name for the party, which an obligation's `obligorPartyId` cites. Defaults to the role, then `role_2` for a second party of the same role."
                        },
                        "principalId": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "The door's principal, when this party has an account. Absent or null is a counterparty named before they join."
                        },
                        "displayName": {
                          "type": "string",
                          "minLength": 1,
                          "description": "The only name a document can print for someone who has not signed in."
                        },
                        "capacity": {
                          "type": "string",
                          "enum": [
                            "person",
                            "company"
                          ],
                          "description": "How a document prints the party. Defaults to `person`."
                        }
                      }
                    }
                  },
                  "obligations": {
                    "type": "array",
                    "description": "What each party promised. Absent is a pact with no obligations, which is a legal pact.",
                    "items": {
                      "type": "object",
                      "required": [
                        "obligorPartyId",
                        "action"
                      ],
                      "properties": {
                        "obligationId": {
                          "type": "string",
                          "pattern": "^[a-z0-9][a-z0-9_-]{0,63}$",
                          "description": "This version's own name for the obligation. Defaults to `ob_<position>`, counting from 1."
                        },
                        "obligorPartyId": {
                          "type": "string",
                          "description": "The `partyId` of the party that owes it, not a principal."
                        },
                        "action": {
                          "type": "string",
                          "minLength": 1,
                          "description": "The thing promised."
                        },
                        "dueAt": {
                          "type": "integer",
                          "description": "Epoch milliseconds, after 1970. Required when `amountCents` is sent: an amount with no date is a debt nobody can be late on."
                        },
                        "amountCents": {
                          "type": "integer",
                          "minimum": 0,
                          "description": "Money in minor units. Sent with `currency` and `dueAt`, and never with one of them alone."
                        },
                        "currency": {
                          "type": "string",
                          "pattern": "^[A-Za-z]{3}$",
                          "description": "The three-letter code, stored upper-case. Sent with `amountCents` and never without it."
                        },
                        "condition": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "What makes it due, for an obligation dated by an event rather than a day."
                        },
                        "evidenceRequired": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "What the obligor must show for it to read as done."
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A replay: this account already held a pact written under this idempotency key, and it is answered as it stands. Nothing was written.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "created",
                    "pact",
                    "version",
                    "parties",
                    "obligations"
                  ],
                  "properties": {
                    "created": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "pact": {
                      "type": "object"
                    },
                    "version": {
                      "type": "object"
                    },
                    "parties": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "obligations": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "A pact this account did not hold. Its version 1 is frozen in the same batch as the header, so a 201 always names text that exists.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "created",
                    "pact",
                    "version",
                    "parties",
                    "obligations"
                  ],
                  "properties": {
                    "created": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "pact": {
                      "type": "object"
                    },
                    "version": {
                      "type": "object"
                    },
                    "parties": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "obligations": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request is missing a field a pact cannot be written without: the terms, a counterparty, a non-empty due date on a money obligation, or an id this service serves. The sentence names which.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Nobody was named: no session cookie and no key. A pact with no account is a row no one owns, so nothing is written.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "This account may not do that",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "Nothing is here",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      },
      "get": {
        "operationId": "getPacts",
        "summary": "The pacts this account holds, newest first: the same rows the owner's page draws, without the terms.",
        "tags": [
          "pacts"
        ],
        "responses": {
          "200": {
            "description": "The account's pacts, newest first: `rows` carries the fifty most recent, and `total` is how many the account holds in all — a caller that read the window as the total would believe a ledger with sixty pacts held fifty. The shape is the owner's page's. The terms are not here: a list a script pages through is not where the wording of every agreement is published, and `GET /_/api/v1/pacts/{id}` returns the text of one.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "total",
                    "rows"
                  ],
                  "properties": {
                    "total": {
                      "type": "integer",
                      "minimum": 0
                    },
                    "rows": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "kind",
                          "status",
                          "created_at",
                          "signable"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "kind": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          },
                          "created_at": {
                            "type": "integer"
                          },
                          "signable": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "required": [
                                "name",
                                "signed"
                              ],
                              "properties": {
                                "name": {
                                  "type": "string"
                                },
                                "signed": {
                                  "type": "boolean"
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Nobody was named, so there is no account whose pacts could be listed.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The door named a person it could not place in an account, or this key is not granted this app. Nothing was read.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "This key has spent its ceiling of 120 requests in the current minute; `retry-after` says when to try again.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "This app failed while answering",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "The door did not answer, so the credential could not be checked and nothing was read.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/_/api/v1/pacts/{id}": {
      "get": {
        "operationId": "getPactsById",
        "summary": "One pact at its current version: the terms, the parties, and that version's obligations.",
        "tags": [
          "pacts"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "the pact to read, as `pact.id` spells it",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The pact as it now stands. `current_version` is derived from the highest version present, so it is the text a reader would sign next.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "current_version",
                    "pact",
                    "version",
                    "parties",
                    "obligations"
                  ],
                  "properties": {
                    "current_version": {
                      "type": "integer",
                      "minimum": 1
                    },
                    "pact": {
                      "type": "object"
                    },
                    "version": {
                      "type": "object"
                    },
                    "parties": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "obligations": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Nobody was named, so there is no account whose pacts could be read.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The door named a person it could not place in an account, or this key is not granted this app. Nothing was read.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No pact in this account holds that id. Another account's pact reads the same way on purpose: a pact this account cannot read is a pact it must not learn exists.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "This key has spent its ceiling of 120 requests in the current minute; `retry-after` says when to try again.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "This app failed while answering",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "The door did not answer, so the credential could not be checked and nothing was read.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/_/api/v1/pacts/{id}/sign": {
      "post": {
        "operationId": "postPactsByIdSign",
        "summary": "Sign the version you were shown, as the party you are.",
        "tags": [
          "signatures"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "the pact to sign, as `pact.id` spells it",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "One signature row and its evidence record. The row is keyed by pact, version and party, so a second mark from one party on one version is a 409 rather than a second row.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "signature",
                    "evidence"
                  ],
                  "properties": {
                    "signature": {
                      "type": "object"
                    },
                    "evidence": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request does not carry what a signature must record: a version, one of the five methods, the consent text's version, a user agent, and a session to have come through.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "Nobody was named, or the caller is a key. A signature is made by a person, and a key names an account (FR-021).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The caller is not a party to this pact, or is the party that wrote it: nobody signs the terms they wrote.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No pact in this account holds that id.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "409": {
            "description": "That party already signed that version, or the version the caller was shown is no longer current. Both are a conflict with a row that exists, and neither writes anything.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/_/api/v1/pacts/{id}/evidence": {
      "get": {
        "operationId": "getPactsByIdEvidence",
        "summary": "Everything a stranger needs to check one pact, and the sentences that say what it does not prove.",
        "tags": [
          "evidence"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "the pact to prove, as `pact.id` spells it",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Every version with its hash, the parties, the obligations, the signatures with their tiers, the renditions with their digests, the one history, and the tier the weakest current signature supports.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "schema",
                    "pact",
                    "current_version",
                    "versions",
                    "signatures",
                    "renditions",
                    "events",
                    "tier",
                    "claims"
                  ],
                  "properties": {
                    "schema": {
                      "type": "string",
                      "enum": [
                        "pact.evidence/1"
                      ]
                    },
                    "pact": {
                      "type": "object"
                    },
                    "current_version": {
                      "type": "integer",
                      "minimum": 1
                    },
                    "versions": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "signatures": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "renditions": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "events": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "tier": {
                      "type": "string",
                      "enum": [
                        "L0",
                        "L1",
                        "L2",
                        "L3",
                        "L4",
                        "L5"
                      ]
                    },
                    "claims": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Nobody was named, so there is no account whose bundle could be read.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "The door named a person it could not place in an account, or this key is not granted this app. Nothing was read.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No pact in this account holds that id. An empty bundle would read as \"nothing was agreed\", which is a worse answer than \"not here\".",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "This key has spent its ceiling of 120 requests in the current minute; `retry-after` says when to try again.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "This app failed while answering",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "The door did not answer, so the credential could not be checked and nothing was read.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    },
    "/_/api/v1/pacts/{id}/invite": {
      "post": {
        "operationId": "postPactsByIdInvite",
        "summary": "Mint one link that lets the person it is sent to read this pact and sign it, with no account.",
        "tags": [
          "pacts",
          "links"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "description": "The seat the link signs. No body at all is the common call, and it means the counterparty's seat.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "partyId": {
                    "type": "string",
                    "description": "The `partyId` of the seat this link signs, when the pact carries more than one. Omitted, the counterparty's seat is used. The issuer's seat is refused: a link signs the side that did not write the terms."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "A link naming the seat it signs. The token in it exists in this response and nowhere else: it is stored only as a sha256, so a link cannot be re-read or re-sent later.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "url",
                    "partyId",
                    "expiresAt"
                  ],
                  "properties": {
                    "url": {
                      "type": "string",
                      "description": "The address to send. It carries the token."
                    },
                    "partyId": {
                      "type": "string"
                    },
                    "expiresAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request could not be read",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "401": {
            "description": "The credential is missing or does not open this account",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "403": {
            "description": "This account may not do that",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No pact with that id for this caller, or no seat a link may sign: the issuer's seat is refused, and a pact with no counterparty has nobody to send a link to.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key minted on the account's settings page at zpe.vc/home/settings. Send it as `Authorization: Bearer <key>`."
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem+json: `type`, `title`, `status`, `code` and `instance`, plus `detail` when a person should read a sentence.",
        "required": [
          "type",
          "title",
          "status",
          "code",
          "instance"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "code": {
            "type": "string",
            "enum": [
              "bad-request",
              "unauthenticated",
              "malformed",
              "unknown",
              "mismatch",
              "revoked",
              "expired",
              "another-account",
              "scope-refused",
              "grant-refused",
              "forbidden",
              "not-found",
              "method-not-allowed",
              "conflict",
              "gone",
              "payload-too-large",
              "unsupported-media-type",
              "unprocessable",
              "rate-limited",
              "payment-required",
              "not-open",
              "internal",
              "unavailable"
            ]
          },
          "instance": {
            "type": "string"
          },
          "detail": {
            "type": "string"
          }
        }
      }
    }
  },
  "x-surface": {
    "app": "pact",
    "base": "/_/api/v1",
    "document": "/_/api/v1/openapi.json",
    "generated_by": "zpe-kit machineDocument"
  },
  "x-zpe-routes": 6,
  "x-zpe-documented": 6,
  "x-zpe-internal": []
}
