{
  "openapi": "3.1.0",
  "info": {
    "title": "Terminal API",
    "version": "v1",
    "description": "For terminal vendors building the POS app. A paired terminal calls these on the terminal host, `https://terminal.api.podcredits.com`, over mutual TLS with its own client certificate; pairing is the one call made before it has one. The guides explain [pairing](https://docs.podcredits.com/terminal/guides/pairing-a-terminal/), [terminal sessions](https://docs.podcredits.com/terminal/guides/terminal-sessions/), [renewing a certificate](https://docs.podcredits.com/terminal/guides/renewing-a-certificate/) and [recovering from errors](https://docs.podcredits.com/terminal/guides/recovering-from-errors/)."
  },
  "servers": [
    {
      "url": "https://terminal.api.podcredits.com",
      "description": "Your platform's terminal host, over mutual TLS"
    }
  ],
  "security": [
    {
      "TerminalCertificate": []
    }
  ],
  "tags": [
    {
      "name": "Pairing a terminal",
      "description": "How a terminal exchanges a Pairing Code for its client certificate, and what makes a call refused; [Pairing a terminal](https://docs.podcredits.com/terminal/guides/pairing-a-terminal/) explains it."
    },
    {
      "name": "Cash payments",
      "description": "Cash a terminal takes at the counter, recorded as a payment; [Accepting cash](https://docs.podcredits.com/merchant/guides/accepting-cash/) tells how it is accepted, deposited and settled."
    },
    {
      "name": "Renewing a certificate",
      "description": "How a paired terminal gets a new certificate over its current one, for the same key; [Renewing a certificate](https://docs.podcredits.com/terminal/guides/renewing-a-certificate/) explains it."
    },
    {
      "name": "Terminal sessions",
      "description": "A cashier's working period on one terminal, opened, kept alive, resumed and closed by the terminal itself; [Terminal sessions](https://docs.podcredits.com/terminal/guides/terminal-sessions/) explains it."
    }
  ],
  "paths": {
    "/v1/terminal_pairings": {
      "post": {
        "tags": [
          "Pairing a terminal"
        ],
        "operationId": "createTerminalPairing",
        "summary": "Pair a terminal",
        "description": "Called by the terminal itself, with no API key or Access Token, to exchange its Pairing Code\nfor its client certificate. The key must be generated in the terminal's hardware-backed\nAndroid Keystore with the code's digest as its attestation challenge: see\n[Pairing a terminal](https://docs.podcredits.com/terminal/guides/pairing-a-terminal/) for how.\n\nEvery refusal is the same `401 pairing_refused`, whichever check failed. Requests are\nlimited per address. A retry with the same code and key, after a lost answer, returns the\nsame certificate.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "serial",
                  "pairing_code",
                  "csr",
                  "attestation_chain"
                ],
                "properties": {
                  "serial": {
                    "type": "string",
                    "description": "The terminal's serial number, as the device reports it.",
                    "examples": [
                      "TX00000001"
                    ]
                  },
                  "pairing_code": {
                    "type": "string",
                    "description": "The Pairing Code the operations team issued for this terminal. Case, dashes and spaces\nmake no difference.\n",
                    "examples": [
                      "ABCDE-FGH12"
                    ]
                  },
                  "csr": {
                    "type": "string",
                    "description": "A PKCS#10 certificate signing request in PEM, signed by the attested key. Its\nsubject is ignored.\n"
                  },
                  "attestation_chain": {
                    "type": "array",
                    "minItems": 1,
                    "description": "The key's attestation certificate chain from the Android Keystore, leaf first,\neach certificate as standard base64 of its DER. The root may be left off.\n",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The terminal's certificate. Sent with `Cache-Control: no-store`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TerminalPairing"
                }
              }
            }
          },
          "400": {
            "description": "`validation_failure` with `invalid_parameter`: a field is missing or not readable. `param` names a missing field, or is `request` when the CSR is not PEM or a chain entry is not base64. Also the codes any `/v1` call can get for a body that is not JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`authentication_failure` with `pairing_refused`: the same for every refusal, whichever check failed. See [Pairing a terminal](https://docs.podcredits.com/terminal/guides/pairing-a-terminal/) for what is checked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "type": "authentication_failure",
                    "code": "pairing_refused",
                    "message": "The pairing was refused. Ask for a new Pairing Code if this one may be spent or expired.",
                    "param": null,
                    "request_id": "req_4f1c0b2e9a7d"
                  }
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: too many pairing requests from this address. Wait for `Retry-After` seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/PairingUnavailable"
          }
        }
      }
    },
    "/v1/terminal/me": {
      "get": {
        "tags": [
          "Pairing a terminal"
        ],
        "operationId": "retrieveTerminalSelf",
        "summary": "Retrieve the calling terminal",
        "description": "Called by a paired terminal, over mutual TLS with its certificate, to learn who that certificate\nsays it is: the terminal, and the merchant and store it is assigned to. The certificate is checked\non this call, so a revoked, replaced or expired one is refused here as on any terminal call.\n",
        "security": [
          {
            "TerminalCertificate": []
          }
        ],
        "responses": {
          "200": {
            "description": "The terminal. Sent with `Cache-Control: no-store`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TerminalSelf"
                }
              }
            }
          },
          "401": {
            "description": "`authentication_failure` with `terminal_credential_refused`: the certificate does not count, because it is unknown, was revoked or replaced, or has expired, or the terminal was moved to another store or merchant since it was paired. Pair the terminal again. See [Pairing a terminal](https://docs.podcredits.com/terminal/guides/pairing-a-terminal/#when-a-terminal-is-refused).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "type": "authentication_failure",
                    "code": "terminal_credential_refused",
                    "message": "This terminal's certificate is not honoured. Pair the terminal again with a new Pairing Code.",
                    "param": null,
                    "request_id": "req_7a2e91c04b5d"
                  }
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/DependencyFailure"
          }
        }
      }
    },
    "/v1/terminal/certificate_renewals": {
      "post": {
        "tags": [
          "Renewing a certificate"
        ],
        "operationId": "createTerminalCertificateRenewal",
        "summary": "Renew the terminal's certificate",
        "description": "Called by a paired terminal, over mutual TLS with its current certificate, for a new certificate\nfor the same key, with no Pairing Code. The current certificate is checked as on any terminal\ncall, so one that was revoked, replaced or has expired cannot be renewed: the terminal is paired\nagain.\n\nBoth certificates are honoured until the new one is first used; from then on only the new one is.\nA retry after a lost answer, with the old certificate and the same key, returns the same new\ncertificate. See [Renewing a certificate](https://docs.podcredits.com/terminal/guides/renewing-a-certificate/).\n",
        "security": [
          {
            "TerminalCertificate": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "csr"
                ],
                "properties": {
                  "csr": {
                    "type": "string",
                    "pattern": "\\S",
                    "description": "A PKCS#10 certificate signing request in PEM, signed by the key the terminal was\npaired with. Its subject is ignored. No attestation chain is needed.\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new certificate. Sent with `Cache-Control: no-store`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TerminalCertificateRenewal"
                },
                "example": {
                  "object": "terminal_certificate_renewal",
                  "livemode": true,
                  "terminal_id": "3f6c2a9e-8b41-4d7a-9e25-0c1b7d4f8a63",
                  "certificate": "-----BEGIN CERTIFICATE-----\nMIIB9TCCAZugAwIBAgIU...\n-----END CERTIFICATE-----\n",
                  "chain": "-----BEGIN CERTIFICATE-----\nMIIBvjCCAWSgAwIBAgIU...\n-----END CERTIFICATE-----\n",
                  "expires_at": 1791604200
                }
              }
            }
          },
          "400": {
            "description": "`validation_failure` with `invalid_parameter` and `param` `csr`: the CSR is missing, is not a PEM certificate signing request, or is not signed by the key this terminal was paired with. A new key needs a new Pairing Code. Also the codes any `/v1` call can get for a body that is not JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "type": "validation_failure",
                    "code": "invalid_parameter",
                    "message": "csr must be signed by the key this terminal was paired with. A new key needs a new Pairing Code.",
                    "param": "csr",
                    "request_id": "req_2b8d4f6a1c3e"
                  }
                }
              }
            }
          },
          "401": {
            "description": "`authentication_failure` with `terminal_credential_refused`: the current certificate does not count, so it cannot be renewed. Pair the terminal again. See [Pairing a terminal](https://docs.podcredits.com/terminal/guides/pairing-a-terminal/#when-a-terminal-is-refused).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/RenewalUnavailable"
          }
        }
      }
    },
    "/v1/terminal/cash_payments": {
      "post": {
        "tags": [
          "Cash payments"
        ],
        "operationId": "createTerminalCashPayment",
        "summary": "Record a cash payment",
        "description": "Called by a paired terminal, over mutual TLS with its certificate, to record cash it took at the\ncounter. The merchant and store are the terminal's assignment, never the body's. The payment is\nalready made, so it is recorded as a `succeeded` payment intent on the terminal's channel, with the\nreceipt data to print and the change due. As with every cash payment it is accepted, not yet\ndeposited: see [Accepting cash](https://docs.podcredits.com/merchant/guides/accepting-cash/).\n\nThe terminal's own `idempotency_key` makes a retry safe. The same key with the same terms answers\nthe same payment with `200` and records nothing twice; the same key with other terms is refused with\n`409`. Use a new key for each sale.\n",
        "security": [
          {
            "TerminalCertificate": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "idempotency_key",
                  "amount",
                  "tendered",
                  "currency"
                ],
                "properties": {
                  "idempotency_key": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "The terminal's key for this sale.",
                    "examples": [
                      "sale-2026-10-08-0042"
                    ]
                  },
                  "amount": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "The price, in the currency's minor unit.",
                    "examples": [
                      25000
                    ]
                  },
                  "tendered": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "The cash handed over. At least `amount`.",
                    "examples": [
                      30000
                    ]
                  },
                  "currency": {
                    "type": "string",
                    "pattern": "^[A-Za-z]{3}$",
                    "description": "A three-letter ISO 4217 code, in either case.",
                    "examples": [
                      "php"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A sale already recorded under this key with the same terms, answered again. Also the answer to a retry after a `503` when the first call had recorded the sale, even when no answer was ever received for it. The same body as `201`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TerminalCashPayment"
                }
              }
            }
          },
          "201": {
            "description": "The payment was recorded. Sent with the receipt data.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TerminalCashPayment"
                },
                "example": {
                  "object": "terminal_cash_payment",
                  "livemode": true,
                  "payment_intent": "pi_01j9x4c7n2p5r8t1v3w6y9z0b2",
                  "status": "succeeded",
                  "amount": 25000,
                  "tendered": 30000,
                  "change_due": 5000,
                  "currency": "php",
                  "receipt": {
                    "description": "Cash accepted",
                    "reference": "pi_01j9x4c7n2p5r8t1v3w6y9z0b2",
                    "amount": 25000,
                    "tendered": 30000,
                    "change_due": 5000,
                    "currency": "php",
                    "store_label": "Makati branch"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationFailure"
          },
          "401": {
            "description": "`authentication_failure` with `terminal_credential_refused`: the certificate does not count. Pair the terminal again. See [Pairing a terminal](https://docs.podcredits.com/terminal/guides/pairing-a-terminal/#when-a-terminal-is-refused).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`conflict` with `idempotency_key_reused`: this `idempotency_key` recorded a different sale. Use a new key for a new sale.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/PaymentRefused"
          },
          "503": {
            "$ref": "#/components/responses/CashPaymentUnavailable"
          }
        }
      }
    },
    "/v1/terminal_sessions": {
      "post": {
        "tags": [
          "Terminal sessions"
        ],
        "operationId": "createTerminalSession",
        "summary": "Open a terminal session",
        "description": "Called by a paired terminal, over mutual TLS with its certificate, when a cashier starts work.\nThe merchant and store are the terminal's assignment, never the body's. A terminal has one\nsession at a time: while one is `active` or `recoverable_interrupted`, opening another is\nrefused.\n\nThe `client_session_token` makes a retry safe: the same token answers the same session, as it\nstands now, with `201` again, without checking the clock: read its `status` and\n`recovery_deadline`, and resume it or open a new session as the guide says. A token first sent\nwith an earlier certificate is refused with `409`. Use a new token for each session. See\n[Terminal sessions](https://docs.podcredits.com/terminal/guides/terminal-sessions/).\n",
        "security": [
          {
            "TerminalCertificate": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "client_session_token"
                ],
                "properties": {
                  "client_session_token": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 64,
                    "pattern": "\\S",
                    "description": "The terminal's token for this session, new for each session. Not blank.",
                    "examples": [
                      "shift-2026-10-08-0001"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The session, open, and what the terminal needs to work. Sent with `Cache-Control: no-store`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TerminalSessionOpened"
                },
                "example": {
                  "id": "6d0f3b8e-2c4a-4f7e-9b1d-8a5c3e7f2b10",
                  "object": "terminal_session",
                  "livemode": true,
                  "terminal_id": "3f6c2a9e-8b41-4d7a-9e25-0c1b7d4f8a63",
                  "status": "active",
                  "opened_at": 1791000000,
                  "last_heartbeat_at": 1791000000,
                  "interrupted_at": null,
                  "recovery_deadline": null,
                  "expires_at": 1791043200,
                  "expired_at": null,
                  "closed_at": null,
                  "heartbeat_interval_seconds": 30,
                  "tids": [
                    {
                      "route": "card_processor",
                      "tid": "T0001"
                    },
                    {
                      "route": "domestic_debit",
                      "tid": "T0002"
                    }
                  ],
                  "store_label": "Front counter",
                  "parameter_profile": null
                }
              }
            }
          },
          "400": {
            "description": "`validation_failure` with `invalid_parameter` and `param` `client_session_token`: the token is missing, not a string, blank or longer than 64 characters. Also the codes any `/v1` call can get for a body that is not JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SessionCallRefused"
          },
          "409": {
            "description": "`conflict` with `session_already_open`, for either of two reasons. This terminal already has a session that is `active` or `recoverable_interrupted`: resume or close it first (a session that time has already ended does not count). Or this `client_session_token` was first sent with an earlier certificate, before a renewal: it is refused even when that session is now closed or expired. Resume the session it opened if it is still live, or open with a new token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "type": "conflict",
                    "code": "session_already_open",
                    "message": "This terminal already has a session open. Resume or close it first.",
                    "param": null,
                    "request_id": "req_9c1e5a7b3d2f"
                  }
                }
              }
            }
          },
          "422": {
            "description": "`policy_rejection`: no session can be opened now. `code` says why:\n\n| `code` | Meaning |\n|---|---|\n| `merchant_not_active` | The terminal's merchant is suspended or closed. |\n| `tenant_not_active` | The merchant's tenant is suspended or closed. |\n| `terminal_not_assigned` | The terminal is not assigned to a store. |\n| `terminal_credential_refused` | The terminal's credential stopped counting while the session was being opened. Treat it as the `401`. |\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/OpenUnavailable"
          }
        }
      }
    },
    "/v1/terminal_sessions/{id}/heartbeat": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "post": {
        "tags": [
          "Terminal sessions"
        ],
        "operationId": "heartbeatTerminalSession",
        "summary": "Send a heartbeat",
        "description": "Called by the terminal every `heartbeat_interval_seconds` to say it is alive. Moves the session's\n`last_heartbeat_at` to now and changes nothing else, so it is safe to repeat. Takes no body.\n\nA session that missed its heartbeats is `recoverable_interrupted`, and a heartbeat does not bring\nit back: resume it. A session is interrupted once 3 heartbeat intervals (90 seconds by default)\nhave passed after its last heartbeat, so a heartbeat that arrives only at the third interval is\nalready refused. Time is checked on every heartbeat, resume and close, whether or not the platform's\ntimer has run.\n",
        "security": [
          {
            "TerminalCertificate": []
          }
        ],
        "responses": {
          "200": {
            "description": "The session, still `active`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TerminalSession"
                },
                "example": {
                  "id": "6d0f3b8e-2c4a-4f7e-9b1d-8a5c3e7f2b10",
                  "object": "terminal_session",
                  "livemode": true,
                  "terminal_id": "3f6c2a9e-8b41-4d7a-9e25-0c1b7d4f8a63",
                  "status": "active",
                  "opened_at": 1791000000,
                  "last_heartbeat_at": 1791000030,
                  "interrupted_at": null,
                  "recovery_deadline": null,
                  "expires_at": 1791043200,
                  "expired_at": null,
                  "closed_at": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SessionCallRefused"
          },
          "404": {
            "$ref": "#/components/responses/SessionNotFound"
          },
          "422": {
            "description": "`invariant_violation`: the session takes no heartbeat now. `code` says where it is:\n\n| `code` | Meaning |\n|---|---|\n| `session_interrupted` | It missed its heartbeats. Resume it. |\n| `session_expired` | It was not resumed in time, or reached its longest lifetime. Open a new one. |\n| `session_closed` | It is closed. Open a new one. |\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "type": "invariant_violation",
                    "code": "session_interrupted",
                    "message": "This session was interrupted. Resume it.",
                    "param": null,
                    "request_id": "req_5e3a9c1d7b4f"
                  }
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SessionUnavailable"
          }
        }
      }
    },
    "/v1/terminal_sessions/{id}/resume": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "post": {
        "tags": [
          "Terminal sessions"
        ],
        "operationId": "resumeTerminalSession",
        "summary": "Resume a terminal session",
        "description": "Called by the terminal to pick an interrupted session back up, before its `recovery_deadline`.\nThe session is `active` again and its last heartbeat moves to now. Resuming an `active` session\nanswers it as it is, so it is safe to repeat. Takes no body.\n\nPast the deadline the session has expired, and the terminal opens a new one.\n",
        "security": [
          {
            "TerminalCertificate": []
          }
        ],
        "responses": {
          "200": {
            "description": "The session, `active`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TerminalSession"
                },
                "example": {
                  "id": "6d0f3b8e-2c4a-4f7e-9b1d-8a5c3e7f2b10",
                  "object": "terminal_session",
                  "livemode": true,
                  "terminal_id": "3f6c2a9e-8b41-4d7a-9e25-0c1b7d4f8a63",
                  "status": "active",
                  "opened_at": 1791000000,
                  "last_heartbeat_at": 1791000200,
                  "interrupted_at": null,
                  "recovery_deadline": null,
                  "expires_at": 1791043200,
                  "expired_at": null,
                  "closed_at": null
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SessionCallRefused"
          },
          "404": {
            "$ref": "#/components/responses/SessionNotFound"
          },
          "422": {
            "description": "`invariant_violation`: the session cannot be resumed. `code` says where it is:\n\n| `code` | Meaning |\n|---|---|\n| `session_expired` | Its recovery window ran out, or it reached its longest lifetime. Open a new one. |\n| `session_closed` | It is closed. Open a new one. |\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "type": "invariant_violation",
                    "code": "session_expired",
                    "message": "This session has expired. Open a new one.",
                    "param": null,
                    "request_id": "req_8d2f6b4a9c1e"
                  }
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SessionUnavailable"
          }
        }
      }
    },
    "/v1/terminal_sessions/{id}/close": {
      "parameters": [
        {
          "$ref": "#/components/parameters/Id"
        }
      ],
      "post": {
        "tags": [
          "Terminal sessions"
        ],
        "operationId": "closeTerminalSession",
        "summary": "Close a terminal session",
        "description": "Called by the terminal when the cashier stops work. An `active` or `recoverable_interrupted`\nsession closes. Closing a closed session answers it unchanged, so it is safe to repeat. Takes\nno body.\n",
        "security": [
          {
            "TerminalCertificate": []
          }
        ],
        "responses": {
          "200": {
            "description": "The session, `closed`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TerminalSession"
                },
                "example": {
                  "id": "6d0f3b8e-2c4a-4f7e-9b1d-8a5c3e7f2b10",
                  "object": "terminal_session",
                  "livemode": true,
                  "terminal_id": "3f6c2a9e-8b41-4d7a-9e25-0c1b7d4f8a63",
                  "status": "closed",
                  "opened_at": 1791000000,
                  "last_heartbeat_at": 1791028800,
                  "interrupted_at": null,
                  "recovery_deadline": null,
                  "expires_at": 1791043200,
                  "expired_at": null,
                  "closed_at": 1791028815
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SessionCallRefused"
          },
          "404": {
            "$ref": "#/components/responses/SessionNotFound"
          },
          "422": {
            "description": "`invariant_violation` with `session_expired`: the session has already expired, so there is nothing to close.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/SessionUnavailable"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "TerminalCertificate": {
        "type": "mutualTLS",
        "description": "The terminal's own client certificate, from `POST /v1/terminal_pairings`, whose private key\nnever leaves the terminal, presented over mutual TLS. Used by terminals only, on every call but\npairing. It is checked on every call, so a revoked one is refused on the very next.\n"
      }
    },
    "responses": {
      "PairingUnavailable": {
        "description": "`dependency_failure` with `service_unavailable`: the platform did not answer, or failed part-way. Whether the terminal was paired is not known. Retry the same request: the same code and key return the same certificate if it was issued, so never make a new key for the retry.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RenewalUnavailable": {
        "description": "`dependency_failure` with `service_unavailable`: the platform did not answer, or failed part-way. Whether a new certificate was made is not known. Retry the same request, with the old certificate and the same key: if one was made you get that one back, not a second, and the old certificate stays honoured until the new one is first used.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "OpenUnavailable": {
        "description": "`dependency_failure` with `service_unavailable`: the platform did not answer, or failed part-way. Whether the session was opened is not known. Retry the same request with the same `client_session_token`: a session that was opened is answered, not opened twice. Never make a new token for the retry.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "CashPaymentUnavailable": {
        "description": "`dependency_failure` with `service_unavailable`: the platform did not answer, or failed part-way. Whether the sale was recorded is not known; the cash is in the till either way. Retry the same request with the same `idempotency_key`, and never make a new key for the retry: if the sale was recorded, in whole or in part, you are answered with that payment, with `200`, and it is recorded once.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "SessionUnavailable": {
        "description": "`dependency_failure` with `service_unavailable`: the platform did not answer, or failed part-way. Whether the call took effect is not known. Retry the same request: a heartbeat is safe to repeat, and if a retry is refused because the session is already as the call would leave it (for example `session_closed` after a close), the first call took effect.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "SessionCallRefused": {
        "description": "`authentication_failure` with `terminal_credential_refused`: the certificate does not count, or the terminal is suspended or retired. See [Pairing a terminal](https://docs.podcredits.com/terminal/guides/pairing-a-terminal/#when-a-terminal-is-refused).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "SessionNotFound": {
        "description": "`not_found` with `resource_missing`: this terminal has no session with this id. Another terminal's session reads the same.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PaymentRefused": {
        "description": "`policy_rejection`: this payment cannot be taken now. `code` says why:\n\n| `code` | Meaning |\n|---|---|\n| `account_inactive` | Your account is not active. |\n| `not_permitted_for_account` | Your account may not use this method yet. |\n| `method_not_enabled` | The method is not enabled for your account. |\n| `method_not_set_up` | The method is enabled but its setup is not finished. |\n| `method_suspended` | The method is suspended for your account. |\n| `pricing_unavailable` | No price is set for this method and amount. |\n| `payment_method_unavailable` | The method cannot take this payment on this channel yet. |\n| `no_payment_method_available` | None of your methods can take this payment now. |\n| `test_mode_unavailable` | Confirming only: this method cannot be simulated in test mode. |\n| `limit_exceeded` | Confirming only: this payment would take your account past its limit. |\n| `restricted` | Confirming only: payments are restricted for your account, store or terminal at the moment. |\n| `risk_declined` | Confirming only: this payment was declined by a risk check. |\n| `not_available` | Unavailable for another reason. |\n",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "type": "policy_rejection",
                "code": "method_suspended",
                "message": "This payment method is suspended for your account.",
                "param": null,
                "request_id": "req_4f1c0b2e9a7d"
              }
            }
          }
        }
      },
      "ValidationFailure": {
        "description": "`validation_failure`: `invalid_parameter` for a parameter missing or not valid, named in `param`; `invalid_json` for a body that is not JSON; `unsupported_media_type` for a body that is not sent as JSON; `not_acceptable` when the request will not accept JSON; `method_not_allowed` for an HTTP method the path does not take.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "type": "validation_failure",
                "code": "invalid_parameter",
                "message": "A whole number of minor units, from 1 to 999999999999999999.",
                "param": "amount",
                "request_id": "req_4f1c0b2e9a7d"
              }
            }
          }
        }
      },
      "DependencyFailure": {
        "description": "`dependency_failure` with `service_unavailable`: something on our side, or a payment network behind it, did not answer. This call only reads, so nothing was changed; retry.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "TerminalCashPayment": {
        "type": "object",
        "required": [
          "object",
          "livemode",
          "payment_intent",
          "status",
          "amount",
          "tendered",
          "change_due",
          "currency",
          "receipt"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "terminal_cash_payment"
          },
          "livemode": {
            "type": "boolean"
          },
          "payment_intent": {
            "type": "string",
            "description": "The id of the payment intent made for this payment."
          },
          "status": {
            "type": "string",
            "description": "`succeeded`: the cash was accepted.",
            "examples": [
              "succeeded"
            ]
          },
          "amount": {
            "type": "integer",
            "description": "The price, in the currency's minor unit."
          },
          "tendered": {
            "type": "integer",
            "description": "The cash the customer handed over."
          },
          "change_due": {
            "type": "integer",
            "description": "`tendered` less `amount`: the change to give back."
          },
          "currency": {
            "$ref": "#/components/schemas/Currency"
          },
          "receipt": {
            "type": "object",
            "description": "What the terminal prints.",
            "required": [
              "description",
              "reference",
              "amount",
              "tendered",
              "change_due",
              "currency",
              "store_label"
            ],
            "properties": {
              "description": {
                "type": "string",
                "examples": [
                  "Cash accepted"
                ]
              },
              "reference": {
                "type": "string",
                "description": "The payment intent's id."
              },
              "amount": {
                "type": "integer"
              },
              "tendered": {
                "type": "integer"
              },
              "change_due": {
                "type": "integer"
              },
              "currency": {
                "$ref": "#/components/schemas/Currency"
              },
              "store_label": {
                "type": "string",
                "description": "The store the terminal is assigned to."
              }
            }
          }
        }
      },
      "TerminalPairing": {
        "type": "object",
        "required": [
          "object",
          "livemode",
          "terminal_id",
          "certificate",
          "chain",
          "expires_at"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "terminal_pairing"
          },
          "terminal_id": {
            "type": "string",
            "description": "The id of the terminal the certificate is for.",
            "examples": [
              "3f6c2a9e-8b41-4d7a-9e25-0c1b7d4f8a63"
            ]
          },
          "certificate": {
            "type": "string",
            "description": "The terminal's client certificate, in PEM, for its key. Presented over mutual TLS on\nthe terminal's later calls. Valid for up to 7 days: `expires_at` says when it ends.\n"
          },
          "chain": {
            "type": "string",
            "description": "The certificates above it, in PEM, its issuer first."
          },
          "expires_at": {
            "type": "integer",
            "description": "When the certificate expires, as a Unix timestamp.",
            "examples": [
              1790999400
            ]
          },
          "livemode": {
            "type": "boolean",
            "const": true,
            "description": "Always `true`. A terminal is a live device; there are no test terminals."
          }
        }
      },
      "TerminalSelf": {
        "type": "object",
        "properties": {
          "object": {
            "type": "string",
            "const": "terminal"
          },
          "id": {
            "type": "string",
            "description": "The terminal's id, as its pairing answered it.",
            "examples": [
              "3f6c2a9e-8b41-4d7a-9e25-0c1b7d4f8a63"
            ]
          },
          "tenant_id": {
            "type": "string",
            "description": "The tenant of the merchant the terminal is assigned to."
          },
          "merchant_id": {
            "type": "string",
            "description": "The merchant the terminal is assigned to."
          },
          "store_label": {
            "type": "string",
            "description": "The store it is assigned to, as the operations team labelled it. Empty when none was given.",
            "examples": [
              "Front counter"
            ]
          },
          "certificate": {
            "type": "object",
            "description": "The certificate this call presented.",
            "properties": {
              "serial_number": {
                "type": "string",
                "description": "Its serial number, in lower case hex.",
                "examples": [
                  "5e1f0c9a3b7d2e48"
                ]
              },
              "expires_at": {
                "type": "integer",
                "description": "When it expires, as a Unix timestamp.",
                "examples": [
                  1790999400
                ]
              }
            }
          },
          "livemode": {
            "type": "boolean",
            "const": true,
            "description": "Always `true`. A terminal is a live device; there are no test terminals."
          }
        }
      },
      "TerminalCertificateRenewal": {
        "type": "object",
        "required": [
          "object",
          "livemode",
          "terminal_id",
          "certificate",
          "chain",
          "expires_at"
        ],
        "properties": {
          "object": {
            "type": "string",
            "const": "terminal_certificate_renewal"
          },
          "terminal_id": {
            "type": "string",
            "description": "The id of the terminal the certificate is for, the one the current certificate names.",
            "examples": [
              "3f6c2a9e-8b41-4d7a-9e25-0c1b7d4f8a63"
            ]
          },
          "certificate": {
            "type": "string",
            "description": "The terminal's new client certificate, in PEM, for the same key. Valid for up to 7 days:\n`expires_at` says when it ends. Once it is first used, the certificate it replaces is refused.\n"
          },
          "chain": {
            "type": "string",
            "description": "The certificates above it, in PEM, its issuer first."
          },
          "expires_at": {
            "type": "integer",
            "description": "When the new certificate expires, as a Unix timestamp.",
            "examples": [
              1791604200
            ]
          },
          "livemode": {
            "type": "boolean",
            "const": true,
            "description": "Always `true`. A terminal is a live device; there are no test terminals."
          }
        }
      },
      "TerminalSession": {
        "type": "object",
        "required": [
          "id",
          "object",
          "livemode",
          "terminal_id",
          "status",
          "opened_at",
          "last_heartbeat_at",
          "interrupted_at",
          "recovery_deadline",
          "expires_at",
          "expired_at",
          "closed_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "The session's id, used in the path of its heartbeat, resume and close.",
            "examples": [
              "6d0f3b8e-2c4a-4f7e-9b1d-8a5c3e7f2b10"
            ]
          },
          "object": {
            "type": "string",
            "const": "terminal_session"
          },
          "livemode": {
            "type": "boolean",
            "const": true,
            "description": "Always `true`. A terminal is a live device; there are no test terminals."
          },
          "terminal_id": {
            "type": "string",
            "description": "The terminal that opened it, the only one that can use it.",
            "examples": [
              "3f6c2a9e-8b41-4d7a-9e25-0c1b7d4f8a63"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "recoverable_interrupted",
              "expired",
              "closed"
            ],
            "description": "`active` while it takes heartbeats; `recoverable_interrupted` once it missed them, until it is\nresumed or its `recovery_deadline` passes; `expired` and `closed` are final.\n"
          },
          "opened_at": {
            "type": "integer",
            "description": "When it was opened, as a Unix timestamp."
          },
          "last_heartbeat_at": {
            "type": "integer",
            "description": "The last time the terminal showed it was alive on it, by opening, a heartbeat or a resume, as a Unix timestamp."
          },
          "interrupted_at": {
            "type": [
              "integer",
              "null"
            ],
            "description": "When it was interrupted for missing its heartbeats, while it is `recoverable_interrupted`."
          },
          "recovery_deadline": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The last moment it can be resumed, while it is `recoverable_interrupted`."
          },
          "expires_at": {
            "type": "integer",
            "description": "When it ends, however well it heartbeats, as a Unix timestamp."
          },
          "expired_at": {
            "type": [
              "integer",
              "null"
            ],
            "description": "When it expired, once it is `expired`."
          },
          "closed_at": {
            "type": [
              "integer",
              "null"
            ],
            "description": "When it was closed, once it is `closed`."
          }
        }
      },
      "TerminalSessionOpened": {
        "description": "The session, as opening it answers, with what the terminal needs to start work.",
        "allOf": [
          {
            "$ref": "#/components/schemas/TerminalSession"
          },
          {
            "type": "object",
            "required": [
              "heartbeat_interval_seconds",
              "tids",
              "store_label",
              "parameter_profile"
            ],
            "properties": {
              "heartbeat_interval_seconds": {
                "type": "integer",
                "description": "How often to send a heartbeat, in seconds. The platform sets it.",
                "examples": [
                  30
                ]
              },
              "tids": {
                "type": "array",
                "description": "The terminal IDs bound to the terminal now, one for each card route it has one for.",
                "items": {
                  "type": "object",
                  "required": [
                    "route",
                    "tid"
                  ],
                  "properties": {
                    "route": {
                      "type": "string",
                      "examples": [
                        "card_processor"
                      ]
                    },
                    "tid": {
                      "type": "string",
                      "examples": [
                        "T0001"
                      ]
                    }
                  }
                }
              },
              "store_label": {
                "type": "string",
                "description": "The store the terminal is assigned to, as the operations team labelled it. Empty when none was given.",
                "examples": [
                  "Front counter"
                ]
              },
              "parameter_profile": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "The newest parameter profile a rollout completed for this terminal, or `null` until one has.",
                "required": [
                  "name",
                  "version",
                  "parameters"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "version": {
                    "type": "integer"
                  },
                  "parameters": {
                    "type": "object",
                    "description": "The profile's parameters, names and values, as the operations team saved them."
                  }
                }
              }
            }
          }
        ]
      },
      "Currency": {
        "type": "string",
        "pattern": "^[a-z]{3}$",
        "description": "A three-letter ISO 4217 code, in lower case.",
        "examples": [
          "php"
        ]
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "type",
              "code",
              "message",
              "param",
              "request_id"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "validation_failure",
                  "authentication_failure",
                  "authorization_failure",
                  "not_found",
                  "conflict",
                  "timeout_or_in_progress",
                  "policy_rejection",
                  "invariant_violation",
                  "rate_limited",
                  "dependency_failure"
                ]
              },
              "code": {
                "type": "string",
                "description": "A stable name for this refusal. Branch on this, never on `message`."
              },
              "message": {
                "type": "string",
                "description": "For the developer reading a log. May be reworded."
              },
              "param": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The field or parameter at fault, when there is one."
              },
              "request_id": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          }
        }
      }
    },
    "parameters": {
      "Id": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The object's id.",
        "schema": {
          "type": "string"
        }
      }
    }
  }
}
