{"openapi":"3.1.0","info":{"title":"HarborPay API","description":"HarborPay sends USD payouts over SWIFT from your prefunded balance to bank accounts in the United States. This is the sandbox: it behaves like production, moves no real money and uses test credentials.\n\nBase URL: `/harborpay/v1` on the host that served these docs. The same content is available as [OpenAPI 3.1](/docs/harborpay/openapi.json) and as [Markdown](/docs/harborpay.md).\n\n## Getting started\n\nA payout needs three resources, created in this order:\n\n1. **Linked account**: the customer you pay out on behalf of.\n2. **Beneficiary**: a bank account that belongs to that linked account. It becomes `ACTIVE` a few seconds after you create it.\n3. **Payout**: an amount sent from your balance to the beneficiary.\n\n```bash\ncurl -X POST \"$HARBORPAY_BASE_URL/payouts\" \\\n  -H \"X-Api-Key: $HARBORPAY_API_KEY\" \\\n  -H \"Idempotency-Key: 3f1c9a52-6f0e-4d7b-9b1e-2a4c8d7e6f10\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"reference\": \"ord_1042-a1\",\n    \"linkedAccountId\": \"hp_la_01J8Z3K4Q2V7XH5N9M6T0P1R2S\",\n    \"beneficiaryId\": \"hp_ben_01J8Z3K4Q2V7XH5N9M6T0P1R2S\",\n    \"amount\": { \"minorUnits\": \"15000\", \"currency\": \"USD\" },\n    \"paymentMethod\": \"SWIFT\"\n  }'\n```\n\nHarborPay answers `202 Accepted` with the payout at `QUEUED`. That means HarborPay has taken the payout on, not that the money has arrived. Outcomes reach you as webhooks at the endpoint you set (see Configure your webhook endpoint).\n\n## Authentication\n\nSend your API key in the `X-Api-Key` header on every request. Sandbox keys look like `hp_test_sk_` followed by 32 letters and digits. Each account has one key, and it doesn't expire.\n\nA missing, wrong, rotated or revoked key gets `401 UNAUTHORIZED`. When a key is rotated, the old one stops working immediately.\n\n## Configure your webhook endpoint\n\nHarborPay sends webhooks to one endpoint per account, which you set:\n\n```bash\ncurl -X PUT \"$HARBORPAY_BASE_URL/webhook-endpoint\" \\\n  -H \"X-Api-Key: $HARBORPAY_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{ \"url\": \"https://your-devbox.example.dev/webhooks/harborpay\" }'\n```\n\n- `PUT /webhook-endpoint` with `{ \"url\" }` sets or replaces it and answers `200 { \"url\", \"updatedAt\" }`. Repeating the same request is safe, so it takes no Idempotency-Key. `GET /webhook-endpoint` returns the same object, and `GET /account` shows the URL as `webhookUrl`. A change applies from the next attempt.\n- The URL must be `http` or `https`, without credentials or a fragment; anything else gets `400 INVALID_REQUEST` at `url`.\n- The sandbox only delivers to approved development hosts. Another host gets `422 WEBHOOK_URL_NOT_ALLOWED`, and `details.allowedOrigins` lists the accepted origins, where `*` stands for any run of characters in the host or port.\n- Until you set one, `url` and `updatedAt` are `null` and HarborPay sends no webhooks. Payouts still change status, and `GET /payouts/{id}` shows it; changes from that time are not sent later, so read them with `GET`.\n- `POST /webhooks/test` sends a signed `ping` to check it (see Test webhooks); it gets `422 WEBHOOK_URL_NOT_SET` until an endpoint is set.\n\n## Conventions\n\n- **Format.** Requests and responses are JSON in UTF-8. Request bodies larger than 32 KiB are rejected with `413 PAYLOAD_TOO_LARGE`.\n- **Strict requests.** An unknown, missing or mistyped field, or a value that breaks a rule, gets `400 INVALID_REQUEST`. `details.errors` lists the failing paths, each with a message.\n- **Tolerant responses.** HarborPay adds fields, event types and reason codes over time. Ignore anything you don't recognise instead of failing on it.\n- **Timestamps** are ISO 8601 in UTC with milliseconds, for example `2026-09-21T14:13:20.000Z`.\n- **Ids** are a type prefix followed by a 26-character ULID (`hp_po_01J8Z3K4Q2V7XH5N9M6T0P1R2S`). Treat them as opaque strings.\n- **Request ids.** Every response carries an `X-Request-Id` header (`req_…`), except a gateway `504`. Quote it when you contact support.\n- **Lists** take `?cursor=` and `?limit=` (1–100, default 50) and return `{ \"data\": [...], \"nextCursor\": \"…\" | null }`, oldest first. Pass `nextCursor` back as `cursor` for the next page; a malformed cursor is a `400`. Filters are exact matches.\n- **Your account only.** Resources belong to one sandbox account. An id from another account is a `404`.\n- **Read-after-write.** Reads can lag behind a successful create by a few seconds. A resource can be used in the next request as soon as its create returns; only GET and list results can lag.\n- **Capacity.** A sandbox account can create up to 5,000 resources; after that, creates get `403 SANDBOX_CAPACITY`.\n- **Timeouts.** Most requests are answered within a second. If you set a client timeout on creates, allow at least 10 seconds and treat a timeout like a 5xx.\n\n## Errors and retries\n\nErrors share one envelope:\n\n```json\n{\n  \"code\": \"INVALID_REQUEST\",\n  \"message\": \"The request is invalid; see details.\",\n  \"details\": { \"errors\": [{ \"path\": \"amount.minorUnits\", \"message\": \"Required\" }] },\n  \"requestId\": \"req_01J8Z3K4Q2V7XH5N9M6T0P1R2S\"\n}\n```\n\n`details` is `{}` when there is nothing to add.\n\n| Status | Code | Meaning | Was anything created? | What to do |\n|---|---|---|---|---|\n| 400 | `INVALID_REQUEST` | The request is invalid; `details.errors` lists the paths | No | Fix the request. The Idempotency-Key is not used up. |\n| 401 | `UNAUTHORIZED` | The API key is missing, invalid, rotated or revoked | No | Check the key. |\n| 403 | `ACCOUNT_DISABLED` | Your account can't create payouts right now | No | Don't retry automatically. Reads still work. |\n| 403 | `SANDBOX_CAPACITY` | The account has created as many resources as the sandbox allows | No | |\n| 404 | `NOT_FOUND` | Not in your account | n/a | After a create with an unknown outcome, a 404 proves nothing: reads can lag. |\n| 409 | `IDEMPOTENCY_CONFLICT` | The Idempotency-Key was already used with a different body | No; the first result stands | Don't retry blindly. It points to a bug or a concurrent writer. |\n| 413 | `PAYLOAD_TOO_LARGE` | The body is larger than 32 KiB | No | |\n| 422 | see Business rejections | HarborPay understood the request and declined it | No | Don't resend it unchanged; decide what to do. If you sent an Idempotency-Key, the response is stored against it. |\n| 422 | `WEBHOOK_URL_NOT_ALLOWED` · `WEBHOOK_URL_NOT_SET` | Your webhook endpoint isn't on an approved host, or isn't set yet | No | See Configure your webhook endpoint. |\n| 429 | `RATE_LIMITED` | Too many requests | No | Wait `Retry-After` seconds, then retry (with the same key, if you sent one). |\n| 500 | `INTERNAL_ERROR` | Unexpected error | Unknown | Retry with the same key and backoff, or look the payout up by `reference`. Without a key, a retry is a new request. |\n| 503 | `SERVICE_UNAVAILABLE` | Temporarily unavailable; comes with `Retry-After` | Unknown | As for 500. |\n| 504 | (none) | Gateway timeout | Unknown | As for 500. The body may not be JSON. |\n\nA timeout or a dropped connection is an unknown outcome too. In every unknown case, retry with the **same** Idempotency-Key (with backoff) or look the payout up with `GET /payouts?reference=`. Don't switch to a new key until you know what happened. Without a key, HarborPay can't tell a retry from a new request, and each one creates a new resource. Errors from the network in front of HarborPay may not be JSON; don't assume a parseable body.\n\n## Endpoints\n\nThe three creates take an optional `Idempotency-Key` header (see Idempotency). `POST /webhooks/test` and `PUT /webhook-endpoint` take none.\n\n| Endpoint | Success | Notes |\n|---|---|---|\n| `GET /account` | `200` | `{ id, environment: \"sandbox\", currency: \"USD\", funding: \"PREFUNDED\", webhookUrl }`; `webhookUrl` is `null` until you set an endpoint |\n| `GET /webhook-endpoint` · `PUT /webhook-endpoint` | `200` | `{ url, updatedAt }`; see Configure your webhook endpoint |\n| `POST /linked-accounts` | `201` | |\n| `GET /linked-accounts/{id}` · `GET /linked-accounts?customerReference=` | `200` | |\n| `POST /beneficiaries` | `201` | `422 INVALID_RESOURCE` if the linked account doesn't exist |\n| `GET /beneficiaries/{id}` · `GET /beneficiaries?reference=` | `200` | |\n| `POST /payouts` | `202` | Business rejections below |\n| `GET /payouts/{id}` · `GET /payouts?reference=` | `200` | References are not unique, so the filter can return several payouts |\n| `POST /webhooks/test` | `202` | `{ eventId }`; sends a signed `ping`; `422 WEBHOOK_URL_NOT_SET` until you set an endpoint |\n\n## Resources and fields\n\nLengths count UTF-16 code units, as JavaScript's `length` does, so an emoji can count as two. A *reference* is 1–80 letters, digits, `_` or `-`.\n\n**Linked account.** Request: `customerReference` (a reference; your customer's id, not unique) and `displayName` (1–140). HarborPay adds `id` (`hp_la_…`), `status: \"ACTIVE\"`, `currency: \"USD\"`, `createdAt` and `updatedAt`. Linked accounts are active as soon as they are created.\n\n**Beneficiary.** Request:\n\n- `reference` (a reference), `linkedAccountId`, `name` (1–140)\n- `address`: `line1` (1–35), optional `line2` (1–35), `city` (1–35), `postalCode` (1–16), `country: \"US\"`\n- `bankAccount`: `accountNumber` (4–17 digits), `bic` (a US BIC: 8 or 11 characters with `US` as the country code, e.g. `TSTBUS33`), `bankCountry: \"US\"`\n\nHarborPay adds `id` (`hp_ben_…`), `status`, `createdAt` and `updatedAt`. Beneficiaries are immutable: new bank details need a new beneficiary.\n\nA new beneficiary starts `PENDING` while HarborPay verifies its bank details, which usually takes a few seconds, and then becomes `ACTIVE`. Check that `GET /beneficiaries/{id}` shows `ACTIVE` before paying out to it: a payout to a `PENDING` beneficiary gets `422 BENEFICIARY_NOT_ACTIVE`.\n\n**Payout.** Request:\n\n- `reference`: your order or attempt id (a reference, not unique)\n- `linkedAccountId`, `beneficiaryId`\n- `amount`: `{ \"minorUnits\": \"15000\", \"currency\": \"USD\" }`\n- `paymentMethod: \"SWIFT\"`\n\n## Money\n\nAmounts are objects with an integer string of cents: `{ \"minorUnits\": \"15000\", \"currency\": \"USD\" }` is USD 150.00. `minorUnits` has no sign, no decimal point and no leading zeros. Limit values in error details (`minimumMinor`, `maximumMinor`, `requestedMinor`) are integer strings too.\n\n## Payout limits\n\nEach payout must be within your account's per-payout minimum and maximum. The limits are set for your account, aren't published, and can change at any time. A change applies to requests received after it; payouts already accepted aren't re-checked. A request outside the current limits is rejected with `422 AMOUNT_BELOW_MINIMUM` or `422 AMOUNT_ABOVE_MAXIMUM`, and its `details` contain the current limits (inclusive) and the amount you asked for. There are no fees in the sandbox, so limits apply to the amount as sent.\n\nFor HarborPay, the message reads \"Amount is below your account's current minimum of USD {minimum}.\" (or \"above … maximum\"), and `details` carries `minimumMinor`, `maximumMinor` and `requestedMinor`.\n\n## Idempotency\n\n- **Optional.** HarborPay uses the `Idempotency-Key` header to recognise a retry of the same request. Without it, HarborPay can't tell a retry from a new request, and each one creates a new resource.\n- **Format and scope.** 1–255 printable ASCII characters without spaces, scoped to your account and the endpoint. Use a new key, such as a UUID, per logical request.\n- **What is stored.** For 24 hours from the first request with a key, HarborPay stores every `201`/`202` and every `422` (including `DAILY_LIMIT_EXCEEDED`). Nothing is stored for `400`, `401`, `403`, `409`, `413`, `429` or `5xx`, so retrying those with the same key is safe. Nothing is stored for a request without a key.\n- **Same key, same body.** You get the stored status and body again, with `Idempotency-Replayed: true`, even if your account was disabled or your limits changed since. Bodies are compared as JSON, so key order and whitespace don't matter. The body is the snapshot from the first response: a payout that has since settled still replays as `QUEUED`. Use `GET /payouts/{id}` for the current state.\n- **Same key, different body.** `409 IDEMPOTENCY_CONFLICT`, and nothing changes: \"This Idempotency-Key was already used for a different request (stored response: 422). Use a new key for a new request.\"\n- **Concurrent requests** with the same key wait for the first one and receive its stored result.\n- **After 24 hours** the key is forgotten and can create again.\n\n## Payout lifecycle\n\n- `QUEUED` → `PROCESSING` → `SETTLED` → `RETURNED`\n- `PROCESSING` → `FAILED`\n- `PROCESSING` → `IN_REVIEW` → `SETTLED` or `FAILED`\n\n| Status | Meaning | Final? | `statusReason` | Webhook |\n|---|---|---|---|---|\n| `QUEUED` | Accepted, not yet sent | No | `null` | none: the `202` is the acknowledgement |\n| `PROCESSING` | Sent into SWIFT; `uetr` is assigned | No | `null` | `payout.processing` |\n| `IN_REVIEW` | Held for compliance review, usually after `PROCESSING`; ends `SETTLED` or `FAILED` | No | `COMPLIANCE_REVIEW` | `payout.in_review` |\n| `SETTLED` | The beneficiary's bank accepted the funds (crediting the account can take longer) | Unless returned | `null` | `payout.settled` |\n| `FAILED` | Stopped before money left your balance | Yes | a failure code | `payout.failed` |\n| `RETURNED` | Sent, then returned by the beneficiary's bank; the full amount is credited back to you | Yes | a return code | `payout.returned` |\n\n`SETTLED` is not always the end: a settled payout can still become `RETURNED`, at a higher version.\n\n**The payout object:** `id` (`hp_po_…`), `reference`, `linkedAccountId`, `beneficiaryId`, `amount`, `paymentMethod`, `status`, `statusVersion`, `statusReason` (`{ code, message }` or `null`), `uetr` (the SWIFT UETR, a UUID v4, from `PROCESSING`; else `null`), `settledAt`, `returnedAt`, `createdAt`, `updatedAt`.\n\n### Versions\n\n`statusVersion` is `1` when the payout is created and goes up by one with every status change. `GET` always returns the latest version, and every webhook carries a complete payout snapshot with its version.\n\nWebhooks can arrive late, more than once, out of order (a newer snapshot before an older one) or not at all. Apply this rule:\n\n- **Apply a snapshot only if its `statusVersion` is greater than the one you have stored.** Otherwise acknowledge it with a `2xx` and ignore it.\n- A jump, say from 2 to 4, means you missed a notification. The snapshot is still complete, so apply it.\n- If in doubt, `GET /payouts/{id}`.\n\n### Reason codes\n\n| Code | Status | Meaning |\n|---|---|---|\n| `COMPLIANCE_REVIEW` | `IN_REVIEW` | HarborPay's compliance team is reviewing this payout. |\n| `BENEFICIARY_ACCOUNT_CLOSED` | `FAILED`, `RETURNED` | The beneficiary's bank reports the account is closed. |\n| `COMPLIANCE_REJECTED` | `FAILED` | HarborPay's compliance review declined this payout. |\n| `BANK_REJECTED` | `FAILED` | The beneficiary's bank or an intermediary rejected the payment without a specific reason. |\n| `BENEFICIARY_NAME_MISMATCH` | `RETURNED` | The beneficiary's bank couldn't match the name to the account. |\n| `RETURNED_BY_BENEFICIARY_BANK` | `RETURNED` | The beneficiary's bank sent the funds back without a specific reason. |\n\nThis list is not exhaustive: treat a code you don't recognise as needing investigation. Messages for `RETURNED` read \"The beneficiary's bank returned the funds: …\" followed by the meaning.\n\n## Business rejections\n\nA `422` means HarborPay understood the request and declined it. Nothing was created, and if you sent an Idempotency-Key, the response is stored against it.\n\n| Code | When | `details` |\n|---|---|---|\n| `INVALID_RESOURCE` | The linked account or beneficiary doesn't exist | `{ \"field\": \"beneficiaryId\" }` |\n| `ACCOUNT_MISMATCH` | The beneficiary belongs to a different linked account | `{ \"beneficiaryId\", \"linkedAccountId\", \"beneficiaryLinkedAccountId\" }` |\n| `BENEFICIARY_NOT_ACTIVE` | The beneficiary is still `PENDING` | `{ \"beneficiaryId\", \"status\" }` |\n| `AMOUNT_BELOW_MINIMUM` · `AMOUNT_ABOVE_MAXIMUM` | The amount is outside your current limits | `{ \"minimumMinor\": \"<minimum>\", \"maximumMinor\": \"<maximum>\", \"requestedMinor\": \"<amount>\" }` |\n| `DAILY_LIMIT_EXCEEDED` | The payout would take the linked account over its daily limit, which resets at 00:00 UTC | `{ \"linkedAccountId\", \"resetsAt\" }` |\n\n## Webhooks\n\nHarborPay sends a webhook for every status change after creation to your account's webhook endpoint (see Configure your webhook endpoint); a change of endpoint applies from the next attempt. While no endpoint is set, it sends none.\n\n### Delivery\n\n- `POST` with a JSON body, a 5-second timeout and no redirects followed. Only a `2xx` counts as received.\n- The first attempt is immediate. Retries follow after 5 s, 15 s, 45 s, 2 min and 6 min: six attempts over about nine minutes.\n- HarborPay retries after any non-`2xx` response (including `3xx`), a timeout or a connection error.\n- A redelivery sends the event's original bytes with the same event id, a fresh timestamp and a fresh signature.\n\n### Body\n\n```json\n{\n  \"id\": \"evt_01J8Z3K4Q2V7XH5N9M6T0P1R2S\",\n  \"type\": \"payout.settled\",\n  \"createdAt\": \"2026-09-21T14:13:20.000Z\",\n  \"data\": { \"id\": \"hp_po_…\", \"status\": \"SETTLED\", \"statusVersion\": 3, \"…\": \"the full payout\" }\n}\n```\n\nTypes: `payout.processing`, `payout.in_review`, `payout.settled`, `payout.failed`, `payout.returned`, and `ping` (with `data: {}`). `type` always matches `data.status`. New types may appear; acknowledge them with a `2xx`.\n\n### Signatures\n\nHarborPay signs webhooks the [Standard Webhooks](https://www.standardwebhooks.com/) way, so Standard Webhooks libraries work if you give them the raw body.\n\n| Header | Value |\n|---|---|\n| `webhook-id` | The event id. It stays the same across retries and redeliveries. |\n| `webhook-timestamp` | Unix seconds when this attempt was signed |\n| `webhook-signature` | Space-separated `v1,<base64>` entries; accept the request if any entry matches |\n| `user-agent` | `HarborPay-Webhooks/1` |\n\nYour webhook secret looks like `whsec_` followed by base64. To verify:\n\n1. Take the **raw** request body, exactly as received. Don't parse and re-serialize it first.\n2. Build the signed content `{webhook-id}.{webhook-timestamp}.{raw body}`.\n3. Compute HMAC-SHA256 over it. The key is the base64-decoded part of the secret after `whsec_`. Base64-encode the result.\n4. Compare it with each `v1,` entry of `webhook-signature` in constant time.\n5. Reject timestamps more than 5 minutes (300 seconds) away from your clock.\n\nOnly then parse the JSON.\n\n```js\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\n\nexport function verifyHarborPay(secret, headers, rawBody, nowS = Math.floor(Date.now() / 1000)) {\n  const id = headers[\"webhook-id\"];\n  const timestamp = headers[\"webhook-timestamp\"];\n  if (!id || !timestamp || Math.abs(nowS - Number(timestamp)) > 300) return false;\n  const key = Buffer.from(secret.slice(\"whsec_\".length), \"base64\");\n  const expected = createHmac(\"sha256\", key).update(`${id}.${timestamp}.${rawBody}`).digest();\n  return (headers[\"webhook-signature\"] ?? \"\").split(\" \").some((entry) => {\n    const [version, signature] = entry.split(\",\");\n    const got = Buffer.from(signature ?? \"\", \"base64\");\n    return version === \"v1\" && got.length === expected.length && timingSafeEqual(got, expected);\n  });\n}\n```\n\n### Test vector\n\nUse this to check your verifier. The timestamp is in the past, so compute the signature yourself or fix your clock at `1790000000`; a live verifier rightly calls it stale.\n\n- Secret: `whsec_aGFyYm9ycGF5LWRvY3MtZXhhbXBsZS1rZXktMDAwMDAx`\n- `webhook-id`: `evt_01J8Z3K4Q2V7XH5N9M6T0P1R2S`\n- `webhook-timestamp`: `1790000000`\n- Body (exact bytes, no trailing newline):\n\n```\n{\"id\":\"evt_01J8Z3K4Q2V7XH5N9M6T0P1R2S\",\"type\":\"payout.settled\",\"createdAt\":\"2026-09-21T14:13:20.000Z\",\"data\":{\"id\":\"hp_po_01J8Z3K0000000000000000000\",\"status\":\"SETTLED\",\"statusVersion\":3}}\n```\n\n- Expected `webhook-signature`: `v1,efUQhNDoZ1f7z5JyAKoWjb3jVxHyxqwQv6ch2w/y99o=`\n- The same body with one trailing space must fail.\n\nThe Standard Webhooks specification's own example (`msg_p5jXN8AQM9LWM0D4loKWxJek`, timestamp `1614265330`) verifies with the same routine.\n\n### Test webhooks\n\n`POST /webhooks/test` sends a signed `ping` to your webhook endpoint. It is attempted once, with no retries, and you can send up to 10 per rolling minute; more get `429 RATE_LIMITED`. Until you set an endpoint it gets `422 WEBHOOK_URL_NOT_SET`.\n\n## Sandbox notes\n\nA payout usually moves to `PROCESSING` about 5 seconds after creation and settles about 5 seconds after that; timings vary. Payouts in the sandbox usually succeed within seconds. The other outcomes described here (reviews, failures, returns, delays) can happen, as they do in production; you can't request them.\n\n**Test values.** BIC `TSTBUS33` or `TSTBUS33XXX`, account number `000123456789`. A non-US BIC such as `TSTBGB22` gets `400` at `bankAccount.bic`. Linked accounts are active at once; beneficiaries become active a few seconds after creation. No amount or reference triggers a particular outcome.\n","version":"2.0.0"},"servers":[{"url":"https://interview-sandbox-4ad8e05e.neobank.xyz/harborpay/v1"}],"components":{"securitySchemes":{"ApiKey":{"type":"apiKey","in":"header","name":"X-Api-Key"}},"schemas":{"PayoutEvent":{"type":"object","properties":{"id":{"type":"string","description":"Event id; equals the webhook-id header.","example":"evt_01J8Z3K4Q2V7XH5N9M6T0P1R2S"},"type":{"type":"string","enum":["payout.processing","payout.in_review","payout.settled","payout.failed","payout.returned"]},"createdAt":{"type":"string","description":"ISO 8601 UTC with milliseconds.","example":"2026-09-21T14:13:20.000Z"},"data":{"$ref":"#/components/schemas/Payout"}},"required":["id","type","createdAt","data"]},"Payout":{"type":"object","properties":{"id":{"type":"string","example":"hp_po_01J8Z3K4Q2V7XH5N9M6T0P1R2S"},"reference":{"type":"string"},"linkedAccountId":{"type":"string"},"beneficiaryId":{"type":"string"},"amount":{"$ref":"#/components/schemas/Money"},"paymentMethod":{"type":"string","enum":["SWIFT"]},"status":{"type":"string","enum":["QUEUED","PROCESSING","IN_REVIEW","SETTLED","FAILED","RETURNED"]},"statusVersion":{"type":"integer","description":"1 at creation, +1 per status change.","example":1},"statusReason":{"$ref":"#/components/schemas/StatusReason"},"uetr":{"type":["string","null"],"description":"SWIFT UETR (UUID v4), assigned at PROCESSING."},"settledAt":{"type":["string","null"],"description":"ISO 8601 UTC with milliseconds.","example":"2026-09-21T14:13:20.000Z"},"returnedAt":{"type":["string","null"],"description":"ISO 8601 UTC with milliseconds.","example":"2026-09-21T14:13:20.000Z"},"createdAt":{"type":"string","description":"ISO 8601 UTC with milliseconds.","example":"2026-09-21T14:13:20.000Z"},"updatedAt":{"type":"string","description":"ISO 8601 UTC with milliseconds.","example":"2026-09-21T14:13:20.000Z"}},"required":["id","reference","linkedAccountId","beneficiaryId","amount","paymentMethod","status","statusVersion","statusReason","uetr","settledAt","returnedAt","createdAt","updatedAt"]},"Money":{"type":"object","properties":{"minorUnits":{"type":"string","pattern":"^[1-9][0-9]{0,17}$","description":"Amount in cents, as an integer string.","example":"15000"},"currency":{"type":"string","enum":["USD"]}},"required":["minorUnits","currency"],"description":"USD amount in minor units: \"15000\" is USD 150.00."},"StatusReason":{"type":["object","null"],"properties":{"code":{"type":"string","description":"Reason code. The list of codes is not exhaustive: treat one you don't recognise as needing investigation.","example":"BENEFICIARY_ACCOUNT_CLOSED"},"message":{"type":"string","example":"The beneficiary's bank reports the account is closed."}},"required":["code","message"]},"PingEvent":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","enum":["ping"]},"createdAt":{"type":"string","description":"ISO 8601 UTC with milliseconds.","example":"2026-09-21T14:13:20.000Z"},"data":{"type":"object","properties":{}}},"required":["id","type","createdAt","data"]},"Account":{"type":"object","properties":{"id":{"type":"string","example":"hp_acct_01J8Z3K4Q2V7XH5N9M6T0P1R2S"},"environment":{"type":"string","enum":["sandbox"]},"currency":{"type":"string","enum":["USD"]},"funding":{"type":"string","enum":["PREFUNDED"]},"webhookUrl":{"type":["string","null"],"description":"Your webhook endpoint (`GET /webhook-endpoint`): where webhooks are sent. `null` until you set one."}},"required":["id","environment","currency","funding","webhookUrl"]},"Error":{"type":"object","properties":{"code":{"type":"string","example":"INVALID_REQUEST"},"message":{"type":"string"},"details":{"type":"object","additionalProperties":{},"description":"`{}` when empty. 400: `errors: [{ path, message }]`."},"requestId":{"type":"string","example":"req_01J8Z3K4Q2V7XH5N9M6T0P1R2S"}},"required":["code","message","details","requestId"]},"LinkedAccount":{"type":"object","properties":{"id":{"type":"string","example":"hp_la_01J8Z3K4Q2V7XH5N9M6T0P1R2S"},"customerReference":{"type":"string"},"displayName":{"type":"string"},"status":{"type":"string","enum":["ACTIVE"]},"currency":{"type":"string","enum":["USD"]},"createdAt":{"type":"string","description":"ISO 8601 UTC with milliseconds.","example":"2026-09-21T14:13:20.000Z"},"updatedAt":{"type":"string","description":"ISO 8601 UTC with milliseconds.","example":"2026-09-21T14:13:20.000Z"}},"required":["id","customerReference","displayName","status","currency","createdAt","updatedAt"],"description":"A customer you pay out on behalf of."},"CreateLinkedAccount":{"type":"object","properties":{"customerReference":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,80}$","description":"Your customer's id. Not unique.","example":"cust_4821"},"displayName":{"type":"string","minLength":1,"maxLength":140,"example":"Sam Lee"}},"required":["customerReference","displayName"],"additionalProperties":false},"LinkedAccountList":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/LinkedAccount"}},"nextCursor":{"type":["string","null"]}},"required":["data","nextCursor"]},"Beneficiary":{"type":"object","properties":{"id":{"type":"string","example":"hp_ben_01J8Z3K4Q2V7XH5N9M6T0P1R2S"},"reference":{"type":"string"},"linkedAccountId":{"type":"string"},"name":{"type":"string"},"address":{"$ref":"#/components/schemas/Address"},"bankAccount":{"$ref":"#/components/schemas/BankAccount"},"status":{"type":"string","enum":["PENDING","ACTIVE"],"description":"`PENDING` while HarborPay verifies the bank details, usually a few seconds; then `ACTIVE`. Pay out only to an `ACTIVE` beneficiary."},"createdAt":{"type":"string","description":"ISO 8601 UTC with milliseconds.","example":"2026-09-21T14:13:20.000Z"},"updatedAt":{"type":"string","description":"ISO 8601 UTC with milliseconds.","example":"2026-09-21T14:13:20.000Z"}},"required":["id","reference","linkedAccountId","name","address","bankAccount","status","createdAt","updatedAt"],"description":"A linked account's bank account. Immutable: new bank details need a new beneficiary."},"Address":{"type":"object","properties":{"line1":{"type":"string","minLength":1,"maxLength":35},"line2":{"type":"string","minLength":1,"maxLength":35},"city":{"type":"string","minLength":1,"maxLength":35},"postalCode":{"type":"string","minLength":1,"maxLength":16},"country":{"type":"string","enum":["US"]}},"required":["line1","city","postalCode","country"]},"BankAccount":{"type":"object","properties":{"accountNumber":{"type":"string","pattern":"^[0-9]{4,17}$","example":"000123456789"},"bic":{"type":"string","pattern":"^[A-Z]{4}US[A-Z0-9]{2}([A-Z0-9]{3})?$","example":"TSTBUS33"},"bankCountry":{"type":"string","enum":["US"]}},"required":["accountNumber","bic","bankCountry"]},"CreateBeneficiary":{"type":"object","properties":{"reference":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,80}$","example":"ben_4821_main"},"linkedAccountId":{"type":"string","minLength":1,"example":"hp_la_01J8Z3K4Q2V7XH5N9M6T0P1R2S"},"name":{"type":"string","minLength":1,"maxLength":140,"example":"Sam Lee"},"address":{"type":"object","properties":{"line1":{"type":"string","minLength":1,"maxLength":35},"line2":{"type":"string","minLength":1,"maxLength":35},"city":{"type":"string","minLength":1,"maxLength":35},"postalCode":{"type":"string","minLength":1,"maxLength":16},"country":{"type":"string","enum":["US"]}},"required":["line1","city","postalCode","country"],"additionalProperties":false},"bankAccount":{"type":"object","properties":{"accountNumber":{"type":"string","pattern":"^[0-9]{4,17}$","example":"000123456789"},"bic":{"type":"string","pattern":"^[A-Z]{4}US[A-Z0-9]{2}([A-Z0-9]{3})?$","example":"TSTBUS33"},"bankCountry":{"type":"string","enum":["US"]}},"required":["accountNumber","bic","bankCountry"],"additionalProperties":false}},"required":["reference","linkedAccountId","name","address","bankAccount"],"additionalProperties":false},"BeneficiaryList":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Beneficiary"}},"nextCursor":{"type":["string","null"]}},"required":["data","nextCursor"]},"CreatePayout":{"type":"object","properties":{"reference":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,80}$","description":"Your order or attempt id. Not unique.","example":"ord_1042-a1"},"linkedAccountId":{"type":"string","minLength":1,"example":"hp_la_01J8Z3K4Q2V7XH5N9M6T0P1R2S"},"beneficiaryId":{"type":"string","minLength":1,"example":"hp_ben_01J8Z3K4Q2V7XH5N9M6T0P1R2S"},"amount":{"type":"object","properties":{"minorUnits":{"type":"string","pattern":"^[1-9][0-9]{0,17}$","description":"Amount in cents, as an integer string.","example":"15000"},"currency":{"type":"string","enum":["USD"]}},"required":["minorUnits","currency"],"additionalProperties":false},"paymentMethod":{"type":"string","enum":["SWIFT"]}},"required":["reference","linkedAccountId","beneficiaryId","amount","paymentMethod"],"additionalProperties":false},"PayoutList":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Payout"}},"nextCursor":{"type":["string","null"]}},"required":["data","nextCursor"]},"WebhookEndpoint":{"type":"object","properties":{"url":{"type":["string","null"],"description":"Where HarborPay sends webhooks. `null` until you set one.","example":"https://your-devbox.example.dev/webhooks/harborpay"},"updatedAt":{"type":["string","null"],"description":"ISO 8601 UTC with milliseconds.","example":"2026-09-21T14:13:20.000Z"}},"required":["url","updatedAt"],"description":"Your webhook endpoint; `url` and `updatedAt` (when it was last set) are `null` until you set one."},"SetWebhookEndpoint":{"type":"object","properties":{"url":{"type":"string","description":"An `http` or `https` URL on an approved development host, without credentials or a fragment.","example":"https://your-devbox.example.dev/webhooks/harborpay"}},"required":["url"],"additionalProperties":false},"WebhookTestResult":{"type":"object","properties":{"eventId":{"type":"string","example":"evt_01J8Z3K4Q2V7XH5N9M6T0P1R2S"}},"required":["eventId"]}},"parameters":{}},"paths":{"/account":{"get":{"tags":["Account"],"summary":"Get your account","security":[{"ApiKey":[]}],"responses":{"200":{"description":"Your sandbox account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Account"}}}},"400":{"description":"Invalid request. `details.errors` lists every failing path; nothing was created and the key is not used up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`UNAUTHORIZED`: the API key is missing, invalid, rotated or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`INTERNAL_ERROR`: the outcome is unknown. Retry with the same key, or look it up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/linked-accounts":{"post":{"tags":["Linked accounts"],"summary":"Create a linked account","description":"A customer you pay out on behalf of. Active at once.","security":[{"ApiKey":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[\\x21-\\x7E]{1,255}$","description":"Optional. HarborPay uses it to recognise a retry of the same request. Without it, HarborPay can't tell a retry from a new request, and each one creates a new resource. If you send one, use a new key per logical request (a UUID works) and reuse it only to retry that request.","example":"3f1c9a52-6f0e-4d7b-9b1e-2a4c8d7e6f10"},"required":false,"description":"Optional. HarborPay uses it to recognise a retry of the same request. Without it, HarborPay can't tell a retry from a new request, and each one creates a new resource. If you send one, use a new key per logical request (a UUID works) and reuse it only to retry that request.","name":"Idempotency-Key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLinkedAccount"}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkedAccount"}}}},"400":{"description":"Invalid request. `details.errors` lists every failing path; nothing was created and the key is not used up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`UNAUTHORIZED`: the API key is missing, invalid, rotated or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`ACCOUNT_DISABLED` (can't create right now; don't auto-retry, reads work) or `SANDBOX_CAPACITY`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`IDEMPOTENCY_CONFLICT`: this key was used for a different request; the first result stands.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`PAYLOAD_TOO_LARGE`: the body is larger than 32 KiB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`INTERNAL_ERROR`: the outcome is unknown. Retry with the same key, or look it up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Gateway timeout: the outcome is unknown. The body is not JSON. Retry with the same key, or look it up."}}},"get":{"tags":["Linked accounts"],"summary":"List linked accounts","description":"Oldest first. Filter by `customerReference` (exact match).","security":[{"ApiKey":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,80}$"},"required":false,"name":"customerReference","in":"query"},{"schema":{"type":"string","description":"Opaque cursor from a previous page's `nextCursor`."},"required":false,"description":"Opaque cursor from a previous page's `nextCursor`.","name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"description":"Page size, 1–100. Default 50.","example":50},"required":false,"description":"Page size, 1–100. Default 50.","name":"limit","in":"query"}],"responses":{"200":{"description":"A page of linked accounts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkedAccountList"}}}},"400":{"description":"Invalid request. `details.errors` lists every failing path; nothing was created and the key is not used up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`UNAUTHORIZED`: the API key is missing, invalid, rotated or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`INTERNAL_ERROR`: the outcome is unknown. Retry with the same key, or look it up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/linked-accounts/{id}":{"get":{"tags":["Linked accounts"],"summary":"Get a linked account","security":[{"ApiKey":[]}],"parameters":[{"schema":{"type":"string","example":"hp_la_01J8Z3K4Q2V7XH5N9M6T0P1R2S"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"The linked account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkedAccount"}}}},"400":{"description":"Invalid request. `details.errors` lists every failing path; nothing was created and the key is not used up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`UNAUTHORIZED`: the API key is missing, invalid, rotated or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`NOT_FOUND`: not in your account. Reads can lag behind a successful create by a few seconds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`INTERNAL_ERROR`: the outcome is unknown. Retry with the same key, or look it up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/beneficiaries":{"post":{"tags":["Beneficiaries"],"summary":"Create a beneficiary","description":"A linked account's US bank account. Immutable. It starts `PENDING` while HarborPay verifies the bank details, usually for a few seconds, then becomes `ACTIVE`: check `GET /beneficiaries/{id}` before paying out to it.","security":[{"ApiKey":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[\\x21-\\x7E]{1,255}$","description":"Optional. HarborPay uses it to recognise a retry of the same request. Without it, HarborPay can't tell a retry from a new request, and each one creates a new resource. If you send one, use a new key per logical request (a UUID works) and reuse it only to retry that request.","example":"3f1c9a52-6f0e-4d7b-9b1e-2a4c8d7e6f10"},"required":false,"description":"Optional. HarborPay uses it to recognise a retry of the same request. Without it, HarborPay can't tell a retry from a new request, and each one creates a new resource. If you send one, use a new key per logical request (a UUID works) and reuse it only to retry that request.","name":"Idempotency-Key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBeneficiary"}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Beneficiary"}}}},"400":{"description":"Invalid request. `details.errors` lists every failing path; nothing was created and the key is not used up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`UNAUTHORIZED`: the API key is missing, invalid, rotated or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`ACCOUNT_DISABLED` (can't create right now; don't auto-retry, reads work) or `SANDBOX_CAPACITY`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`IDEMPOTENCY_CONFLICT`: this key was used for a different request; the first result stands.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`PAYLOAD_TOO_LARGE`: the body is larger than 32 KiB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`INVALID_RESOURCE`: the linked account doesn't exist (`details.field`). Stored against the key, if you sent one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`INTERNAL_ERROR`: the outcome is unknown. Retry with the same key, or look it up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Gateway timeout: the outcome is unknown. The body is not JSON. Retry with the same key, or look it up."}}},"get":{"tags":["Beneficiaries"],"summary":"List beneficiaries","description":"Oldest first. Filter by `reference` (exact match).","security":[{"ApiKey":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,80}$"},"required":false,"name":"reference","in":"query"},{"schema":{"type":"string","description":"Opaque cursor from a previous page's `nextCursor`."},"required":false,"description":"Opaque cursor from a previous page's `nextCursor`.","name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"description":"Page size, 1–100. Default 50.","example":50},"required":false,"description":"Page size, 1–100. Default 50.","name":"limit","in":"query"}],"responses":{"200":{"description":"A page of beneficiaries.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BeneficiaryList"}}}},"400":{"description":"Invalid request. `details.errors` lists every failing path; nothing was created and the key is not used up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`UNAUTHORIZED`: the API key is missing, invalid, rotated or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`INTERNAL_ERROR`: the outcome is unknown. Retry with the same key, or look it up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/beneficiaries/{id}":{"get":{"tags":["Beneficiaries"],"summary":"Get a beneficiary","security":[{"ApiKey":[]}],"parameters":[{"schema":{"type":"string","example":"hp_ben_01J8Z3K4Q2V7XH5N9M6T0P1R2S"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"The beneficiary.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Beneficiary"}}}},"400":{"description":"Invalid request. `details.errors` lists every failing path; nothing was created and the key is not used up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`UNAUTHORIZED`: the API key is missing, invalid, rotated or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`NOT_FOUND`: not in your account. Reads can lag behind a successful create by a few seconds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`INTERNAL_ERROR`: the outcome is unknown. Retry with the same key, or look it up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/payouts":{"post":{"tags":["Payouts"],"summary":"Create a payout","description":"Accepted asynchronously: the 202 body is the payout at `QUEUED`, version 1. Outcomes arrive as webhooks.","security":[{"ApiKey":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[\\x21-\\x7E]{1,255}$","description":"Optional. HarborPay uses it to recognise a retry of the same request. Without it, HarborPay can't tell a retry from a new request, and each one creates a new resource. If you send one, use a new key per logical request (a UUID works) and reuse it only to retry that request.","example":"3f1c9a52-6f0e-4d7b-9b1e-2a4c8d7e6f10"},"required":false,"description":"Optional. HarborPay uses it to recognise a retry of the same request. Without it, HarborPay can't tell a retry from a new request, and each one creates a new resource. If you send one, use a new key per logical request (a UUID works) and reuse it only to retry that request.","name":"Idempotency-Key","in":"header"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePayout"}}}},"responses":{"202":{"description":"Accepted (QUEUED). Not paid yet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Payout"}}}},"400":{"description":"Invalid request. `details.errors` lists every failing path; nothing was created and the key is not used up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`UNAUTHORIZED`: the API key is missing, invalid, rotated or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`ACCOUNT_DISABLED` (can't create right now; don't auto-retry, reads work) or `SANDBOX_CAPACITY`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`IDEMPOTENCY_CONFLICT`: this key was used for a different request; the first result stands.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`PAYLOAD_TOO_LARGE`: the body is larger than 32 KiB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Business rejection; nothing was created and the response is stored against the key, if you sent one: `INVALID_RESOURCE`, `ACCOUNT_MISMATCH`, `BENEFICIARY_NOT_ACTIVE`, `AMOUNT_BELOW_MINIMUM`, `AMOUNT_ABOVE_MAXIMUM`, `DAILY_LIMIT_EXCEEDED`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`RATE_LIMITED`: wait `Retry-After` seconds, then retry with the same key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`INTERNAL_ERROR`: the outcome is unknown. Retry with the same key, or look it up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`SERVICE_UNAVAILABLE`: the outcome is unknown. Retry with the same key after `Retry-After` seconds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Gateway timeout: the outcome is unknown. The body is not JSON. Retry with the same key, or look it up."}}},"get":{"tags":["Payouts"],"summary":"List payouts","description":"Oldest first. Filter by `reference` (exact match; references are not unique).","security":[{"ApiKey":[]}],"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,80}$"},"required":false,"name":"reference","in":"query"},{"schema":{"type":"string","description":"Opaque cursor from a previous page's `nextCursor`."},"required":false,"description":"Opaque cursor from a previous page's `nextCursor`.","name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":100,"description":"Page size, 1–100. Default 50.","example":50},"required":false,"description":"Page size, 1–100. Default 50.","name":"limit","in":"query"}],"responses":{"200":{"description":"A page of payouts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayoutList"}}}},"400":{"description":"Invalid request. `details.errors` lists every failing path; nothing was created and the key is not used up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`UNAUTHORIZED`: the API key is missing, invalid, rotated or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`INTERNAL_ERROR`: the outcome is unknown. Retry with the same key, or look it up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/payouts/{id}":{"get":{"tags":["Payouts"],"summary":"Get a payout","security":[{"ApiKey":[]}],"parameters":[{"schema":{"type":"string","example":"hp_po_01J8Z3K4Q2V7XH5N9M6T0P1R2S"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"The payout's latest state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Payout"}}}},"400":{"description":"Invalid request. `details.errors` lists every failing path; nothing was created and the key is not used up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`UNAUTHORIZED`: the API key is missing, invalid, rotated or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`NOT_FOUND`: not in your account. Reads can lag behind a successful create by a few seconds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`INTERNAL_ERROR`: the outcome is unknown. Retry with the same key, or look it up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/webhook-endpoint":{"get":{"tags":["Webhook endpoint"],"summary":"Get your webhook endpoint","description":"Where HarborPay sends webhooks. `url` and `updatedAt` are `null` until you set one.","security":[{"ApiKey":[]}],"responses":{"200":{"description":"Your webhook endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"400":{"description":"Invalid request. `details.errors` lists every failing path; nothing was created and the key is not used up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`UNAUTHORIZED`: the API key is missing, invalid, rotated or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`INTERNAL_ERROR`: the outcome is unknown. Retry with the same key, or look it up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Webhook endpoint"],"summary":"Set your webhook endpoint","description":"Sets or replaces where HarborPay sends webhooks; a change applies from the next attempt. Repeating the same request is safe, so it takes no Idempotency-Key. Until you set one, HarborPay sends no webhooks, and status changes from that time aren't sent later: read them with `GET /payouts/{id}`.","security":[{"ApiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetWebhookEndpoint"}}}},"responses":{"200":{"description":"Saved.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEndpoint"}}}},"400":{"description":"Invalid request. `details.errors` lists every failing path, e.g. `url` when it isn't an `http` or `https` URL, or has credentials or a fragment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`UNAUTHORIZED`: the API key is missing, invalid, rotated or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"`PAYLOAD_TOO_LARGE`: the body is larger than 32 KiB.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`WEBHOOK_URL_NOT_ALLOWED`: the sandbox only delivers to approved development hosts. `details.allowedOrigins` lists the accepted origins; `*` stands for any run of characters in the host or port.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`INTERNAL_ERROR`: the outcome is unknown. Retry with the same key, or look it up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/webhooks/test":{"post":{"tags":["Webhook testing"],"summary":"Send a test webhook","description":"Sends a signed `ping` to your webhook endpoint once. At most 10 per rolling minute. No Idempotency-Key.","security":[{"ApiKey":[]}],"responses":{"202":{"description":"Queued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookTestResult"}}}},"400":{"description":"Invalid request. `details.errors` lists every failing path; nothing was created and the key is not used up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`UNAUTHORIZED`: the API key is missing, invalid, rotated or revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"`WEBHOOK_URL_NOT_SET`: no webhook endpoint is set yet (`PUT /webhook-endpoint`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`RATE_LIMITED`: wait `Retry-After` seconds, then retry with the same key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`INTERNAL_ERROR`: the outcome is unknown. Retry with the same key, or look it up.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"504":{"description":"Gateway timeout: the outcome is unknown. The body is not JSON. Retry with the same key, or look it up."}}}}},"webhooks":{"payout.processing":{"post":{"summary":"Payout status PROCESSING","parameters":[{"schema":{"type":"string","description":"The event id. It stays the same across retries and redeliveries.","example":"evt_01J8Z3K4Q2V7XH5N9M6T0P1R2S"},"required":true,"description":"The event id. It stays the same across retries and redeliveries.","name":"webhook-id","in":"header"},{"schema":{"type":"string","description":"Unix seconds when this attempt was signed.","example":"1790000000"},"required":true,"description":"Unix seconds when this attempt was signed.","name":"webhook-timestamp","in":"header"},{"schema":{"type":"string","description":"Space-separated `v1,<base64>` entries: HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw body}`. Accept the request if any entry matches.","example":"v1,efUQhNDoZ1f7z5JyAKoWjb3jVxHyxqwQv6ch2w/y99o="},"required":true,"description":"Space-separated `v1,<base64>` entries: HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw body}`. Accept the request if any entry matches.","name":"webhook-signature","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayoutEvent"}}}},"responses":{"200":{"description":"Any 2xx acknowledges the event."}}}},"payout.in_review":{"post":{"summary":"Payout status IN_REVIEW","parameters":[{"schema":{"type":"string","description":"The event id. It stays the same across retries and redeliveries.","example":"evt_01J8Z3K4Q2V7XH5N9M6T0P1R2S"},"required":true,"description":"The event id. It stays the same across retries and redeliveries.","name":"webhook-id","in":"header"},{"schema":{"type":"string","description":"Unix seconds when this attempt was signed.","example":"1790000000"},"required":true,"description":"Unix seconds when this attempt was signed.","name":"webhook-timestamp","in":"header"},{"schema":{"type":"string","description":"Space-separated `v1,<base64>` entries: HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw body}`. Accept the request if any entry matches.","example":"v1,efUQhNDoZ1f7z5JyAKoWjb3jVxHyxqwQv6ch2w/y99o="},"required":true,"description":"Space-separated `v1,<base64>` entries: HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw body}`. Accept the request if any entry matches.","name":"webhook-signature","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayoutEvent"}}}},"responses":{"200":{"description":"Any 2xx acknowledges the event."}}}},"payout.settled":{"post":{"summary":"Payout status SETTLED","parameters":[{"schema":{"type":"string","description":"The event id. It stays the same across retries and redeliveries.","example":"evt_01J8Z3K4Q2V7XH5N9M6T0P1R2S"},"required":true,"description":"The event id. It stays the same across retries and redeliveries.","name":"webhook-id","in":"header"},{"schema":{"type":"string","description":"Unix seconds when this attempt was signed.","example":"1790000000"},"required":true,"description":"Unix seconds when this attempt was signed.","name":"webhook-timestamp","in":"header"},{"schema":{"type":"string","description":"Space-separated `v1,<base64>` entries: HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw body}`. Accept the request if any entry matches.","example":"v1,efUQhNDoZ1f7z5JyAKoWjb3jVxHyxqwQv6ch2w/y99o="},"required":true,"description":"Space-separated `v1,<base64>` entries: HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw body}`. Accept the request if any entry matches.","name":"webhook-signature","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayoutEvent"}}}},"responses":{"200":{"description":"Any 2xx acknowledges the event."}}}},"payout.failed":{"post":{"summary":"Payout status FAILED","parameters":[{"schema":{"type":"string","description":"The event id. It stays the same across retries and redeliveries.","example":"evt_01J8Z3K4Q2V7XH5N9M6T0P1R2S"},"required":true,"description":"The event id. It stays the same across retries and redeliveries.","name":"webhook-id","in":"header"},{"schema":{"type":"string","description":"Unix seconds when this attempt was signed.","example":"1790000000"},"required":true,"description":"Unix seconds when this attempt was signed.","name":"webhook-timestamp","in":"header"},{"schema":{"type":"string","description":"Space-separated `v1,<base64>` entries: HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw body}`. Accept the request if any entry matches.","example":"v1,efUQhNDoZ1f7z5JyAKoWjb3jVxHyxqwQv6ch2w/y99o="},"required":true,"description":"Space-separated `v1,<base64>` entries: HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw body}`. Accept the request if any entry matches.","name":"webhook-signature","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayoutEvent"}}}},"responses":{"200":{"description":"Any 2xx acknowledges the event."}}}},"payout.returned":{"post":{"summary":"Payout status RETURNED","parameters":[{"schema":{"type":"string","description":"The event id. It stays the same across retries and redeliveries.","example":"evt_01J8Z3K4Q2V7XH5N9M6T0P1R2S"},"required":true,"description":"The event id. It stays the same across retries and redeliveries.","name":"webhook-id","in":"header"},{"schema":{"type":"string","description":"Unix seconds when this attempt was signed.","example":"1790000000"},"required":true,"description":"Unix seconds when this attempt was signed.","name":"webhook-timestamp","in":"header"},{"schema":{"type":"string","description":"Space-separated `v1,<base64>` entries: HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw body}`. Accept the request if any entry matches.","example":"v1,efUQhNDoZ1f7z5JyAKoWjb3jVxHyxqwQv6ch2w/y99o="},"required":true,"description":"Space-separated `v1,<base64>` entries: HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw body}`. Accept the request if any entry matches.","name":"webhook-signature","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayoutEvent"}}}},"responses":{"200":{"description":"Any 2xx acknowledges the event."}}}},"ping":{"post":{"summary":"Test webhook","parameters":[{"schema":{"type":"string","description":"The event id. It stays the same across retries and redeliveries.","example":"evt_01J8Z3K4Q2V7XH5N9M6T0P1R2S"},"required":true,"description":"The event id. It stays the same across retries and redeliveries.","name":"webhook-id","in":"header"},{"schema":{"type":"string","description":"Unix seconds when this attempt was signed.","example":"1790000000"},"required":true,"description":"Unix seconds when this attempt was signed.","name":"webhook-timestamp","in":"header"},{"schema":{"type":"string","description":"Space-separated `v1,<base64>` entries: HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw body}`. Accept the request if any entry matches.","example":"v1,efUQhNDoZ1f7z5JyAKoWjb3jVxHyxqwQv6ch2w/y99o="},"required":true,"description":"Space-separated `v1,<base64>` entries: HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw body}`. Accept the request if any entry matches.","name":"webhook-signature","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PingEvent"}}}},"responses":{"200":{"description":"Any 2xx acknowledges the event."}}}}}}