{
  "openapi": "3.1.0",
  "info": {
    "title": "PrivacyHood Merchant API",
    "version": "1.0.0",
    "summary": "Accept private payments: create a charge, get a signed webhook, pull a cryptographic receipt.",
    "description": "Private payments on Robinhood Chain (chainId 4663, USDG). Funds route through a shielded pool before reaching your payout address, so a customer's wallet is never linked on-chain to yours.\n\nAuthentication is a bearer API key (`phk_<keyId>.<hmac>`). The key is returned exactly once at creation and is never stored by PrivacyHood \u2014 only its id, so it can be revoked but never shown again. Keep it server-side.\n\nAll monetary amounts are strings. `amountBaseUnits` is an integer string in USDG base units (6 decimals): \"25000000\" is 25 USDG.\n\nThe payer covers the fee: an invoice for 25 USDG deposits 25 USDG to your payout address, and the customer is quoted amount+fee before they commit.",
    "contact": {
      "name": "PrivacyHood",
      "url": "https://www.privacyhood.org/docs"
    }
  },
  "servers": [
    {
      "url": "https://privacyhood-backend-843fl.ondigitalocean.app"
    }
  ],
  "tags": [
    {
      "name": "API keys",
      "description": "Durable merchant identity."
    },
    {
      "name": "Invoices",
      "description": "One-off charges."
    },
    {
      "name": "Subscriptions",
      "description": "Recurring charges (pull model \u2014 never auto-debits a customer)."
    },
    {
      "name": "Public",
      "description": "No authentication."
    }
  ],
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Your merchant API key: `authorization: Bearer phk_<keyId>.<hmac>`"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable reason."
          }
        },
        "required": [
          "error"
        ]
      },
      "InvoiceStatus": {
        "type": "string",
        "enum": [
          "open",
          "pending",
          "paid",
          "void"
        ],
        "description": "Computed on read from the paying transfer. `open` (unpaid or the previous attempt died) \u2192 `pending` (payment in flight) \u2192 `paid`. `void` if cancelled."
      },
      "Invoice": {
        "type": "object",
        "properties": {
          "invoiceId": {
            "type": "string",
            "examples": [
              "inv_e4bfe9f9-43a6-4713-93e1-ddfec3964a90"
            ]
          },
          "status": {
            "$ref": "#/components/schemas/InvoiceStatus"
          },
          "amountBaseUnits": {
            "type": "string",
            "examples": [
              "25000000"
            ]
          },
          "token": {
            "type": "string",
            "examples": [
              "USDG"
            ]
          },
          "recipient": {
            "type": "string",
            "description": "Merchant payout address (0x, EIP-55)."
          },
          "memo": {
            "type": [
              "string",
              "null"
            ]
          },
          "ref": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own order id, echoed in the webhook."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "deliveredTx": {
            "type": [
              "string",
              "null"
            ],
            "description": "Delivery transaction hash; present only once paid."
          },
          "test": {
            "type": "boolean",
            "description": "Sandbox invoice (test mode) \u2014 settled via simulate-pay, never real money."
          }
        },
        "required": [
          "invoiceId",
          "status",
          "amountBaseUnits",
          "token",
          "recipient",
          "createdAt"
        ]
      },
      "InvoiceCreated": {
        "type": "object",
        "description": "`manageToken` and `webhookSecret` are returned ONCE and never again.",
        "properties": {
          "invoiceId": {
            "type": "string"
          },
          "manageToken": {
            "type": "string",
            "description": "Per-invoice credential. Not needed if you use an API key."
          },
          "amountBaseUnits": {
            "type": "string"
          },
          "token": {
            "type": "string"
          },
          "recipient": {
            "type": "string"
          },
          "memo": {
            "type": [
              "string",
              "null"
            ]
          },
          "ref": {
            "type": [
              "string",
              "null"
            ]
          },
          "webhookUrl": {
            "type": [
              "string",
              "null"
            ]
          },
          "webhookSecret": {
            "type": [
              "string",
              "null"
            ],
            "description": "HMAC signing secret (`whsec_\u2026`). Shown once."
          }
        },
        "required": [
          "invoiceId",
          "manageToken",
          "amountBaseUnits",
          "token",
          "recipient"
        ]
      },
      "WebhookEvent": {
        "type": "object",
        "description": "POSTed to your `webhookUrl` after an invoice settles. Delivery is driven by a background cron (typically within about a minute of settlement), not synchronously with payment. Signed with `x-privacyhood-signature: sha256=<hmac of the RAW body under THAT INVOICE's webhookSecret>` \u2014 the secret is PER INVOICE, so look it up by `data.invoiceId`. Verify before trusting. At-least-once delivery: retried with backoff up to 6 attempts until you return 2xx, so make your handler idempotent on `data.invoiceId`.",
        "properties": {
          "id": {
            "type": "string",
            "examples": [
              "evt_9f2c4a1b8e7d6c5f4a3b2c1d"
            ]
          },
          "event": {
            "type": "string",
            "enum": [
              "invoice.paid"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "properties": {
              "invoiceId": {
                "type": "string"
              },
              "ref": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "memo": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "amountBaseUnits": {
                "type": "string"
              },
              "token": {
                "type": "string"
              },
              "recipient": {
                "type": "string"
              },
              "deliveredTx": {
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "invoiceId",
              "amountBaseUnits",
              "token",
              "recipient"
            ]
          }
        },
        "required": [
          "id",
          "event",
          "createdAt",
          "data"
        ]
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, malformed, revoked, or foreign API key. Also returned when a key acts on an invoice it does not own \u2014 deliberately identical, so nothing leaks about what exists.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Per-IP rate limit exceeded (~10/min for creates, ~120/min for reads).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/api-keys": {
      "post": {
        "tags": [
          "API keys"
        ],
        "summary": "Create an API key",
        "description": "Unauthenticated by design: a key grants nothing you cannot already do without one, it only groups invoices under an identity you hold. The key is shown once and never stored.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "label": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "For your own bookkeeping."
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "test"
                    ],
                    "description": "Omit for a live key. \"test\" mints a sandbox key (id tk_\u2026) whose invoices never move real money."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created. Store `apiKey` now \u2014 it cannot be retrieved later.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "keyId": {
                      "type": "string"
                    },
                    "apiKey": {
                      "type": "string"
                    },
                    "label": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "mode": {
                      "type": "string",
                      "enum": [
                        "live",
                        "test"
                      ]
                    }
                  },
                  "required": [
                    "keyId",
                    "apiKey"
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/api-keys/revoke": {
      "post": {
        "tags": [
          "API keys"
        ],
        "summary": "Revoke your API key",
        "description": "A key revokes itself. Immediate. Invoices already created stay payable \u2014 you just lose API access to them, so mint the replacement first.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "keyId": {
                      "type": "string"
                    },
                    "revoked": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/invoices": {
      "get": {
        "tags": [
          "Invoices"
        ],
        "summary": "List your invoices",
        "description": "Every invoice created with this key, newest first, with status computed live. Keys are isolated \u2014 another key returns an empty list.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "invoices": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Invoice"
                      }
                    },
                    "count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "tags": [
          "Invoices"
        ],
        "summary": "Create an invoice",
        "description": "Send the customer to `https://www.privacyhood.org/pay?invoice=<invoiceId>` to pay it. The API key is technically optional, but without it the invoice belongs to nobody and can never be listed, voided, or receipted again. Sending no key still returns 201, but the invoice is then reachable only via its manageToken \u2014 always send the key from a server.",
        "security": [
          {
            "apiKey": []
          },
          {}
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "recipient": {
                    "type": "string",
                    "description": "Your payout address (0x, EIP-55 checksummed)."
                  },
                  "amount": {
                    "type": "string",
                    "description": "Whole tokens as a string, e.g. \"25\". 1\u201310,000."
                  },
                  "amountBaseUnits": {
                    "type": "string",
                    "description": "Alternative to `amount`: integer string, 6 decimals."
                  },
                  "ref": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "Your order id. Echoed in the webhook."
                  },
                  "memo": {
                    "type": "string",
                    "maxLength": 280,
                    "description": "Shown to the payer at checkout."
                  },
                  "webhookUrl": {
                    "type": "string",
                    "format": "uri",
                    "description": "https:// and publicly resolvable only. Private/loopback/metadata hosts are rejected."
                  }
                },
                "required": [
                  "recipient"
                ],
                "anyOf": [
                  {
                    "required": [
                      "amount"
                    ]
                  },
                  {
                    "required": [
                      "amountBaseUnits"
                    ]
                  }
                ],
                "description": "Exactly one of `amount` or `amountBaseUnits` is required."
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceCreated"
                }
              }
            }
          },
          "400": {
            "description": "Invalid address, amount out of range, or unsafe webhook URL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/invoices/{invoiceId}": {
      "get": {
        "tags": [
          "Public"
        ],
        "summary": "Read an invoice",
        "description": "Public \u2014 this is what the checkout page reads. Exposes only what a payer needs. Ids are unguessable, so this is not enumerable.",
        "parameters": [
          {
            "name": "invoiceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "404": {
            "description": "No such invoice.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/invoices/{invoiceId}/receipt": {
      "get": {
        "tags": [
          "Invoices"
        ],
        "summary": "Get a signed receipt",
        "description": "A portable, protocol-signed proof of payment embedding the zero-knowledge clean-funds certificate. Paid invoices only. Contains post-privacy facts only \u2014 never the payer's wallet. Accepts either the creating API key or that invoice's manageToken.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "name": "invoiceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Signed receipt. Verify `signature` against `GET /certificate/signer`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "type": {
                      "type": "string",
                      "enum": [
                        "privacyhood-invoice-receipt"
                      ]
                    },
                    "version": {
                      "type": "integer"
                    },
                    "invoice": {
                      "type": "object"
                    },
                    "payment": {
                      "type": "object"
                    },
                    "certificate": {
                      "type": [
                        "object",
                        "null"
                      ]
                    },
                    "issuedAt": {
                      "type": "integer"
                    },
                    "signer": {
                      "type": "string"
                    },
                    "signature": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "Not paid yet \u2014 a receipt only exists after settlement.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/invoices/{invoiceId}/void": {
      "post": {
        "tags": [
          "Invoices"
        ],
        "summary": "Void an unpaid invoice",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "name": "invoiceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Voided.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "invoiceId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "Already paid \u2014 settlement is final.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "description": " Accepts either the creating API key or that invoice's manageToken."
      }
    },
    "/subscriptions": {
      "post": {
        "tags": [
          "Subscriptions"
        ],
        "summary": "Create a subscription",
        "description": "A cron mints a fresh invoice each period; `https://www.privacyhood.org/pay?sub=<id>` always resolves to the current one. PULL model \u2014 each period is paid deliberately by the customer, nothing auto-debits them.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "recipient": {
                    "type": "string"
                  },
                  "amount": {
                    "type": "string"
                  },
                  "cadence": {
                    "type": "string",
                    "enum": [
                      "daily",
                      "weekly",
                      "monthly"
                    ]
                  },
                  "memo": {
                    "type": "string"
                  },
                  "ref": {
                    "type": "string",
                    "description": "A PREFIX \u2014 each cycle's invoice gets its own suffixed ref, not this exact string."
                  }
                },
                "required": [
                  "recipient",
                  "amount",
                  "cadence"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created. `manageToken` is returned once.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "subscriptionId": {
                      "type": "string"
                    },
                    "manageToken": {
                      "type": "string",
                      "description": "Shown once. Required to cancel."
                    },
                    "amountBaseUnits": {
                      "type": "string"
                    },
                    "token": {
                      "type": "string"
                    },
                    "recipient": {
                      "type": "string"
                    },
                    "memo": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "refPrefix": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Each cycle's invoice gets its own suffixed ref."
                    },
                    "cadence": {
                      "type": "string",
                      "enum": [
                        "daily",
                        "weekly",
                        "monthly"
                      ]
                    },
                    "currentInvoiceId": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "First period's invoice; null if generation failed. Prefer the stable /pay?sub=<id> link."
                    },
                    "nextRunAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  },
                  "required": [
                    "subscriptionId",
                    "manageToken",
                    "amountBaseUnits",
                    "token",
                    "recipient",
                    "cadence",
                    "currentInvoiceId",
                    "nextRunAt"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid input.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/fee-tiers": {
      "get": {
        "tags": [
          "Public"
        ],
        "summary": "Fee schedule",
        "description": "The live fee table, so you can price against exactly what is charged.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "enabled": {
                      "type": "boolean"
                    },
                    "baseRate": {
                      "type": "number"
                    },
                    "phoodAddress": {
                      "type": "string"
                    },
                    "decimals": {
                      "type": "integer"
                    },
                    "tiers": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/health": {
      "get": {
        "tags": [
          "Public"
        ],
        "summary": "Liveness + database probe",
        "responses": {
          "200": {
            "description": "Healthy.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Unhealthy."
          }
        }
      }
    },
    "/subscriptions/{subscriptionId}/cancel": {
      "post": {
        "tags": [
          "Subscriptions"
        ],
        "summary": "Cancel a subscription",
        "description": "Stops future billing. Authorized by the subscription's manageToken as a bearer token. Invoices already minted stay payable.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "name": "subscriptionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "subscriptionId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/invoices/{invoiceId}/simulate-pay": {
      "post": {
        "tags": [
          "Invoices"
        ],
        "summary": "Simulate payment (test mode)",
        "description": "TEST MODE only. Settles a test invoice and fires its real signed invoice.paid webhook synchronously, returning the delivery result. Rejected (400) for a live invoice. Idempotent + re-fireable (resend). Authorize with the test API key or the invoice's manage token. deliveredTx is a deterministic synthetic hash, not an on-chain tx.",
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "name": "invoiceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Settled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "invoiceId": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "paid"
                      ]
                    },
                    "test": {
                      "type": "boolean"
                    },
                    "deliveredTx": {
                      "type": "string"
                    },
                    "webhook": {
                      "type": "object",
                      "description": "Present only when the invoice has a webhookUrl.",
                      "properties": {
                        "url": {
                          "type": "string"
                        },
                        "delivered": {
                          "type": "boolean"
                        },
                        "responseStatus": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Not a test invoice (or void).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    }
  }
}
