# OutlinePay API reference

OutlinePay is a processor-agnostic payments API for merchant platforms looking to offer US payments. We provide headless capabilities for merchant management, checkout pages, payment links, refunds, disputes, and event webhooks.

This file is generated from the OpenAPI spec (https://outlinepay.com/openapi.json), the same source as the HTML reference at https://outlinepay.com/docs. Base URL: `https://api.outlinepay.com`. Test keys move no money.

## Conventions

- **Authentication** ([details](https://outlinepay.com/docs#conventions-auth)): send your secret key as a Bearer token, `Authorization: Bearer olp_sk_test_...`. Test keys start `olp_sk_test_` and move no money. Live keys start `olp_sk_live_` and move real money. A key is shown once at creation and stored hashed.
- **Errors** ([details](https://outlinepay.com/docs#conventions-errors)): every error returns a matching HTTP status and `{"error": {"code": "...", "details": [...]}}`. `code` classifies the refusal and is one of the values under Error codes below. `details` is never empty. Each entry has a `message`. It has a `param` when it is about exactly one request field, a `processor_message` when the processor refused a value we forwarded, and a `doc_url` when a docs section explains the fix. A response without `error.details` did not come from the API: treat it as a transient error and retry.
- **Idempotency** ([details](https://outlinepay.com/docs#conventions-idempotency)): send an `Idempotency-Key` header on any POST to make retries safe. It is required on Create a refund. The same key with the same body replays the stored response. The same key with a different body is `409 idempotency_conflict`. Keys are scoped to your account and mode and kept for 24 hours. A 5xx is not stored.
- **Pagination** ([details](https://outlinepay.com/docs#conventions-pagination)): lists are newest first. Pass `limit` (1 to 100, default 20) and `starting_after=<id>`. Responses are `{"data": [...], "has_more": bool}`.
- **Rate limits** ([details](https://outlinepay.com/docs#conventions-rate-limits)): 100 reads and 60 writes per minute per API key. Over the limit you get `429 rate_limited` with a `Retry-After` header in seconds.
- **Ids and values** ([details](https://outlinepay.com/docs#conventions-ids)): ids are prefixed and opaque (`pay_`, `link_`, `re_`, `mrc_`, `dp_`, `we_`, `evt_`), so store them as text. Amounts are integers in the currency's minor unit: `10000` is $100.00. Timestamps are ISO 8601 UTC strings.
- **Unknown parameters** ([details](https://outlinepay.com/docs#conventions-errors)): a parameter an endpoint does not document is refused with `400 invalid_request`, not ignored.

## Merchants

### POST /v1/merchants

**Create a merchant**

Creates a merchant and returns it with an empty `processors` list. Opening the payment-processor account is a separate step, Attach a processor.

Idempotency-Key: optional.

Request body (JSON):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| name | string | yes |  |
| email | string | yes |  |
| country | string | yes | The merchant's legal-entity country, ISO 3166-1 alpha-2. Where the business is registered, not where its customers are. Required, and never inferred, because it decides the onboarding requirements. `US` is the only country enabled by default. A merchant created with a country that is not yet enabled is accepted here and then refused when you attach a processor. Other countries are available on request, see https://outlinepay.com/docs/countries. |
| locale | enum | no | The language this merchant reads. It decides what language their onboarding form is rendered in and what language the payment processor writes to them in afterwards, so send it if you know it. Optional: leave it out and the merchant is onboarded in English. `en` and `ko` are what we support today. Anything else is rejected, not downgraded to English. Tell us which ones you need and we will add them. One of: `en`, `ko`. |
| entity_type | enum | no | Whether the merchant is a registered business or a person trading under their own name. One of: `individual`, `company`. |
| business | object | no | Commercial details about the merchant's business. Fields marked merchant-owned are the merchant's to edit once they have onboarded, so they only ever travel inward. The details you already hold. Stored and returned on the merchant in full. The forward-only `tax_id` and `representative` are not accepted here: you supply them when you attach a processor. |
| business.legal_name | string | no | Registered legal name, if it differs from `name`. |
| business.structure | enum | no | The merchant's legal structure, in OutlinePay's processor-agnostic vocabulary, translated to your processor's own value. Applies to a company. An individual has none. These map to, in order: `sole_proprietorship`, `single_member_llc`, `multi_member_llc`, `private_corporation`, `private_partnership`. One of: `sole_proprietor`, `llc`, `llc_multi`, `corporation`, `partnership`. |
| business.phone | string | no | Business phone. Use E.164: '+', the country code, then the subscriber number, e.g. +12125550123 (US) or +821012345678 (KR). A phone no processor could parse is refused as a 400 `invalid_request` naming this field. |
| business.url | string | no | Public website: a full http(s) URL with a domain name, e.g. https://hanui.example. The processor additionally refuses placeholder and shared domains (example.com and the like) when it is attached. |
| business.product_description | string | no | One or two sentences on what the merchant sells. |
| business.industry_code | string | no | Four-digit merchant category code (MCC) classifying what the business sells: the card networks' ISO 18245 standard, not ours. Common examples: 5399 general merchandise, 5812 restaurants, 5691 clothing, 5734 software. Omit it and the processor infers one from the website and product description. We accept the codes listed at https://outlinepay.com/docs/mccs and refuse any other value naming this field. |
| business.statement_descriptor | string | no | What buyers see on their card statement. 5 to 22 characters, at least one Latin letter, no angle brackets. Keep it recognisable, or it drives chargebacks. |
| business.address | object | no |  |
| business.address.line1 | string | no |  |
| business.address.line2 | string | no |  |
| business.address.city | string | no |  |
| business.address.state | string | no | Two-letter state or province code. For a US address, the USPS code. The processor cannot serve US insular areas (GU, VI, AS, MP). |
| business.address.postal_code | string | no | Postal code. For a US address, 5 digits or ZIP+4, e.g. 19104 or 19104-2617. |
| business.address.country | string | no | Two-letter ISO country code, required whenever an address is sent. This is the country of *this address*: a business address must be in the merchant's own country (the top-level `country` on the merchant), while a representative's home address may be anywhere. |
| metadata | object | no | Your own key/value pairs, for correlating this object back to your records. We never interpret the contents: they are stored, returned on every read, and carried on every event this object emits. A payment's, refund's or payment link's is also forwarded to the processor. A merchant's is not. At most 50 pairs, keys up to 40 characters, values up to 500. Values are stored as strings. Keys beginning `outlinepay_`, plus `outline_fee` and `partner_fee`, are reserved by OutlinePay and rejected. Never put secrets here. Your own key/value pairs, such as your id for this seller. Kept by OutlinePay and never sent to the processor. |

Notes:

- **Prefill.** business is commercial detail you already hold. We store it and hand it to the processor later so onboarding stops asking the merchant for it. All of it is returned on the merchant.
- **Not accepted here.** The forward-only `tax_id` and `representative`. You supply those when you attach a processor, because we never store them.
- **Accuracy.** The merchant, not you, confirms to the processor that these details are true, and every value you send stays editable during onboarding. Tell them which values came from you so they can check them.

Emits: `merchant.created`.

Returns a Merchant object.

### GET /v1/merchants

**List merchants**

Your merchants, newest first. Filter by `status` and creation time.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| status | query, enum | no | Where the merchant stands with its processors: the field to read for whether it can charge today. `created`: no processor has verified the merchant yet. This one value covers both a merchant that has not started onboarding and one whose submission is still under review. To tell those apart, read `processors[].onboarding_completed`. `active`: a processor has verified the merchant and it can accept payments. `restricted`: a processor verified the merchant earlier but has since limited the account, usually because it now wants more information. Only the merchant can clear it, by answering the processor in the embedded components. A merchant that was never verified stays `created`, and `processors[].verified` never returns to false, so `restricted` is the only signal that a merchant which used to be chargeable no longer is. The API does not carry the reason for a restriction. The processor delivers it to the merchant in the `notifications` component, which is also the only place the merchant can answer it. So when you see `restricted`, put the merchant in front of `notifications` and `account_management` (see Create an account session). We emit `merchant.updated` when the `status` returns to `active`. Only merchants in this state. One of: `created`, `active`, `restricted`. |
| created_after | query, string | no | Only records whose `created_at` is at or after this ISO 8601 timestamp or date. Inclusive. |
| created_before | query, string | no | Only records whose `created_at` is before this ISO 8601 timestamp or date. Exclusive, so a month's `created_before` is the next month's `created_after`. |
| limit | query, integer | no | Page size, 1–100 (default 20). |
| starting_after | query, string | no | A resource id. Returns records after it (newest-first cursor). |

Returns a page of Merchant objects: `{"data": [...], "has_more": bool}`.

### GET /v1/merchants/{id}

**Retrieve a merchant**

The authoritative merchant record, including per-processor onboarding/verification state.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| id | path, string | yes | The merchant id. |

Returns a Merchant object.

### PATCH /v1/merchants/{id}

**Update a merchant**

Updates the merchant's processor-agnostic business identity. Changes travel the other way too, with no call from you: when the merchant edits their details at the processor, we write them back here automatically.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| id | path, string | yes | The merchant id. |

Request body (JSON):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| entity_type | enum | no | Set-only. Sending it blank does not clear it. One of: `individual`, `company`. |
| locale | enum | no | The language this merchant reads. Change it when you learn you had it wrong, or when the merchant asks: it was previously fixed at creation, and a merchant who signed up in the wrong language stayed in it. Follows this endpoint's merge rule. Omit it and the language is left alone. Send `en` or `ko` to set it. Send it blank to clear it, which returns the merchant to being onboarded in English. It takes effect on the next account session you create. Components already on screen keep the locale they were initialised with, so by default the merchant sees the change on their next page load. If you want it sooner, the embedded components can be re-translated in place without re-mounting: call `stripeConnectInstance.update({locale})` with the `locale` from a freshly created account session. Nothing is sent to the processor when this changes, because the processor holds no copy of it. One of: `en`, `ko`. |
| business | object | no | The business fields a partial update may touch. The identity fields to set or clear. |
| business.legal_name | string | no | Registered legal name, if it differs from `name`. |
| business.structure | enum | no | The merchant's legal structure, in OutlinePay's processor-agnostic vocabulary (company only). See MerchantBusiness for what each maps to. One of: `sole_proprietor`, `llc`, `llc_multi`, `corporation`, `partnership`. |
| business.url | string | no | Public website: a full http(s) URL with a domain name, e.g. https://hanui.example. The processor additionally refuses placeholder and shared domains (example.com and the like) when it is attached. |
| business.address | object | no |  |
| business.address.line1 | string | no |  |
| business.address.line2 | string | no |  |
| business.address.city | string | no |  |
| business.address.state | string | no | Two-letter state or province code. For a US address, the USPS code. The processor cannot serve US insular areas (GU, VI, AS, MP). |
| business.address.postal_code | string | no | Postal code. For a US address, 5 digits or ZIP+4, e.g. 19104 or 19104-2617. |
| business.address.country | string | no | Two-letter ISO country code, required whenever an address is sent. This is the country of *this address*: a business address must be in the merchant's own country (the top-level `country` on the merchant), while a representative's home address may be anywhere. |
| business.phone | string | no | Business phone. Use E.164: '+', the country code, then the subscriber number, e.g. +12125550123 (US) or +821012345678 (KR). A phone no processor could parse is refused as a 400 `invalid_request` naming this field. |
| business.product_description | string | no | One or two sentences on what the merchant sells. |
| business.industry_code | string | no | Four-digit merchant category code (MCC) classifying what the business sells: the card networks' ISO 18245 standard, not ours. Common examples: 5399 general merchandise, 5812 restaurants, 5691 clothing, 5734 software. Omit it and the processor infers one from the website and product description. We accept the codes listed at https://outlinepay.com/docs/mccs and refuse any other value naming this field. |
| business.statement_descriptor | string | no | What buyers see on their card statement. 5 to 22 characters, at least one Latin letter, no angle brackets. Keep it recognisable, or it drives chargebacks. |
| metadata | object | no | Your own key/value pairs, for correlating this object back to your records. We never interpret the contents: they are stored, returned on every read, and carried on every event this object emits. A payment's, refund's or payment link's is also forwarded to the processor. A merchant's is not. At most 50 pairs, keys up to 40 characters, values up to 500. Values are stored as strings. Keys beginning `outlinepay_`, plus `outline_fee` and `partner_fee`, are reserved by OutlinePay and rejected. Never put secrets here. Merged key by key under this endpoint's rule. Send a key with a value to set it, or blank to remove it. Keys you leave out are kept. Changing only metadata makes no call to the processor. |

Notes:

- **Merge semantics.** A field you omit is left unchanged, a field you send is set, and a field you send blank is cleared.
- **Merchant-owned.** `product_description`, `industry_code`, `statement_descriptor` and `phone` are editable here only until a processor is attached. From then on the merchant owns them: they are edited in the account-management component and reach us through the inbound sync instead.
- **Not accepted here.** The forward-only `tax_id` and `representative`, which belong to the attach endpoint.
- **Propagation.** Stored here, then pushed to every attached processor account, matching fields best-effort: each processor receives the ones it models and anything it cannot model is still stored here, just not forwarded. Today `legal_name`, `structure` and `address` reach the processor only for a company, since an individual's identity is held as a person rather than a business.

Emits: `merchant.updated`.

Returns a Merchant object.

### POST /v1/merchants/{merchant_id}/processors

**Attach a processor**

Opens the merchant's connected payment-processor account, the step that makes a merchant chargeable.

Idempotency-Key: optional.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| merchant_id | path, string | yes | The merchant id. |

Request body (JSON):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| processor | string | no | Which processor to attach. Currently only `stripe`. |
| tax_id | string | no | Business tax identifier: the EIN for a US business (nine digits, e.g. 12-3456789). Sent to the processor to satisfy its verification. |
| representative | object | no | The person who controls the business. Every field here is forward-only: we pass it to the processor so onboarding stops asking, and keep none of it. Do not send a national ID (SSN). The processor collects that from the merchant directly. |
| representative.first_name | string | no |  |
| representative.last_name | string | no |  |
| representative.email | string | no |  |
| representative.phone | string | no | Use E.164: '+', the country code, then the subscriber number, e.g. +12125550123 (US) or +821012345678 (KR). A phone no processor could parse is refused as a 400 `invalid_request` naming this field. |
| representative.date_of_birth | string | no | ISO 8601 date, YYYY-MM-DD, e.g. 1985-04-27. Must be a past date at least 13 years back, the processor's minimum age for a representative. |
| representative.address | object | no | The representative's home address. |
| representative.address.line1 | string | no |  |
| representative.address.line2 | string | no |  |
| representative.address.city | string | no |  |
| representative.address.state | string | no | Two-letter state or province code. For a US address, the USPS code. The processor cannot serve US insular areas (GU, VI, AS, MP). |
| representative.address.postal_code | string | no | Postal code. For a US address, 5 digits or ZIP+4, e.g. 19104 or 19104-2617. |
| representative.address.country | string | no | Two-letter ISO country code, required whenever an address is sent. This is the country of *this address*: a business address must be in the merchant's own country (the top-level `country` on the merchant), while a representative's home address may be anywhere. |

Notes:

- **Forward-only identity.** This is where `tax_id` and the representative are supplied. We pass them to the processor and then discard them: never written down, never returned, so they cannot appear in a later GET, an event payload or a backup.
- **Effect on onboarding.** Combined with the business stored at creation, it takes a US company's onboarding from 15 outstanding requirements down to 4. The merchant is left with its bank account, the last four digits of the representative's national ID, and the terms of service.
- **What happens next.** The account is open but restricted, so this call does not make the merchant chargeable on its own: `onboarding_completed` and `verified` both come back false and `status` stays `created`. Create an account session next and render the onboarding component, which is where the processor collects what it still needs. Until that finishes, charges are refused with 409 `merchant_not_onboarded`.
- **If the representative is rejected.** For a company, opening the account and registering the representative are two steps at the processor, so a representative it refuses can arrive after the account has opened. The account then stays attached: the 400 says so, and the merchant lists the processor with `representative_prefilled` false. Send the same request again with a corrected representative. The open account is reused, so retries never create a second one. The retry reads only the representative: the stored business went with the account when it opened, and a `tax_id` it did not carry is collected during onboarding.
- **Already attached.** A merchant that already has an account for the given processor comes back as a 409, unless that account is still waiting for its representative, which resumes instead. The 409 message says which.
- **Default rail.** The first processor a merchant attaches becomes its `default_processor`, used whenever a charge names none. It does not change on its own when you attach another.

Emits: `merchant.updated`.

Returns a Merchant object.

### POST /v1/merchants/{id}/account_session

**Create an account session**

Returns a short-lived `client_secret` your page hands to the processor's embedded components, plus the publishable key and the merchant's locale. Your server is the authorization gate. It decides which logged-in merchant a session is for. See `components` below for the full set.

This is the step that makes an attached merchant chargeable: the onboarding component is the only place a merchant completes its verification, so nothing can be charged until it has been rendered and finished. See the Quickstart for a worked example of both halves.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| id | path, string | yes | The merchant id. |

Request body (JSON):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| components | array<string> | yes | Which components to scope the session to, in OutlinePay's processor-agnostic vocabulary. Required: a session grants exactly what you name here, so name only what the page mounts. Note: your browser still mounts each one with the processor's own SDK (for Stripe, Connect.js), which uses its own component ids. |

Notes:

- **Attach a processor first.** A session is scoped to the merchant's connected account, so it needs one to exist. Without it this returns 400 `invalid_request`, "This merchant has no processor yet. Attach one first."
- **What to do with the client_secret.** Hand it back to your own page and give it to the processor's SDK, then create the onboarding component. Your server makes this call so your secret key never reaches the browser, and your page asks an endpoint of yours for the secret.
- **Mint a fresh one every time.** The session is short-lived and the SDK asks for a new secret by itself when it lapses mid-form, which is why it takes a function rather than a string. Point that function at an endpoint that calls this one afresh. Handing back a cached secret strands the merchant partway through onboarding.
- **Do not send an Idempotency-Key.** Idempotency keys are ignored here: a replayed response would carry an expired `client_secret`, and creating a second session is harmless.
- **How you know onboarding finished.** `processors[].onboarding_completed` turns true when the merchant submits, and `processors[].verified` turns true when the processor accepts. The merchant's `status` turns `active` at that moment and we emit `merchant.updated`. The component's own callbacks mean the form closed, not that the merchant was accepted, so read the merchant.
- **Not gated on onboarding.** This endpoint is not refused for a merchant that has not onboarded, since it is the way out of a 409 `merchant_not_onboarded`.
- **The onboarding component shows our merchant terms first.** When the response's `terms.required` is true, outlinepay.js frames `terms.url` ahead of the processor's form and the merchant accepts the OutlinePay merchant terms there, once, for every processor. The merchant object's `terms_acceptance` and a `merchant.updated` event record it. If your Content-Security-Policy sets frame-src, allow the origin of `terms.url`.

Returns:

| Field | Type | Description |
| --- | --- | --- |
| object | string | The literal string `account_session`. |
| processor | string | The processor these credentials belong to, e.g. `stripe`. |
| client_secret | string |  |
| publishable_key | string |  |
| components | array<string> |  |
| locale | string | The merchant's language, spelled the way this processor's SDK wants it. Pass it straight through when you initialise the components. It is not the tag you sent us on the merchant: Stripe's embedded components accept only regional codes, so a merchant stored as `ko` comes back here as `ko-KR`, and a language the SDK cannot render falls back to `en-US` rather than failing. Setting the merchant's locale alone does not translate the form. The SDK has to be told. |
| terms | object | Whether this merchant still has to accept the OutlinePay merchant terms, and where they are shown. outlinepay.js reads this on its own: when `required` is true, mounting the onboarding component shows the terms screen at `url` first, and the processor's form after the merchant agrees. Keep forwarding this response body verbatim. `url` is short-lived and scoped to this one merchant, minted fresh with every session. If your Content-Security-Policy sets frame-src, allow the origin of `url`. |
| terms.required | boolean | True until the merchant has accepted the current terms version. |
| terms.url | string | The terms screen to frame, or null when required is false. |

## Payments

### POST /v1/payments

**Create a payment**

Creation is never a payment outcome. The response is status `awaiting_payment` plus an OutlinePay-owned `checkout_url`. Whoever opens it completes the payment on the processor's hosted page. The terminal state (`succeeded`/`failed`/`expired`) arrives by webhook, with Retrieve a payment as the polling fallback. Point `success_url` and `cancel_url` at endpoints you control. The redirect is navigation, not proof of payment.

Idempotency-Key: optional.

Request body (JSON):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| merchant_id | string | yes |  |
| amount | integer | yes | Minor units of `currency` (for `usd`, cents). The minimum charge is 100 ($1.00): a lower amount is refused as a 400 `invalid_request` naming `amount`, before the processor is called. |
| currency | enum | no | What the customer is charged in, as distinct from what the merchant is paid in, unless you turn on `localized_pricing`. `usd` is the only value we accept today, and it is the default, so you can leave it out. Contact us if you need another. One of: `usd`. |
| description | string | no |  |
| customer | object | no | Who is paying. Required (with `email`) whenever `payment_methods` includes `bank_transfer`, optional otherwise. The email keys the buyer's identity, so repeat buyers keep the same virtual account number. |
| customer.email | string | no |  |
| success_url | string | yes | Where the processor sends the browser after payment. |
| cancel_url | string | no | Where the processor sends the browser on cancel. |
| processor | string | no | Which rail to charge on. Defaults to the merchant's default processor. |
| payment_methods | array<string> | no | Restrict the checkout to these method categories (an allow-list). Omit for the documented default, `["card", "wallet"]`. Naming `bank_transfer` requires `customer.email`. |
| bank_verification | enum | no | How a customer's bank account must be proven to be theirs before it can be debited. Applies to `bank_debit` and is ignored on a checkout that offers no bank method. `instant_or_delayed` (default): the customer links their bank instantly, or, if they'd rather not or their bank can't be reached, enters the account number and confirms two small deposits instead. That second path takes 1-2 business days for the deposits to arrive and gives them 10 days to enter them, so the payment can sit in `processing` for a fortnight. Accepts the most customers. `instant`: instant linking only. You learn within seconds whether the account is good. A customer whose bank cannot be linked this way cannot pay by bank at all. Only meaningful when the checkout offers `bank_debit`. Omit it and you get `instant_or_delayed`. One of: `instant_or_delayed`, `instant`. |
| invoice | object | no | Ask the payment processor to issue an invoice once this payment succeeds. Off unless you send it, because the processor charges the merchant per invoice. It cannot be added after the payment is created. The finished document is `invoice_url` on the payment, and what it cost the merchant is `fees.invoice`. |
| invoice.enabled | boolean | no | Must be true or false. Anything else is refused. |
| localized_pricing | object | no | Let the payment processor present the price in the customer's own currency when they are outside the US. Off unless you send it: the converted figure the customer sees includes the processor's conversion fee, 2 to 4% with Stripe, paid by the customer, and the customer can still choose to pay in `currency`. The merchant receives `amount` in `currency` either way, and the payment then records what the customer paid as `presentment`. Fixed at creation. |
| localized_pricing.enabled | boolean | no | Must be true or false. Anything else is refused. |
| metadata | object | no | Your own key/value pairs, for correlating this object back to your records. We never interpret the contents: they are stored, returned on every read, and carried on every event this object emits. A payment's, refund's or payment link's is also forwarded to the processor. A merchant's is not. At most 50 pairs, keys up to 40 characters, values up to 500. Values are stored as strings. Keys beginning `outlinepay_`, plus `outline_fee` and `partner_fee`, are reserved by OutlinePay and rejected. Never put secrets here. |

Notes:

- **Onboarding required.** The merchant must have finished onboarding before you can charge it. Until the processor verifies the account this returns 409 `merchant_not_onboarded` and no payment is created. Put the merchant through Create an account session first. Its `status` turns `active` and we emit `merchant.updated` when it can take money.

Returns a Payment object.

### GET /v1/payments

**List payments**

Your payments, newest first. Filter by `merchant_id`, `status` and creation time.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| merchant_id | query, string | no | Only payments for this merchant. |
| status | query, enum | no | Where the payment is. Both lifecycles are drawn under The Payment object. `awaiting_payment`: the customer has not finished. A declined card does not end this state. The customer retries on the checkout page. `processing`: the customer has paid but the money is still moving. Only delayed methods (`bank_debit`, `bank_transfer`) reach it, it can last days, and nothing is decided yet, so do not fulfill on it. `failed`: reachable only from `processing`, so only on delayed methods. `failure_reason` says why. Only payments in this state. One of: `awaiting_payment`, `processing`, `succeeded`, `failed`, `expired`. |
| created_after | query, string | no | Only records whose `created_at` is at or after this ISO 8601 timestamp or date. Inclusive. |
| created_before | query, string | no | Only records whose `created_at` is before this ISO 8601 timestamp or date. Exclusive, so a month's `created_before` is the next month's `created_after`. |
| limit | query, integer | no | Page size, 1–100 (default 20). |
| starting_after | query, string | no | A resource id. Returns records after it (newest-first cursor). |

Returns a page of Payment objects: `{"data": [...], "has_more": bool}`.

### GET /v1/payments/{id}

**Retrieve a payment**

The authoritative status of a payment, the polling fallback to the webhook.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| id | path, string | yes | The payment id. |

Returns a Payment object.

## Payment Links

### POST /v1/payment_links

**Create a payment link**

Returns a permanent OutlinePay-owned URL. No processor session is minted here. The first click does that. Each completed payment appears as its own payment object with its own webhook.

Idempotency-Key: optional.

Request body (JSON):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| merchant_id | string | yes |  |
| amount | integer | yes | Minor units of `currency` (for `usd`, cents). Minimum 100 ($1.00), refused as a 400 `invalid_request` below it, as on Create a payment. |
| currency | enum | no | What the customer is charged in, fixed for the life of the link, unless you turn on `localized_pricing`. `usd` is the only value we accept today, and it is the default. One of: `usd`. |
| description | string | no |  |
| processor | string | no | Which rail this link charges on. Defaults to the merchant's default processor, and is fixed for the life of the link. |
| payment_methods | array<string> | no | Restrict every session and payment this link mints to these method categories (an allow-list), fixed for the life of the link. Omit for the documented default, `["card", "wallet"]`. `bank_transfer` is not accepted here: a virtual account number is issued to a known buyer, and a link has no buyer until someone opens it. Create payments instead. |
| bank_verification | enum | no | How a customer's bank account must be proven to be theirs before it can be debited. Applies to `bank_debit` and is ignored on a checkout that offers no bank method. `instant_or_delayed` (default): the customer links their bank instantly, or, if they'd rather not or their bank can't be reached, enters the account number and confirms two small deposits instead. That second path takes 1-2 business days for the deposits to arrive and gives them 10 days to enter them, so the payment can sit in `processing` for a fortnight. Accepts the most customers. `instant`: instant linking only. You learn within seconds whether the account is good. A customer whose bank cannot be linked this way cannot pay by bank at all. Only meaningful when the link offers `bank_debit`. Fixed for the life of the link. Omit it and you get `instant_or_delayed`. One of: `instant_or_delayed`, `instant`. |
| invoice | object | no | Ask the payment processor to issue an invoice for every payment this link takes, once each one succeeds. The same option as on Create a payment, fixed for the life of the link. Off unless you send it, because the processor charges the merchant per invoice. Each payment the link mints then carries `invoice_url` and `fees.invoice`. |
| invoice.enabled | boolean | no | Must be true or false. Anything else is refused. |
| localized_pricing | object | no | The same option as on Create a payment, fixed for the life of the link: let the processor present the price in the customer's own currency, with the conversion paid by the customer, and record what they paid as `presentment` on each payment. Off unless you send it. |
| localized_pricing.enabled | boolean | no | Must be true or false. Anything else is refused. |
| metadata | object | no | Your own key/value pairs, for correlating this object back to your records. We never interpret the contents: they are stored, returned on every read, and carried on every event this object emits. A payment's, refund's or payment link's is also forwarded to the processor. A merchant's is not. At most 50 pairs, keys up to 40 characters, values up to 500. Values are stored as strings. Keys beginning `outlinepay_`, plus `outline_fee` and `partner_fee`, are reserved by OutlinePay and rejected. Never put secrets here. |

Notes:

- **Onboarding required.** Gated like Create a payment: until the processor verifies the account it returns 409 `merchant_not_onboarded` and no link is created.

Emits: `payment_link.created`.

Returns a PaymentLink object.

### GET /v1/payment_links

**List payment links**

Your payment links, newest first. Filter by `merchant_id`, `status` and creation time.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| merchant_id | query, string | no | Only links for this merchant. |
| status | query, enum | no | Only links in this state. One of: `active`, `deactivated`. |
| created_after | query, string | no | Only records whose `created_at` is at or after this ISO 8601 timestamp or date. Inclusive. |
| created_before | query, string | no | Only records whose `created_at` is before this ISO 8601 timestamp or date. Exclusive, so a month's `created_before` is the next month's `created_after`. |
| limit | query, integer | no | Page size, 1–100 (default 20). |
| starting_after | query, string | no | A resource id. Returns records after it (newest-first cursor). |

Returns a page of PaymentLink objects: `{"data": [...], "has_more": bool}`.

### GET /v1/payment_links/{id}

**Retrieve a payment link**

The authoritative state of a link, including whether it is still active.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| id | path, string | yes | The payment link id. |

Returns a PaymentLink object.

### POST /v1/payment_links/{id}/deactivate

**Deactivate a payment link**

Deactivates the link. Later clicks get a "no longer active" page, and a checkout a buyer already opened from it stops accepting payment.

Idempotency-Key: optional.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| id | path, string | yes | The payment link id. |

Emits: `payment_link.deactivated`.

Returns a PaymentLink object.

## Refunds

### POST /v1/refunds

**Create a refund**

Refunds a succeeded payment. Usually pending here and resolved by the `refund.completed` / `refund.failed` webhook. The platform fee is not returned.

Idempotency-Key: required.

Request body (JSON):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| payment_id | string | yes |  |
| amount | integer | no | Omit to refund what is still refundable. |
| metadata | object | no | Your own key/value pairs, for correlating this object back to your records. We never interpret the contents: they are stored, returned on every read, and carried on every event this object emits. A payment's, refund's or payment link's is also forwarded to the processor. A merchant's is not. At most 50 pairs, keys up to 40 characters, values up to 500. Values are stored as strings. Keys beginning `outlinepay_`, plus `outline_fee` and `partner_fee`, are reserved by OutlinePay and rejected. Never put secrets here. |

Notes:

- **Amount is in the smallest currency unit.** An integer, like every amount in this API: 2500 is $25.00 and 1 is one cent. Omit it to refund the payment's `amount_refundable`, which is what is still owed, not its amount. Several partial refunds are allowed as long as they add up to no more than that.
- **Only succeeded payments.** A payment that never succeeded refuses with 400 `invalid_request`. A fully refunded payment has `amount_refundable` 0 and refuses the same way.

Emits: `refund.completed`.

Returns a Refund object.

### GET /v1/refunds

**List refunds**

Your refunds across every payment, newest first. A payment already carries its own refunds inline, so use this when you do not start from a payment, such as reconciling a day's returns. Filter by `merchant_id`, `payment_id`, `status` and creation time.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| merchant_id | query, string | no | Only refunds of this merchant's payments. |
| payment_id | query, string | no | Only refunds against this payment. |
| status | query, enum | no | `failed` also covers a refund the processor withdrew rather than refused, so the two are told apart by `failure_reason`, not by the status. Only refunds in this state. One of: `pending`, `succeeded`, `failed`. |
| created_after | query, string | no | Only records whose `created_at` is at or after this ISO 8601 timestamp or date. Inclusive. |
| created_before | query, string | no | Only records whose `created_at` is before this ISO 8601 timestamp or date. Exclusive, so a month's `created_before` is the next month's `created_after`. |
| limit | query, integer | no | Page size, 1–100 (default 20). |
| starting_after | query, string | no | A resource id. Returns records after it (newest-first cursor). |

Notes:

- **Two views of the same refund.** A payment's refunds[] array is the lean view (`id`, `status`, `amount`, `metadata`, `created_at`). This endpoint returns the full Refund object, which additionally names its payment, merchant, mode and processor. The ids match, so either can be followed to the other.

Returns a page of Refund objects: `{"data": [...], "has_more": bool}`.

### GET /v1/refunds/{id}

**Retrieve a refund**

The authoritative state of a refund, the polling fallback to the webhook.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| id | path, string | yes | The refund id. |

Returns a Refund object.

## Disputes

### GET /v1/disputes

**List disputes**

Your disputes, newest first. Usually empty: a dispute exists only when a cardholder has opened a chargeback. Filter by `merchant_id`, `payment_id`, `status` and creation time.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| merchant_id | query, string | no | Only disputes against this merchant's payments. |
| payment_id | query, string | no | Only disputes against this payment. |
| status | query, enum | no | The chargeback's state, in OutlinePay's vocabulary and translated from whichever processor reported it. The `inquiry_*` set is the card networks' early retrieval request, raised before a formal chargeback. The rest is the real lifecycle, terminating in `won`/`lost`, or `refunded` when the merchant refunds instead of contesting. Only disputes in this state. One of: `inquiry_needs_response`, `inquiry_under_review`, `inquiry_closed`, `needs_response`, `under_review`, `won`, `lost`, `refunded`. |
| created_after | query, string | no | Only records whose `created_at` is at or after this ISO 8601 timestamp or date. Inclusive. |
| created_before | query, string | no | Only records whose `created_at` is before this ISO 8601 timestamp or date. Exclusive, so a month's `created_before` is the next month's `created_after`. |
| limit | query, integer | no | Page size, 1–100 (default 20). |
| starting_after | query, string | no | A resource id. Returns records after it (newest-first cursor). |

Returns a page of Dispute objects: `{"data": [...], "has_more": bool}`.

### GET /v1/disputes/{id}

**Retrieve a dispute**

A single dispute, including its evidence deadline. Read-only.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| id | path, string | yes | The dispute id. |

Returns a Dispute object.

## Events

### GET /v1/events

**List events**

Everything OutlinePay has emitted for you, newest first. Replay past events any time. Filter by `type` and creation time.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| type | query, enum | no | The outbound event catalog. `payment.updated` is the only type that is not a change of state. It fires a moment after a payment's outcome, carrying the facts that can only be read off the charge: the processor's own fee, the method that settled, and the card behind a card payment. All are still null on the outcome event itself, so record net from `payment.updated`, not from `payment.succeeded`. The full rule is under "Fees arrive just after the outcome" in Conventions. Only events of this type. One of: `merchant.created`, `merchant.updated`, `payment.processing`, `payment.succeeded`, `payment.failed`, `payment.expired`, `payment.updated`, `payment_link.created`, `payment_link.deactivated`, `refund.completed`, `refund.failed`, `dispute.created`, `dispute.updated`, `dispute.closed`. |
| created_after | query, string | no | Only records whose `created_at` is at or after this ISO 8601 timestamp or date. Inclusive. |
| created_before | query, string | no | Only records whose `created_at` is before this ISO 8601 timestamp or date. Exclusive, so a month's `created_before` is the next month's `created_after`. |
| limit | query, integer | no | Page size, 1–100 (default 20). |
| starting_after | query, string | no | A resource id. Returns records after it (newest-first cursor). |

Returns a page of Event objects: `{"data": [...], "has_more": bool}`.

### GET /v1/events/{id}

**Retrieve an event**

A single event exactly as it was emitted, for replaying or verifying a delivery.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| id | path, string | yes | The event id. |

Returns a Event object.

## Webhook Endpoints

### POST /v1/webhook_endpoints

**Create a webhook endpoint**

Subscribes an HTTPS URL to your events. The response reveals the signing secret exactly once. Store it, and use it to verify the `OutlinePay-Signature` header on every delivery. Each mode allows up to 16 active endpoints. Past that, creation fails with `invalid_request` until you delete one.

Idempotency-Key: optional.

Request body (JSON):

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| url | string | yes | An HTTPS URL to receive events. |

Returns a WebhookEndpoint object.

### GET /v1/webhook_endpoints

**List webhook endpoints**

Your endpoints, newest first. The signing secret is never included here. Filter by creation time.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| created_after | query, string | no | Only records whose `created_at` is at or after this ISO 8601 timestamp or date. Inclusive. |
| created_before | query, string | no | Only records whose `created_at` is before this ISO 8601 timestamp or date. Exclusive, so a month's `created_before` is the next month's `created_after`. |
| limit | query, integer | no | Page size, 1–100 (default 20). |
| starting_after | query, string | no | A resource id. Returns records after it (newest-first cursor). |

Returns a page of WebhookEndpoint objects: `{"data": [...], "has_more": bool}`.

### DELETE /v1/webhook_endpoints/{id}

**Delete a webhook endpoint**

Deletes the endpoint. Deliveries stop immediately, it disappears from the list, its slot under the 16-endpoint cap frees up, and its id is gone: a repeat call answers 404. There is no undo. To start delivering to the same URL again, create a new endpoint, which will have a new signing secret.

Parameters:

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| id | path, string | yes | The webhook endpoint id. |

Returns:

| Field | Type | Description |
| --- | --- | --- |
| id | string |  |
| object | string | The type of the resource that was deleted. |
| deleted | boolean | Always true. |

## Objects

### The Merchant object

The businesses you accept payments for. A merchant becomes chargeable in three steps, in this order: create it, attach a processor to open its account, then have the merchant finish onboarding in an embedded component on your page. Charges are refused until the third step is done.

| Field | Type | Description |
| --- | --- | --- |
| id | string |  |
| object | string | The literal string `merchant`. |
| name | string |  |
| email | string |  |
| status | enum | Where the merchant stands with its processors: the field to read for whether it can charge today. `created`: no processor has verified the merchant yet. This one value covers both a merchant that has not started onboarding and one whose submission is still under review. To tell those apart, read `processors[].onboarding_completed`. `active`: a processor has verified the merchant and it can accept payments. `restricted`: a processor verified the merchant earlier but has since limited the account, usually because it now wants more information. Only the merchant can clear it, by answering the processor in the embedded components. A merchant that was never verified stays `created`, and `processors[].verified` never returns to false, so `restricted` is the only signal that a merchant which used to be chargeable no longer is. The API does not carry the reason for a restriction. The processor delivers it to the merchant in the `notifications` component, which is also the only place the merchant can answer it. So when you see `restricted`, put the merchant in front of `notifications` and `account_management` (see Create an account session). We emit `merchant.updated` when the `status` returns to `active`. One of: `created`, `active`, `restricted`. |
| mode | enum | The world this object lives in, test (sandbox) or live. Matches your API key's prefix (olp_sk_test_ / olp_sk_live_). One of: `test`, `live`. |
| country | string | The merchant's legal-entity country, ISO 3166-1 alpha-2. Where the business is registered, not where its customers are. US today. Other countries are available on request. |
| locale | string, nullable | The language this merchant's onboarding renders in, or null if none was given (which onboards in English). |
| entity_type | enum, nullable | The legal entity type, if any. One of: `individual`, `company`. |
| default_processor | string, nullable | The rail a charge uses when the request names none. Null until the first processor is attached, which becomes the default. It never changes on its own after that. |
| business | object, nullable | Commercial details about the merchant's business. Fields marked merchant-owned are the merchant's to edit once they have onboarded, so they only ever travel inward. What you supplied at creation, updated when the merchant edits its details at the processor, so it can change without a call from you. Only business-level data is kept. Representative details are forwarded to the processor and never stored, so they are never returned here. |
| business.legal_name | string | Registered legal name, if it differs from `name`. |
| business.structure | enum | The merchant's legal structure, in OutlinePay's processor-agnostic vocabulary, translated to your processor's own value. Applies to a company. An individual has none. These map to, in order: `sole_proprietorship`, `single_member_llc`, `multi_member_llc`, `private_corporation`, `private_partnership`. One of: `sole_proprietor`, `llc`, `llc_multi`, `corporation`, `partnership`. |
| business.phone | string | Business phone. Use E.164: '+', the country code, then the subscriber number, e.g. +12125550123 (US) or +821012345678 (KR). A phone no processor could parse is refused as a 400 `invalid_request` naming this field. |
| business.url | string | Public website: a full http(s) URL with a domain name, e.g. https://hanui.example. The processor additionally refuses placeholder and shared domains (example.com and the like) when it is attached. |
| business.product_description | string | One or two sentences on what the merchant sells. |
| business.industry_code | string | Four-digit merchant category code (MCC) classifying what the business sells: the card networks' ISO 18245 standard, not ours. Common examples: 5399 general merchandise, 5812 restaurants, 5691 clothing, 5734 software. Omit it and the processor infers one from the website and product description. We accept the codes listed at https://outlinepay.com/docs/mccs and refuse any other value naming this field. |
| business.statement_descriptor | string | What buyers see on their card statement. 5 to 22 characters, at least one Latin letter, no angle brackets. Keep it recognisable, or it drives chargebacks. |
| business.address | object |  |
| business.address.line1 | string |  |
| business.address.line2 | string |  |
| business.address.city | string |  |
| business.address.state | string | Two-letter state or province code. For a US address, the USPS code. The processor cannot serve US insular areas (GU, VI, AS, MP). |
| business.address.postal_code | string | Postal code. For a US address, 5 digits or ZIP+4, e.g. 19104 or 19104-2617. |
| business.address.country | string | Two-letter ISO country code, required whenever an address is sent. This is the country of *this address*: a business address must be in the merchant's own country (the top-level `country` on the merchant), while a representative's home address may be anywhere. |
| metadata | object | Your own key/value pairs, for correlating this object back to your records. We never interpret the contents: they are stored, returned on every read, and carried on every event this object emits. A payment's, refund's or payment link's is also forwarded to the processor. A merchant's is not. At most 50 pairs, keys up to 40 characters, values up to 500. Values are stored as strings. Keys beginning `outlinepay_`, plus `outline_fee` and `partner_fee`, are reserved by OutlinePay and rejected. Never put secrets here. |
| processors | array<object> |  |
| processors[].processor | string |  |
| processors[].account_id | string, nullable | The processor's own account identifier (for Stripe, the connected account id, e.g. "acct_1AbC..."). Null in the brief window before the account is opened. |
| processors[].onboarding_completed | boolean | True once the merchant has submitted its details to the processor. It says the merchant did its part, not that the processor accepted them, which is what verified says. While either is false, requirements says what is still outstanding and whose move it is. It never returns to false once set. |
| processors[].representative_prefilled | boolean | True once the representative supplied at attach reached the processor. False when none was supplied, or when it was rejected, in which case Attach a processor accepts the corrected one. A delivery flag only: the representative itself is forward-only and never returned. |
| processors[].requirements | object, nullable | What the merchant still owes this processor, or null once it owes nothing. |
| processors[].requirements.status | enum | How urgent the strictest outstanding item is. `overdue` is holding the account back now, `due` keeps it working, `upcoming` is wanted once the merchant's volume passes the processor's thresholds. One of: `overdue`, `due`, `upcoming`. |
| processors[].requirements.blocks_charges | boolean | True when the merchant cannot take a payment until something here is resolved. |
| processors[].requirements.blocks_payouts | boolean | True when the merchant cannot be paid out until something here is resolved. |
| processors[].requirements.awaiting | enum | Whose move is next. `merchant` means the account session and onboarding component will collect what is missing. `processor` means it has what it needs and is reviewing, so there is nothing to do but wait. One of: `merchant`, `processor`. |
| processors[].requirements.refreshed_at | string | When we last read this from the processor, or when the account was opened there if we have not read it since. |
| processors[].requirements.outstanding | array<object> | One entry per thing the merchant has to do, the ones blocking charges first. Can be empty while the rest of this object is populated: the processor sometimes restricts an account before it says which fields it wants, and an empty list is not the same answer as a null requirements. |
| processors[].requirements.outstanding[].code | enum | What the merchant has to do, in OutlinePay's vocabulary: accept the processor's agreement, supply the representative's identity, add a payout account, or correct the business category or details. `other` means the processor asked for something we have no word for yet, and `processor_items` is then the only description of it. One of: `processor_terms`, `representative_identity`, `payout_account`, `business_category`, `business_details`, `other`. |
| processors[].requirements.outstanding[].blocks | array<string> | What this item holds up. A group's value covers every item in it, so an item blocking charges and another blocking payouts give a group that lists both. For the exact answer read `blocks_charges` and `blocks_payouts` on the object above. |
| processors[].requirements.outstanding[].processor_items | array<string> | The processor's own names for the fields behind this item, verbatim. Useful for telling a merchant precisely what failed, and outside our compatibility contract like every processor_ field. |
| processors[].verified | boolean | True once the processor has accepted the merchant and will take charges for it. Until it is true, creating a payment or a payment link is refused with 409 `merchant_not_onboarded`, whatever `onboarding_completed` says. It turns true at the same moment the merchant's `status` becomes `active`, and a `merchant.updated` event is emitted then. It never returns to false. Whether the merchant can charge right now is `status`, which is where a later restriction shows up. |
| terms_acceptance | object, nullable | The merchant's acceptance of the OutlinePay merchant terms, or null if they have not accepted yet. Read-only. The merchant agrees on the terms screen the onboarding component shows ahead of the processor's form, we record it, and `merchant.updated` announces the change. One acceptance covers every processor. A merchant without one cannot take payments (409 `merchant_terms_not_accepted`), except merchants created before 2026-08-16, who are exempt until they next pass through onboarding. |
| terms_acceptance.version | string | The terms version accepted. |
| terms_acceptance.accepted_at | string |  |
| created_at | string |  |

### The Payment object

A single charge. Creation returns a hosted `checkout_url`. The outcome arrives by webhook.

| Field | Type | Description |
| --- | --- | --- |
| id | string |  |
| object | string | The literal string `payment`, naming this resource's type. Every resource carries one, and it is the discriminator for an event's `data`. |
| status | enum | Where the payment is. Both lifecycles are drawn under The Payment object. `awaiting_payment`: the customer has not finished. A declined card does not end this state. The customer retries on the checkout page. `processing`: the customer has paid but the money is still moving. Only delayed methods (`bank_debit`, `bank_transfer`) reach it, it can last days, and nothing is decided yet, so do not fulfill on it. `failed`: reachable only from `processing`, so only on delayed methods. `failure_reason` says why. One of: `awaiting_payment`, `processing`, `succeeded`, `failed`, `expired`. |
| mode | enum | The world this object lives in, test (sandbox) or live. Matches your API key's prefix (olp_sk_test_ / olp_sk_live_). One of: `test`, `live`. |
| merchant_id | string |  |
| amount | integer | Minor units of `currency` (for `usd`, cents). |
| currency | enum | What the customer was charged in, unless `localized_pricing` was on and they took the converted price, in which case `presentment` says what they paid. `usd` is the only currency we support today. One of: `usd`. |
| description | string, nullable |  |
| customer | object, nullable | The customer details you sent at creation, echoed in the same shape. Null when you sent none. |
| customer.email | string |  |
| fees | object | Fees charged on the gross amount, in minor units. Refunds do not return fees. `outline` and `partner` are frozen as soon as they are knowable, which for almost every payment is creation time. A later rate change does not touch them. The exception is a checkout whose fee depends on which method the customer picks (a capped bank rail offered beside an uncapped one). There, `outline` and `partner` stay null until the customer answers on the hosted checkout page, and `selected_payment_method` says whether they have. A payment that expires unanswered keeps null. |
| fees.processor | integer, nullable | The processor's own cut, read from the charge's balance transactions (see the top-level `processor` field for which processor). Null until captured, a moment after the outcome. That means it is still null on the `payment.succeeded` or `payment.failed` event; the `payment.updated` event that follows carries it. Set on failed payments too. There it is the processor's flat charge for a returned bank debit, and the only fee borne: a failed payment collects nothing for OutlinePay or for you. |
| fees.outline | integer, nullable | OutlinePay's fee, from the rate card. Same in test and live. Null while a method choice is still outstanding. 0 on a payment that failed or expired, because the processor returns this fee when a debit is returned. |
| fees.partner | integer, nullable | Your fee, at your configured rate. Null while a method choice is still outstanding. 0 on a payment that failed or expired, since nothing was collected. |
| fees.dispute | integer | What the disputes on this payment cost, summed: the same money that is itemised per-dispute on disputes[].fee. 0 when there are none. |
| fees.invoice | integer, nullable | What the payment processor charged the merchant for issuing the invoice, before tax, on a payment created with `invoice.enabled`. `fees.tax.invoice` carries the tax, and the two add up to what the merchant paid. Null on every payment that did not ask for one. Live mode only, and slow. The processor itemises this fee only in a report that lands about five days after the payment, so the figure arrives then, on its own payment.updated event. Null until it does, and always null in test mode. |
| fees.fraud_screening | integer, nullable | What the payment processor charged the merchant for screening this payment for fraud, before tax. `fees.tax.fraud_screening` carries the tax. Like fees.invoice it is reported late: the figure arrives about five days after the payment, on its own payment.updated event. Null until it does, always null in test mode, and null for good on a merchant the processor does not bill for screening, which some are not. `pending` is what tells those apart. While `fraud_screening` is listed there a figure is still coming, and once it is not, null means no charge. |
| fees.tax | object | Tax the payment processor charged on its own fees, one entry per fee that can carry it. The rate follows the merchant's business address, so it differs by merchant. 0 has two causes: the fee is not taxed where the merchant is, or this payment's share of the tax came to under half a cent. Each entry is null exactly while the fee it belongs to is null, since both are read from one line of the same report. Add an entry to its fee for what the merchant actually paid. Only the processor's product fees are taxed. Payment processing is not, so there is no entry for `processor`, and OutlinePay adds no tax to its own fee or to yours. |
| fees.tax.invoice | integer, nullable | Tax on `fees.invoice`. |
| fees.tax.fraud_screening | integer, nullable | Tax on `fees.fraud_screening`. |
| fees.total | integer, nullable | Every fee borne on this payment: the sum of the fields above it, tax included (processor + outline + partner + dispute + invoice + fraud_screening + tax.invoice + tax.fraud_screening). Counts an uncaptured processor fee as 0. Null while a method choice is outstanding. |
| fees.pending | array<string> | The fee lines above that have not arrived yet and are still expected, so the figures beside them will still change. Empty once nothing is outstanding, which is the usual answer. Read this to tell a settled figure from an unsettled one: a null fee that is not listed here is not coming. A pending line counts as 0 until it lands, so `total` reads low and `merchant_net` reads high while anything is listed here. Wait for the payment.updated event that empties this before you record either figure. A line leaves the list when its figure arrives, or when we stop expecting it, which is 14 days after the payment for `invoice` and `fraud_screening`. Neither is ever listed in test mode, where they never arrive at all. Disputes are never listed: one can open months later and add a fee to a payment that had none. |
| merchant_net | integer, nullable | What the merchant ends up with: `amount − amount_refunded − amount_disputed − fees.total`. Reads high until the processor's fee lands, a moment after the outcome, including on the `payment.succeeded` event itself. Record net from the `payment.updated` event that follows. Null while a method choice is outstanding, alongside `fees.total`. Can go negative. A refund returns the principal and keeps every fee, so a fully refunded payment nets the negative of its fees. On a payment that failed or expired there is no principal at all, so this is the negative of the processor's charge for the failure (its flat fee for a returned bank debit), and 0 until that charge has been read. |
| amount_refunded | integer | Sum of non-failed refunds, in minor units. |
| amount_disputed | integer | Sum of `disputes[].amount_withheld`: what the processor is holding back over open or lost disputes, in minor units. Returns to 0 when a dispute is won. |
| amount_refundable | integer | `amount − amount_refunded`: headroom for a further refund. Reaches 0 at a full refund, and is 0 on any payment that did not settle, since only a succeeded payment can be refunded. Also 0 while a dispute is holding the money (disputes[].amount_withheld above zero), which the processor refuses to refund whether the dispute is open or already lost. It becomes refundable again if the dispute is won. |
| refunds | array<object> | Refunds against this payment, oldest first. |
| refunds[].id | string |  |
| refunds[].status | enum | `failed` also covers a refund the processor withdrew rather than refused, so the two are told apart by `failure_reason`, not by the status. One of: `pending`, `succeeded`, `failed`. |
| refunds[].amount | integer | Minor units. |
| refunds[].failure_reason | enum, nullable | Why a refund did not reach the customer, in OutlinePay's vocabulary, translated from the processor's own failure code at the boundary. Null unless the refund failed. `card_unusable`: the card can't be refunded to. It was reported lost or stolen, or it's expired or canceled. Refund the customer another way, retrying will fail again. `insufficient_funds`: the merchant's balance didn't have enough to cover the refund. Retry after it funds. `declined`: the card issuer declined the refund. Retry once, then refund another way if it declines again. `disputed`: the charge is under dispute. Do not retry, the dispute governs this refund now. `canceled`: the refund was withdrawn, not refused. Nothing to retry, create a new refund if one is still owed. `other`: the processor gave a reason we don't have a word for, or gave none. Treat it as a failure with no further guidance. One of: `card_unusable`, `insufficient_funds`, `declined`, `disputed`, `canceled`, `other`. |
| refunds[].metadata | object | Your own key/value pairs, for correlating this object back to your records. We never interpret the contents: they are stored, returned on every read, and carried on every event this object emits. A payment's, refund's or payment link's is also forwarded to the processor. A merchant's is not. At most 50 pairs, keys up to 40 characters, values up to 500. Values are stored as strings. Keys beginning `outlinepay_`, plus `outline_fee` and `partner_fee`, are reserved by OutlinePay and rejected. Never put secrets here. |
| refunds[].created_at | string | ISO 8601 UTC. |
| disputes | array<object> | Chargebacks against this payment, oldest first. Usually empty. |
| disputes[].id | string |  |
| disputes[].status | enum | The chargeback's state, in OutlinePay's vocabulary and translated from whichever processor reported it. The `inquiry_*` set is the card networks' early retrieval request, raised before a formal chargeback. The rest is the real lifecycle, terminating in `won`/`lost`, or `refunded` when the merchant refunds instead of contesting. One of: `inquiry_needs_response`, `inquiry_under_review`, `inquiry_closed`, `needs_response`, `under_review`, `won`, `lost`, `refunded`. |
| disputes[].reason | enum, nullable | Why the cardholder disputed, in OutlinePay's vocabulary, translated from the processor's own reason codes. `bank_error` covers the several ways a bank can decline to move the money, and anything we cannot place maps to `other`. One of: `fraudulent`, `product_not_received`, `product_unacceptable`, `duplicate`, `credit_not_processed`, `subscription_canceled`, `unrecognized`, `customer_initiated`, `bank_error`, `other`. |
| disputes[].amount | integer | Minor units. |
| disputes[].fee | integer, nullable | What the processor charged the merchant to handle this dispute, in minor units (Stripe bills a flat 15.00 USD). Charged whether the dispute is won or lost. The payment's `fees.total` and `merchant_net` account for it. |
| disputes[].amount_withheld | integer, nullable | How much of the payment the processor is currently holding back over this dispute, in minor units. Equal to amount from the moment a dispute opens, and 0 once the merchant wins. While it is above zero the payment cannot be refunded and `amount_refundable` reads 0. |
| disputes[].evidence_due_at | string, nullable | Deadline to submit evidence, ISO 8601 UTC. |
| disputes[].created_at | string | ISO 8601 UTC. |
| processor | string | The underlying processor, e.g. `stripe`. |
| processor_payment_method | string, nullable | What actually settled, in the processor's own vocabulary (for example `card` or `us_bank_account`). Like every `processor_` field it is outside our portability contract: read it for display or for reconciling against your processor, not for product logic. Null until the charge exists, which for a delayed method such as ACH is days after the payment was created. It is read off the charge at the same moment as `fees.processor` and arrives on the same `payment.updated` event. `link` covers anything paid through the processor's wallet, including a bank account paid through it, so it does not say whether a card was involved. |
| processor_reference | string, nullable | The processor's own identifier for this transaction, to quote back when reconciling against it. It is the reference on the receipt and the invoice the processor issues the merchant, and on the processor's billing for the fees it charges the merchant directly, none of which carry an OutlinePay id. The `processor_` prefix carries the same warning as the field above, and its shape differs by processor. Null until a charge exists, arriving on the same `payment.updated` that fills `fees.processor` and `processor_payment_method`. A failed payment can carry one, because a decline and a returned bank debit both produced a charge, so a value here is not proof that money moved. The reference has been recorded since 2026-09-03, so a payment charged before that date reads null. |
| selected_payment_method | enum, nullable | A payment-method category a partner may offer, in OutlinePay's vocabulary, translated to the processor's own methods at the boundary. `card` is debit and credit cards. The card-based wallets (Apple Pay, Google Pay, Link) ride along automatically and cannot be offered without cards. `wallet` is the standalone wallets (Cash App, Amazon Pay). `bnpl` is buy-now-pay-later (Klarna, Affirm, Afterpay). `bank_debit` is ACH Direct Debit, paid from a US bank account. `bank_transfer` is a push transfer: the customer is shown a virtual account number and sends the funds from their own bank. The two bank rails behave differently from the rest. They settle after the customer finishes, so the payment sits in `processing` first, for days if the transfer is slow, and `bank_debit` can reach `failed`. Both OutlinePay's fee and yours are capped on them, so offering one beside an uncapped method sends the customer to a page asking which they intend to use before the price is fixed. `bank_transfer` additionally requires `customer.email` at creation (the virtual account number and any refund follow a named buyer, and the same buyer keeps the same account number across payments), cannot be offered on a payment link, and is available to US merchants only for now. See "Payment methods" under Conventions. Provide an array to offer exactly those categories. Omit the field and you get the default of `["card", "wallet"]`. Which of the offered categories this payment will use. Set when the customer answered a choice OutlinePay put to them, which happens on the checkout where their answer is what fixes the fee (a capped bank rail offered alongside an uncapped method). Also set whenever the checkout offered exactly one category. Null only when several categories that price alike were offered. The customer then picks one on the processor's page after the price is settled, and that choice is not recorded here. Read `processor_payment_method` for what the processor actually used. One of: `card`, `wallet`, `bnpl`, `bank_debit`, `bank_transfer`. |
| card | object, nullable | The card that settled this payment, as the processor read it off the card networks. It arrives with the other settlement facts, a moment after the outcome: null on the `payment.succeeded` event itself, filled on the `payment.updated` event that follows, like `fees.processor`. Null forever on a payment no card settled, such as a bank rail or a payment that expired unpaid. A wallet payment is a card underneath, so it appears here too, with `wallet` naming which wallet presented it. A payment whose `processor_payment_method` reads `link` stays null, because the processor does not reveal the card behind its own wallet. |
| card.brand | string, nullable | The card's scheme, as the networks name it: `visa`, `mastercard`, `amex`, `discover`, `jcb`, `unionpay` and more. A string, not an enum, so a new scheme appears as soon as the processor reports one. |
| card.network | string, nullable | The network the charge was actually routed over. Matches `brand` on almost every card. The two differ on a co-badged card, which carries two schemes and lets the processor pick the route. |
| card.funding | string, nullable | How the card draws money: `credit`, `debit`, `prepaid`, or `unknown`. |
| card.last4 | string, nullable | The last four digits of the card number. The full number never reaches OutlinePay or you. |
| card.exp_month | integer, nullable | Expiration month, 1 through 12. |
| card.exp_year | integer, nullable | Expiration year, four digits. |
| card.country | string, nullable | Two-letter ISO code of the country the card was issued in. |
| card.authorization_code | string, nullable | The approval number the issuer returned when it authorized the charge, the same code printed on a card receipt. Quote it when reconciling against card-company records. Null when the network did not return one. |
| card.wallet | string, nullable | Set when a digital wallet presented the card: `apple_pay`, `google_pay`, and so on. Null on a plain card payment. |
| payment_link_id | string, nullable | Set when the payment came from a link. |
| checkout_url | string, nullable | Hosted checkout URL. Present while the payment is not yet terminal, which includes `processing`: a delayed payment's page is where the customer sees what is still outstanding, and they may return to it days later. Null once the payment succeeds, fails or expires. |
| receipt_url | string, nullable | The customer-facing receipt: an OutlinePay-owned URL that resolves to the processor's hosted receipt, viewable in a browser and downloadable as a PDF. The document always shows the payment's latest state, refunds included. Appears when the payment succeeds, null before, and never expires. |
| invoice | object | Whether this payment asked the processor for an invoice. Always present, `{"enabled": false}` when you did not ask, so off and never-asked read the same. Fixed at creation. |
| invoice.enabled | boolean |  |
| localized_pricing | object | Whether the processor was allowed to present the price in the customer's own currency. Always present, `{"enabled": false}` when you did not ask, so off and never-asked read the same. Fixed at creation. |
| localized_pricing.enabled | boolean |  |
| presentment | object, nullable | What the customer was actually charged when the processor presented the price in their own currency: `amount` in that currency's minor unit, and `currency`. Only on a payment created with `localized_pricing.enabled`, and only when the customer took the converted price rather than paying in `currency`. Null otherwise, and null until the charge is read, which is the same `payment.updated` that fills `fees.processor` and `card`. The merchant still receives `amount` in `currency`. The difference between the two is the processor's conversion, paid by the customer. What the customer's own bank does after that is not visible to anyone. |
| presentment.amount | integer |  |
| presentment.currency | string |  |
| invoice_url | string, nullable | The invoice, on a payment created with `invoice.enabled`: an OutlinePay-owned URL that resolves to the processor's hosted invoice, viewable in a browser and downloadable as a PDF. It is the merchant's own document, issued under their business details, and it shows the payment's latest state including refunds. Appears when the payment succeeds, null before, and null forever on a payment that did not ask for one. `receipt_url` is proof that money moved, while this is the itemised document a buyer's accounting asks for. What it cost the merchant is `fees.invoice`. |
| expires_at | string, nullable | ISO 8601 UTC. When the *checkout session* lapses, not the payment. The window is set by the processor when the session is minted. For Stripe-hosted checkout that is 24 hours after creation. A payment still `awaiting_payment` at that moment expires. A payment in `processing` has already been paid for, so this is often in the past on a healthy payment. |
| processing_at | string, nullable | ISO 8601 UTC. When the customer finished and the payment entered `processing`. Only delayed methods reach this state. |
| succeeded_at | string, nullable |  |
| funds_available_at | string, nullable | ISO 8601 UTC, e.g. 2026-09-17T00:00:00Z. The date this payment's net enters the merchant's balance at the processor. No payout happens on this date, and no money arrives in the merchant's bank account on it. What follows is set by the merchant's own payout schedule: on a manual schedule the merchant can request a payout of that amount from this date and not before, and on an automatic schedule there is no request at all, the amount is swept into whichever scheduled payout comes next on or after it. Null on `payment.succeeded` and filled by the `payment.updated` that follows, the same timing as `fees.processor`. Null forever on a payment that collected nothing, and on any payment that settled before this field existed. |
| failed_at | string, nullable |  |
| expired_at | string, nullable |  |
| failure_reason | enum, nullable | Why a payment failed, in OutlinePay's vocabulary, translated from the processor's own failure code at the boundary. `insufficient_funds`: the account didn't have the money. Retry later, the account itself is fine. `account_closed`: the account no longer exists. Ask for different details. `account_details_invalid`: the account or routing number was wrong, or a linked account's number was deactivated. Ask for correct details. `not_authorized`: there's no valid authorization to debit this account, usually because the customer disputed an earlier debit. Collect a fresh authorization before trying again. `account_blocked`: the customer's bank is refusing debits, frozen or restricted. The customer has to sort it out with their bank. `other`: the processor gave a reason we don't have a word for, or gave none. Treat it as a failure with no further guidance. Why the payment failed, and null unless it did. Only bank rails reach it: a declined card doesn't end the checkout, the customer just tries again on the processor's page, so a card payment goes from `awaiting_payment` to `succeeded` or expires. Grouped by what to do next rather than by the processor's own code. Several of Stripe's ACH codes call for the same action and fold into one value here. One of: `insufficient_funds`, `account_closed`, `account_details_invalid`, `not_authorized`, `account_blocked`, `other`. |
| payment_methods | array<string>, nullable | The method categories you restricted this payment to, echoed back. Null means you did not ask, which offers the documented default of `["card", "wallet"]`, not every method the processor supports. |
| bank_verification | enum, nullable | How a customer's bank account must be proven to be theirs before it can be debited. Applies to `bank_debit` and is ignored on a checkout that offers no bank method. `instant_or_delayed` (default): the customer links their bank instantly, or, if they'd rather not or their bank can't be reached, enters the account number and confirms two small deposits instead. That second path takes 1-2 business days for the deposits to arrive and gives them 10 days to enter them, so the payment can sit in `processing` for a fortnight. Accepts the most customers. `instant`: instant linking only. You learn within seconds whether the account is good. A customer whose bank cannot be linked this way cannot pay by bank at all. Echoed as you sent it. Null means you did not ask, so `instant_or_delayed` applied. One of: `instant_or_delayed`, `instant`. |
| metadata | object | Your own key/value pairs, for correlating this object back to your records. We never interpret the contents: they are stored, returned on every read, and carried on every event this object emits. A payment's, refund's or payment link's is also forwarded to the processor. A merchant's is not. At most 50 pairs, keys up to 40 characters, values up to 500. Values are stored as strings. Keys beginning `outlinepay_`, plus `outline_fee` and `partner_fee`, are reserved by OutlinePay and rejected. Never put secrets here. |
| created_at | string | ISO 8601 UTC. |

### The PaymentLink object

Reusable, shareable URLs that mint a fresh checkout session on every visit.

| Field | Type | Description |
| --- | --- | --- |
| id | string |  |
| object | string | The literal string `payment_link`. |
| url | string | The permanent, OutlinePay-owned link URL. |
| status | enum | One of: `active`, `deactivated`. |
| mode | enum | The world this object lives in, test (sandbox) or live. Matches your API key's prefix (olp_sk_test_ / olp_sk_live_). One of: `test`, `live`. |
| merchant_id | string |  |
| amount | integer |  |
| currency | enum | What the customer is charged in, fixed for the life of the link, unless `localized_pricing` is on. `usd` is the only currency we support today. One of: `usd`. |
| description | string, nullable |  |
| processor | string | The rail this link charges on, e.g. `stripe`. Fixed at creation. Every session and payment it produces uses it. |
| payment_methods | array<string>, nullable | The method categories every session and payment this link mints is restricted to, echoed back. Fixed at creation, like the rail. Null means you did not ask, which offers the documented default of `["card", "wallet"]`. |
| bank_verification | enum, nullable | How a customer's bank account must be proven to be theirs before it can be debited. Applies to `bank_debit` and is ignored on a checkout that offers no bank method. `instant_or_delayed` (default): the customer links their bank instantly, or, if they'd rather not or their bank can't be reached, enters the account number and confirms two small deposits instead. That second path takes 1-2 business days for the deposits to arrive and gives them 10 days to enter them, so the payment can sit in `processing` for a fortnight. Accepts the most customers. `instant`: instant linking only. You learn within seconds whether the account is good. A customer whose bank cannot be linked this way cannot pay by bank at all. Echoed as you sent it, and applied to every session this link mints. Null means you did not ask, so `instant_or_delayed` applied. One of: `instant_or_delayed`, `instant`. |
| invoice | object | Whether every payment this link mints asks the processor for an invoice. Always present, `{"enabled": false}` when you did not ask, so off and never-asked read the same. |
| invoice.enabled | boolean |  |
| localized_pricing | object | Whether every payment this link mints may have its price presented in the customer's own currency. Always present, `{"enabled": false}` when you did not ask, so off and never-asked read the same. |
| localized_pricing.enabled | boolean |  |
| metadata | object | Your own key/value pairs, for correlating this object back to your records. We never interpret the contents: they are stored, returned on every read, and carried on every event this object emits. A payment's, refund's or payment link's is also forwarded to the processor. A merchant's is not. At most 50 pairs, keys up to 40 characters, values up to 500. Values are stored as strings. Keys beginning `outlinepay_`, plus `outline_fee` and `partner_fee`, are reserved by OutlinePay and rejected. Never put secrets here. |
| created_at | string |  |

### The Refund object

Return money for a succeeded payment, in full or in part.

| Field | Type | Description |
| --- | --- | --- |
| id | string |  |
| object | string | The literal string `refund`. |
| status | enum | `failed` also covers a refund the processor withdrew rather than refused, so the two are told apart by `failure_reason`, not by the status. One of: `pending`, `succeeded`, `failed`. |
| mode | enum | The world this object lives in, test (sandbox) or live. Matches your API key's prefix (olp_sk_test_ / olp_sk_live_). One of: `test`, `live`. |
| payment_id | string |  |
| merchant_id | string |  |
| processor | string | The rail this refund settled on, e.g. `stripe`. Inherited from the payment. |
| amount | integer |  |
| failure_reason | enum, nullable | Why a refund did not reach the customer, in OutlinePay's vocabulary, translated from the processor's own failure code at the boundary. Null unless the refund failed. `card_unusable`: the card can't be refunded to. It was reported lost or stolen, or it's expired or canceled. Refund the customer another way, retrying will fail again. `insufficient_funds`: the merchant's balance didn't have enough to cover the refund. Retry after it funds. `declined`: the card issuer declined the refund. Retry once, then refund another way if it declines again. `disputed`: the charge is under dispute. Do not retry, the dispute governs this refund now. `canceled`: the refund was withdrawn, not refused. Nothing to retry, create a new refund if one is still owed. `other`: the processor gave a reason we don't have a word for, or gave none. Treat it as a failure with no further guidance. One of: `card_unusable`, `insufficient_funds`, `declined`, `disputed`, `canceled`, `other`. |
| metadata | object | Your own key/value pairs, for correlating this object back to your records. We never interpret the contents: they are stored, returned on every read, and carried on every event this object emits. A payment's, refund's or payment link's is also forwarded to the processor. A merchant's is not. At most 50 pairs, keys up to 40 characters, values up to 500. Values are stored as strings. Keys beginning `outlinepay_`, plus `outline_fee` and `partner_fee`, are reserved by OutlinePay and rejected. Never put secrets here. |
| created_at | string |  |

### The Dispute object

Chargebacks a cardholder opened with their bank. Read-only. Evidence is submitted by the merchant in the processor's embedded component.

| Field | Type | Description |
| --- | --- | --- |
| id | string |  |
| object | string | The literal string `dispute`. |
| status | enum | The chargeback's state, in OutlinePay's vocabulary and translated from whichever processor reported it. The `inquiry_*` set is the card networks' early retrieval request, raised before a formal chargeback. The rest is the real lifecycle, terminating in `won`/`lost`, or `refunded` when the merchant refunds instead of contesting. One of: `inquiry_needs_response`, `inquiry_under_review`, `inquiry_closed`, `needs_response`, `under_review`, `won`, `lost`, `refunded`. |
| mode | enum | The world this object lives in, test (sandbox) or live. Matches your API key's prefix (olp_sk_test_ / olp_sk_live_). One of: `test`, `live`. |
| payment_id | string |  |
| merchant_id | string |  |
| processor | string | The rail the disputed charge is on, e.g. `stripe`. Inherited from the payment. |
| reason | enum, nullable | Why the cardholder disputed, in OutlinePay's vocabulary, translated from the processor's own reason codes. `bank_error` covers the several ways a bank can decline to move the money, and anything we cannot place maps to `other`. One of: `fraudulent`, `product_not_received`, `product_unacceptable`, `duplicate`, `credit_not_processed`, `subscription_canceled`, `unrecognized`, `customer_initiated`, `bank_error`, `other`. |
| amount | integer | Disputed amount, minor units. |
| fee | integer, nullable | What the processor charged the merchant to handle this dispute, in minor units (Stripe bills a flat 15.00 USD). Charged whether the dispute is won or lost, so winning still leaves the merchant down this much. The payment's `fees.total` and `merchant_net` account for it. |
| amount_withheld | integer, nullable | How much of the payment the processor is currently holding back over this dispute, in minor units. Equal to amount from the moment a dispute opens, and 0 once the merchant wins. While it is above zero the payment cannot be refunded. |
| evidence_due_at | string, nullable | Deadline to submit evidence, ISO 8601 UTC. |
| created_at | string |  |

### The Event object

The replayable log of everything OutlinePay has emitted for you.

| Field | Type | Description |
| --- | --- | --- |
| id | string |  |
| object | string | The literal string `event`. |
| type | enum | The outbound event catalog. `payment.updated` is the only type that is not a change of state. It fires a moment after a payment's outcome, carrying the facts that can only be read off the charge: the processor's own fee, the method that settled, and the card behind a card payment. All are still null on the outcome event itself, so record net from `payment.updated`, not from `payment.succeeded`. The full rule is under "Fees arrive just after the outcome" in Conventions. One of: `merchant.created`, `merchant.updated`, `payment.processing`, `payment.succeeded`, `payment.failed`, `payment.expired`, `payment.updated`, `payment_link.created`, `payment_link.deactivated`, `refund.completed`, `refund.failed`, `dispute.created`, `dispute.updated`, `dispute.closed`. |
| mode | enum | The world this object lives in, test (sandbox) or live. Matches your API key's prefix (olp_sk_test_ / olp_sk_live_). One of: `test`, `live`. |
| created_at | string |  |
| data | object | The resource payload captured when the event was emitted: a Payment for payment.* events, a Merchant for merchant.*, and so on. Its `object` field names which, so a generic handler can branch on the payload rather than parsing the event type. |

### The WebhookEndpoint object

Where OutlinePay delivers signed events. The signing secret is shown once, at creation.

| Field | Type | Description |
| --- | --- | --- |
| id | string |  |
| object | string | The literal string `webhook_endpoint`. |
| url | string |  |
| status | enum | Always `active` today: a deleted endpoint leaves the API entirely rather than appearing as disabled. One of: `active`, `disabled`. |
| mode | enum | The world this object lives in, test (sandbox) or live. Matches your API key's prefix (olp_sk_test_ / olp_sk_live_). One of: `test`, `live`. |
| secret | string, nullable | Signing secret. Returned only on creation. |
| created_at | string |  |

## Error codes

The value of `error.code`. See Errors under Conventions for the envelope.

- `invalid_request`
- `authentication_error`
- `not_found`
- `payment_method_not_enabled`
- `merchant_not_onboarded`
- `merchant_terms_not_accepted`
- `partner_sites_incomplete`
- `payments_paused`
- `refunds_paused`
- `idempotency_conflict`
- `rate_limited`
- `processor_error`

## Event types

The outbound event catalog.

`payment.updated` is the only type that is not a change of state. It fires a moment after a payment's outcome, carrying the facts that can only be read off the charge: the processor's own fee, the method that settled, and the card behind a card payment. All are still null on the outcome event itself, so record net from `payment.updated`, not from `payment.succeeded`. The full rule is under "Fees arrive just after the outcome" in Conventions.

- `merchant.created`
- `merchant.updated`
- `payment.processing`
- `payment.succeeded`
- `payment.failed`
- `payment.expired`
- `payment.updated`
- `payment_link.created`
- `payment_link.deactivated`
- `refund.completed`
- `refund.failed`
- `dispute.created`
- `dispute.updated`
- `dispute.closed`

## Webhook signatures

Every webhook delivery is a POST with a JSON body and this header:

```
OutlinePay-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256>
```

`v1` is the HMAC-SHA256, hex encoded, of the string `"<t>.<raw body>"`, keyed by the endpoint's signing secret (`olp_whsec_...`, shown once when you create the endpoint). To verify a delivery:

1. Read the raw request body. Do not parse and re-serialize it first.
2. Split the header on `,` and each part on the first `=`. Take `t` and `v1`.
3. Reject the delivery if `t` is missing or more than 5 minutes from your clock. `t` is when this attempt was sent, and every retry is signed fresh.
4. Compute HMAC-SHA256 of `t + "." + body` with your signing secret and compare it to `v1` in constant time.
5. Answer with any 2xx within 10 seconds. Anything else is retried, up to 8 times over roughly 90 minutes. The same event can arrive twice, so skip event ids you have already handled.

A PHP sample is in the quickstart: https://outlinepay.com/docs/quickstart#qs-verify. Delivery rules: https://outlinepay.com/docs#conventions-webhook-delivery.
