API Reference
Every request, response, and error shape - grouped by resource. Module pages under Modules explain when to use each surface and link here for schemas - this page is the exhaustive reference. Every request uses the same Bearer authentication described below.
Authentication
Outpost uses OAuth2 client_credentials for server-to-server calls. Your backend exchanges a client_id and api_token for an access_token, then sends it as a Bearer token on every API request.
Create and manage your API credentials in the Outpost merchant portal. Your credentials are scoped to your own merchant account.
How-to and operational notes live on the Authentication page - the shapes below are the reference copy.
Flow
- Request an
access_tokenwith yourclient_idandapi_token - Read
access_tokenandexpires_infrom the response - Cache the token server-side with TTL
expires_in - 300s - Refresh on expiry or on a 401, then retry the request once
- Send
Authorization: Bearer <access_token>on every API call
Get access token
POST/oauth2/tokenHost: https://access.outpostanywhere.com
Do not send token requests to the API host (https://api.outpostanywhere.com).
Send your api_token as the OAuth client_secret form field - that is the wire name for this grant.
Request
curl -X POST \
"https://access.outpostanywhere.com/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=$OUTPOST_API_TOKEN" | jqResponse
{
"access_token": "<JWT>",
"expires_in": 86400,
"token_type": "Bearer"
}Run from your server. Never expose your api_token in the browser.
Resource
Tax
Calculate tax, commit transactions, retrieve and refund them, and keep your product catalogue in sync.
Create Tax Calculation
POST/api/tax/calculationsCreates a tax calculation for a given set of line items and customer information. The calculation determines applicable tax rates based on the customer's location and can be used to create a transaction after the successful payment.
Request Headers
| Parameter | Type | Required | Description |
|---|---|---|---|
| Authorization | - | Yes | Bearer token for authentication |
| Content-Type | - | Yes | application/json |
Request Body
{
"currency": "EUR",
"taxBehavior": "EXCLUSIVE",
"customer": {
"merchantCustomerReference": "CUST-12345",
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"taxIdentifiers": [
{
"type": "EU_VAT",
"code": "NL123456789B01"
}
],
"billingAddress": {
"line1": "Keizersgracht 123",
"line2": "Apt 4B",
"city": "Amsterdam",
"postalCode": "1015 CJ",
"country": "NL"
}
},
"lineItems": [
{
"merchantLineItemReference": "ITEM-001",
"productCode": "TEST-SKU-005",
"description": "Premium Subscription - Annual",
"unitPrice": 99.99,
"quantity": 1,
"discountAmount": 10.00
}
],
"evidence": {
"billingCountry": "NL",
"ipAddress": "185.23.108.42",
"paymentMethodCountry": "NL"
}
}Request Body Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| currency | string | Yes | ISO 4217 currency code (e.g., EUR, USD, GBP, BRL). Brazil B2C NFS-e requires BRL. |
| taxBehavior | string enum | No | Allowed values: EXCLUSIVE, INCLUSIVE. Defaults to EXCLUSIVE. With INCLUSIVE, unitPrice is the gross amount and tax is extracted from it. |
| customer | object | Yes | Customer information object |
| customer.merchantCustomerReference | string | No | Your internal customer identifier |
| customer.firstName | string | Conditional | Customer's first name. Required for Brazil B2C NFS-e. |
| customer.lastName | string | Conditional | Customer's last name. Required for Brazil B2C NFS-e. |
| customer.email | string | No | Customer's email address |
| customer.taxIdentifiers | array | Conditional | Customer tax identifiers, discriminated on type. Required for Brazil NFS-e calculations. |
| customer.taxIdentifiers[].type | string enum | Conditional | Identifier type: BR_CPF, BR_CNPJ, EU_VAT, GB_VAT, or US_EIN. The jurisdiction is part of the type - there is no country field. Unknown types are rejected. Brazil requires exactly one BR_CPF or BR_CNPJ; other types may accompany it. |
| customer.taxIdentifiers[].code | string | Conditional | Identifier value. Format/check-digit validated for BR_CPF and BR_CNPJ (not against Receita Federal registration). VIES format with a two-letter country prefix for EU_VAT. GB_VAT and US_EIN are informational and recorded as provided. Formatted or bare codes accepted. |
| customer.taxIdentifiers[].buyerTaxRegime | string enum | Conditional | BR_CNPJ only. Allowed values: SIMPLES_NACIONAL, LUCRO_PRESUMIDO, LUCRO_REAL. Required for Brazil B2B NFS-e calculations. |
| customer.billingAddress | object | Yes | Customer's billing address |
| customer.billingAddress.line1 | string | Yes | Primary address line |
| customer.billingAddress.line2 | string | No | Secondary address line |
| customer.billingAddress.city | string | Yes | City name |
| customer.billingAddress.state | string | Conditional | State/province code (required for US and CA) |
| customer.billingAddress.postalCode | string | Yes | Postal/ZIP code |
| customer.billingAddress.country | string | Yes | ISO 3166-1 alpha-2 country code |
| customer.shippingAddress | object | Conditional | Customer's shipping address. Required when any line item is PHYSICAL. The destination drives US sales-tax sourcing. |
| customer.shippingAddress.line1 | string | Conditional | Primary address line. Required when the shipping address is present and the country is outside the EU. |
| customer.shippingAddress.line2 | string | No | Secondary address line |
| customer.shippingAddress.city | string | Conditional | City name. Required when the shipping address is present and the country is outside the EU. |
| customer.shippingAddress.state | string | Conditional | State/province code (required for US and CA) |
| customer.shippingAddress.postalCode | string | Conditional | Postal/ZIP code. Required when the shipping address is present and the country is outside the EU. |
| customer.shippingAddress.country | string | Conditional | ISO 3166-1 alpha-2 country code. Required whenever the shipping address is present. |
| lineItems | array | Yes | Array of line items (minimum 1) |
| lineItems[].merchantLineItemReference | string | Yes | Your internal line item identifier |
| lineItems[].productCode | string | Yes | Product/service code for tax classification |
| lineItems[].description | string | Yes | Human-readable item description |
| lineItems[].unitPrice | decimal | Yes | Price per unit (must be ≥ 0) |
| lineItems[].quantity | integer | No | Quantity (default: 1, must be > 0) |
| lineItems[].discountAmount | decimal | No | Discount amount per unit. For quantity 3 and discountAmount 2.00, the total discount is 6.00; applied before tax calculation. |
| lineItems[].productType | string enum | No | Allowed values: PHYSICAL, DIGITAL. Defaults to DIGITAL. Physical goods are priced with import duty on the cross-border path. |
| lineItems[].countryOfOrigin | string | No | ISO 3166-1 alpha-2 country of manufacture. Requires productType PHYSICAL. Needed per line on the cross-border duty path. |
| lineItems[].hsCode | string | No | Destination-qualified HS code used to classify the line for duty. Requires productType PHYSICAL and must not be blank when present. |
| shipFrom | object | No | Ship-from origin address for cross-border duty. Requires at least one PHYSICAL line item. When the origin country differs from the destination, the cart routes to the duty provider. |
| shipFrom.line1 | string | No | Primary address line |
| shipFrom.line2 | string | No | Secondary address line |
| shipFrom.city | string | No | City name |
| shipFrom.state | string | No | State/province code |
| shipFrom.postalCode | string | No | Postal/ZIP code |
| shipFrom.country | string | Conditional | ISO 3166-1 alpha-2 country code. Required whenever shipFrom is present. |
| evidence | object | Conditional | Tax evidence (required for non-US/non-BR billing addresses; optional for Brazil) |
| evidence.billingCountry | string | Conditional | ISO 3166-1 alpha-2 billing country code. Required for non-US/non-BR evidence; if provided for Brazil, must be BR. |
| evidence.ipAddress | string | Conditional | Customer's IP address. Required for non-US/non-BR billing addresses; optional for Brazil. |
| evidence.paymentMethodCountry | string | No | Payment method country code |
evidence is required and evidence.billingCountry must match customer.billingAddress.country. For Brazil, evidence can be omitted; if provided, evidence.billingCountry must be BR and evidence.ipAddress is optional.currency BRL, include customer first and last name, include exactly one valid BR_CPF identifier in customer.taxIdentifiers and set taxBehavior to INCLUSIVE when the submitted line amount should remain the service value and ISS tax base.NFS-e vs NFCom: regular (non-telecom) services use NFS-e / ISS. Telecom or communication services (for example eSIM) need NFCom / ICMS instead - do not run them through the NFS-e path.
CPF/CNPJ checks are format and check-digit only. A format-valid ID can still fail when the e-invoice is submitted to the authority.
Response 201 Created
{
"taxCalculationId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"createdAt": "2026-01-05T12:00:00Z",
"expiresAt": "2026-01-12T12:00:00Z",
"currency": "EUR",
"subTotalAmount": "99.99",
"discountAmount": "10.00",
"netAmount": "89.99",
"taxAmount": "18.90",
"totalAmount": "108.89",
"customer": {
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"billingAddress": {
"line1": "Keizersgracht 123",
"line2": "Apt 4B",
"city": "Amsterdam",
"postalCode": "1015 CJ",
"country": "NL"
}
},
"lineItems": [
{
"lineItemId": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"merchantLineItemReference": "ITEM-001",
"productCode": "TEST-SKU-005",
"description": "Premium Subscription - Annual",
"unitPrice": "99.99",
"quantity": 1,
"discountAmount": "10.00",
"itemSubTotalAmount": "99.99",
"itemNetAmount": "89.99",
"itemTotalAmount": "108.89",
"tax": {
"rate": "21.00",
"taxAmount": "18.90",
"currency": "EUR"
},
"refunded": false
}
],
"evidence": {
"billingCountry": "NL",
"ipAddress": "185.23.108.42",
"paymentMethodCountry": "NL"
},
"entity": {
"companyName": "Outpost Commerce B.V."
}
}Brazil B2C NFS-e Example
Brazil B2C NFS-e calculations require BRL, customer first and last name, and a BR_CPF tax identifier. In inclusive mode, the submitted line amount remains the service value and ISS tax base; tax is not backed out of the line amount. The response keeps unit price, subtotal, net amount, and total amount at the submitted amount and reports ISS separately.
{
"currency": "BRL",
"taxBehavior": "INCLUSIVE",
"customer": {
"merchantCustomerReference": "BR-CUST-001",
"firstName": "Beatriz",
"lastName": "Costa",
"email": "beatriz.costa@example.com",
"taxIdentifiers": [
{
"type": "BR_CPF",
"code": "<VALID_CUSTOMER_CPF>"
}
],
"billingAddress": {
"line1": "Rua Oscar Freire, 900",
"city": "Sao Paulo",
"state": "SP",
"postalCode": "01426-002",
"country": "BR"
}
},
"lineItems": [
{
"merchantLineItemReference": "BR-VAS-ITEM-001",
"productCode": "BR-VAS-001",
"description": "Value-added mobile service",
"unitPrice": 49.90,
"quantity": 1
}
],
"evidence": {
"billingCountry": "BR",
"ipAddress": "200.160.2.3",
"paymentMethodCountry": "BR"
}
}{
"taxCalculationId": "019ef95a-cc77-78da-9041-58908baabaf9",
"createdAt": "2026-01-05T12:00:00Z",
"expiresAt": "2026-01-12T12:00:00Z",
"currency": "BRL",
"subTotalAmount": "49.90",
"discountAmount": "0.00",
"netAmount": "49.90",
"taxAmount": "1.45",
"totalAmount": "49.90",
"customer": {
"firstName": "Beatriz",
"lastName": "Costa",
"email": "beatriz.costa@example.com",
"billingAddress": {
"line1": "Rua Oscar Freire, 900",
"city": "Sao Paulo",
"state": "SP",
"postalCode": "01426-002",
"country": "BR"
}
},
"lineItems": [
{
"lineItemId": "83e026ae-b270-4590-926a-03b286fc84d8",
"merchantLineItemReference": "BR-VAS-ITEM-001",
"productCode": "BR-VAS-001",
"description": "Value-added mobile service",
"unitPrice": "49.90",
"quantity": 1,
"discountAmount": "0.00",
"itemSubTotalAmount": "49.90",
"itemNetAmount": "49.90",
"itemTotalAmount": "49.90",
"tax": {
"rate": "2.90",
"taxAmount": "1.45",
"currency": "BRL"
}
}
],
"evidence": {
"billingCountry": "BR",
"ipAddress": "200.160.2.3",
"paymentMethodCountry": "BR"
},
"entity": {
"companyName": "Outpost Brazil Limitada"
}
}Brazil CPF Validation Error
Brazil requires exactly one BR_CPF or BR_CNPJ identifier. Use BR_CPF for B2C and BR_CNPJ for B2B. If no Brazilian identifier is present, if more than one is present, or if the value fails check-digit validation, the calculation request is rejected before tax is calculated.
{
"code": "invalid_argument",
"errors": [
{
"field": "customer.taxIdentifiers[0].code",
"message": "must be a valid CPF"
}
]
}Response Fields
| Parameter | Type | Required | Description |
|---|---|---|---|
| taxCalculationId | string | - | Unique identifier for the calculation |
| createdAt | string | - | ISO 8601 timestamp when calculation was created |
| expiresAt | string | - | ISO 8601 timestamp when calculation expires |
| currency | string | - | Currency code |
| subTotalAmount | decimal | - | Sum of line item amounts before discounts |
| discountAmount | decimal | - | Total discount amount applied |
| netAmount | decimal | - | Amount after discounts, before tax |
| taxAmount | decimal | - | Total tax amount |
| totalAmount | decimal | - | Final amount including tax |
| customer | object | - | Customer information |
| totalDutyAmount | decimal | - | Total import duty across all line items. Omitted when no line carries duty. |
| lineItems | array | - | Calculated line items with tax details |
| lineItems[].tax.rate | decimal | - | Effective tax rate applied to the line |
| lineItems[].tax.taxAmount | decimal | - | Tax amount for the line |
| lineItems[].tax.dutyAmount | decimal | - | Import duty for the line. Omitted when the line carries no duty. |
| lineItems[].tax.dutyRate | decimal | - | Duty rate applied to the line |
| lineItems[].tax.dutyBasis | string enum | - | How the duty rate was derived: CLASSIFIED (from the HS code) or FALLBACK_ESTIMATE. |
| lineItems[].tax.appliedHsCode | string | - | HS code the line was actually classified with |
| lineItems[].tax.charges | array | - | Jurisdiction-level breakdown. Always present. See the charges note below before using it. |
| evidence | object | - | Tax evidence used for calculation |
| entity | object | - | The Outpost legal entity acting as the recording seller (present when Tax of Record / liability transfer applies). |
| entity.companyName | string | - | Legal company name of the recording entity. |
A calculation is valid for 7 days from createdAt. After expiresAt it can no longer be converted into a transaction - create a new one.
Charge Breakdown
Every line item carries a tax.charges array describing the individual jurisdiction-level charges behind the line totals.
| Parameter | Type | Required | Description |
|---|---|---|---|
| kind | string enum | - | TAX, IMPORT_DUTY, or IMPORT_FEE |
| name | string | - | Charge name |
| jurisdiction | string | - | Jurisdiction the charge belongs to |
| jurisdictionLevel | string enum | - | COUNTRY, STATE, COUNTY, CITY, or DISTRICT |
| country | string | - | ISO 3166-1 alpha-2 country code |
| taxType | string enum | - | VAT, GST, SALES_TAX, IMPORT_VAT, or OTHER |
| providerTaxType | string | - | The tax provider's own type label, passed through unchanged |
| rate | decimal | - | Rate for this charge |
| amount | decimal | - | Amount for this charge |
| taxableAmount | decimal | - | Amount this charge was calculated on |
taxAmount and dutyAmount for money flows. TAX entries sum to taxAmount and IMPORT_* entries sum to dutyAmount, but the breakdown is for display and reconciliation - do not derive amounts by filtering this list.Error Responses
| HTTP | Code | Description |
|---|---|---|
| 400 | invalid_argument | Missing or invalid field - check field requirements. A malformed body, or an unknown tax identifier type, is reported on the field "body". Includes legacy tax-identifier shapes (e.g. type "CPF" without the BR_ prefix). |
| 400 | missing_country_of_origin | A physical line item on a cross-border order has no countryOfOrigin. The field names the line index. |
| 400 | shipping_address_required | The cart contains a PHYSICAL line item but no customer.shippingAddress |
| 400 | invalid_state | Please activate Outpost Taxes / Tax of Record to process transactions |
| 409 | region_not_activated | Merchant not activated for the customer's tax jurisdiction |
| 409 | region_not_supported | Customer location outside supported jurisdictions |
| 409 | physical_goods_not_supported | Physical goods cannot be priced for that destination yet |
| 409 | integration_credentials_invalid | The tax provider rejected the configured integration credentials |
| 409 | integration_not_configured | Tax compliance integration is not configured |
| 422 | nfse_validation_failed | Brazil NFS-e pre-payment validation rejected the customer data. Each reason is returned on the field "customer". |
| 502 | tax_provider_unavailable | The tax provider is temporarily unavailable. Safe to retry with backoff - this is the only retryable failure on this endpoint. |
| 401 | - | Invalid or missing Authorization token |
Code Example
# Step 2: Calculate tax for the transaction
curl -X POST "https://api.outpostanywhere.com/api/tax/calculations" \
-H "Authorization: Bearer $OUTPOST_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"currency": "EUR",
"taxBehavior": "EXCLUSIVE",
"customer": {
"merchantCustomerReference": "CUST-001",
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"billingAddress": {
"line1": "123 Main St",
"city": "Berlin",
"postalCode": "10115",
"country": "DE"
}
},
"lineItems": [
{
"merchantLineItemReference": "ITEM-001",
"productCode": "SKU-123",
"description": "Sample Product",
"unitPrice": 19.99,
"quantity": 1
}
],
"evidence": {
"billingCountry": "DE",
"ipAddress": "192.168.1.1",
"paymentMethodCountry": "DE"
}
}' | jq
# Response: 201 Created with taxCalculationId
# Save the taxCalculationId for the Store stepCreate Tax Transaction
POST/api/tax/transactionsConverts a valid tax calculation into a confirmed tax transaction. This endpoint should be called when an order is finalized/paid. Only call after payment authorization/capture succeeds.
Request Headers
| Parameter | Type | Required | Description |
|---|---|---|---|
| Authorization | - | Yes | Bearer token for authentication |
| Content-Type | - | Yes | application/json |
Request Body
{
"taxCalculationId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"merchantTransactionReference": "ORDER-2026-001234",
"payment": {
"processor": "stripe",
"paymentReference": "ch_3Q1234567890abcdef"
}
}Request Body Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| taxCalculationId | string | Yes | ID of the tax calculation generated by Outpost to convert |
| merchantTransactionReference | string | Yes | Your unique order/transaction reference |
| payment | object | No | Payment processor information |
| payment.processor | string | No | Payment processor name (e.g., "stripe", "adyen") |
| payment.paymentReference | string | No | Payment processor transaction ID |
Response 200 OK
{
"transactionId": "e83a9c47-2b5d-4f8a-9c12-3d4e5f6a7b8c",
"refunded": false,
"createdAt": "2026-01-05T12:05:00Z",
"confirmedAt": "2026-01-05T12:05:00Z",
"currency": "EUR",
"subTotalAmount": "99.99",
"discountAmount": "10.00",
"netAmount": "89.99",
"taxAmount": "18.90",
"totalAmount": "108.89",
"merchantTransactionReference": "ORDER-2026-001234",
"transactionDate": "2026-01-05T12:05:00Z",
"customer": {
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"billingAddress": {
"line1": "Keizersgracht 123",
"line2": "Apt 4B",
"city": "Amsterdam",
"postalCode": "1015 CJ",
"country": "NL"
}
},
"payment": {
"processor": "stripe",
"paymentReference": "ch_3Q1234567890abcdef"
},
"refundsOnTransaction": [],
"lineItems": [
{
"lineItemId": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"merchantLineItemReference": "ITEM-001",
"productCode": "TEST-SKU-005",
"description": "Premium Subscription - Annual",
"unitPrice": "99.99",
"quantity": 1,
"discountAmount": "10.00",
"itemSubTotalAmount": "99.99",
"itemNetAmount": "89.99",
"itemTotalAmount": "108.89",
"tax": {
"rate": "21.00",
"taxAmount": "18.90",
"currency": "EUR"
},
"refunded": false
}
],
"evidence": {
"billingCountry": "NL",
"ipAddress": "185.23.108.42",
"paymentMethodCountry": "NL"
}
}Error Responses
| HTTP | Code | Description |
|---|---|---|
| 400 | invalid_argument | Missing or invalid field - check field requirements. taxCalculationId must be a valid UUID. |
| 400 | invalid_state | Tax calculation has expired. Returned on the field taxCalculationId - create a new calculation. |
| 400 | invalid_state | Tax calculation has already been used. Returned on the field taxCalculationId. |
| 400 | invalid_state | Transaction with that merchantTransactionReference already exists. Returned on the field merchantTransactionReference - see Idempotency below. |
| 400 | invalid_state | Please activate Outpost Taxes / Tax of Record to process transactions. Returned on the field status. |
| 400 | invalid_state | The transaction comes from a customer location that is not activated by Outpost. Note this is a 400 here, not the 409 region_not_activated returned by Create Tax Calculation. |
| 400 | invalid_state | MoR product is already enabled in this region |
| 404 | not_found | Tax calculation not found |
Code Example
# Step 3: Store the transaction using taxCalculationId from calculate response
curl -X POST "https://api.outpostanywhere.com/api/tax/transactions" \
-H "Authorization: Bearer $OUTPOST_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"taxCalculationId": "'"$TAX_CALCULATION_ID"'",
"merchantTransactionReference": "TXN-001",
"payment": {
"paymentReference": "PAY-001",
"processor": "stripe"
}
}' | jq
# Response: 200 OK with TaxTransactionResponseGet Tax Transaction
GET/api/tax/transactions/{transaction_id}Retrieves the details of an existing tax transaction.
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| transaction_id | string | Yes | ID of the transaction |
Response 200 OK
Returns a TaxTransactionResponse object (same structure as Create Tax Transaction response).
Error Responses
| HTTP | Code | Description |
|---|---|---|
| 400 | invalid_argument | transaction_id is not a valid transaction id |
| 404 | not_found | Transaction not found |
Get Transaction Refunds
GET/api/tax/transactions/{transaction_id}/refundsRetrieves all refunds associated with a transaction.
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| transaction_id | string | Yes | ID of the original transaction |
Response 200 OK
[
{
"refundTransactionId": "d1e2f3a4-5b6c-7d8e-9f0a-1b2c3d4e5f6a",
"merchantRefundReference": "REFUND-2026-000456",
"refundDate": "2026-01-10T15:30:00Z",
"refundLineItems": [
{
"lineItemId": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"refundLineItemReference": "REFUND-ITEM-001",
"refundReason": "Customer requested cancellation",
"refundItemSubTotalAmount": "99.99",
"refundItemTotalAmount": "108.89",
"refundTax": {
"rate": "21.00",
"taxAmount": "18.90",
"currency": "EUR"
}
}
],
"createdAt": "2026-01-10T15:30:00Z",
"currency": "EUR",
"subTotalAmount": "99.99",
"taxAmount": "18.90",
"totalAmount": "108.89"
}
]Error Responses
| HTTP | Code | Description |
|---|---|---|
| 400 | invalid_argument | transaction_id is not a valid transaction id |
| 404 | not_found | Transaction not found |
Create Refund Transaction
POST/api/tax/transactions/{transaction_id}/refundsCreates a refund for one or more line items in an existing transaction.
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| transaction_id | string | Yes | ID of the original transaction |
Request Body
{
"merchantRefundReference": "REFUND-2026-000456",
"refundDate": "2026-01-10T15:30:00Z",
"refundLineItems": [
{
"lineItemId": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"refundLineItemReference": "REFUND-ITEM-001",
"refundReason": "Customer requested cancellation"
}
]
}Request Body Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| merchantRefundReference | string | Yes | Your unique refund reference |
| refundDate | string | No | ISO 8601 date/time of the refund |
| refundLineItems | array | Yes | Array of line items to refund (minimum 1) |
| refundLineItems[].lineItemId | string | Yes | ID of the original line item |
| refundLineItems[].refundLineItemReference | string | Yes | Your unique reference for this refund item |
| refundLineItems[].refundReason | string | Yes | Reason for the refund |
Response 200 OK
{
"refundTransactionId": "d1e2f3a4-5b6c-7d8e-9f0a-1b2c3d4e5f6a",
"merchantRefundReference": "REFUND-2026-000456",
"refundDate": "2026-01-10T15:30:00Z",
"refundLineItems": [
{
"lineItemId": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"refundLineItemReference": "REFUND-ITEM-001",
"refundReason": "Customer requested cancellation",
"refundItemSubTotalAmount": "99.99",
"refundItemTotalAmount": "108.89",
"refundTax": {
"rate": "21.00",
"taxAmount": "18.90",
"currency": "EUR"
}
}
],
"createdAt": "2026-01-10T15:30:00Z",
"currency": "EUR",
"subTotalAmount": "99.99",
"taxAmount": "18.90",
"totalAmount": "108.89"
}Error Responses
| HTTP | Code | Description |
|---|---|---|
| 400 | invalid_argument | Missing or invalid field. refundDate must match yyyy-MM-ddTHH:mm:ssZ and cannot be later than today. |
| 400 | invalid_state | Refund with that merchantRefundReference already exists. Returned on the field merchantRefundReference. |
| 404 | not_found | Transaction or line items not found |
Import Catalogue Product
POST/api/catalogue/productsImports a single product variant into your catalogue for tax classification. Each call imports one variant. To model a product that comes in multiple options (size, colour, plan tier), send one request per variant and group them under a shared parent product.
Request Headers
| Parameter | Type | Required | Description |
|---|---|---|---|
| Authorization | - | Yes | Bearer token for authentication |
| Content-Type | - | Yes | application/json |
Request Body
{
"productId": "TSHIRT-001-M",
"productGroupId": "TSHIRT-001",
"title": "Classic T-Shirt",
"requiresShipping": true,
"taxable": true,
"description": "100% cotton crew-neck t-shirt",
"category": "Apparel",
"imageUrl": "https://cdn.example.com/tshirt-001.png",
"sku": "TSHIRT-001-M",
"hsCode": "6109.10",
"countryOfOrigin": "PT",
"priceAmount": 24.99,
"priceCurrency": "USD"
}Request Body Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| productId | string | Yes | Unique identifier for this variant (the sellable unit). Used as the variant key. |
| productGroupId | string | No | Parent product identifier. Share across variants to group them under one product. Defaults to productId (standalone) when omitted. |
| title | string | Yes | Product title. Product-level - keep identical across variants of a group. |
| requiresShipping | boolean | Yes | Whether the item ships physically. |
| taxable | boolean | No | Whether the item is taxable (default: true). |
| description | string | No | Product description. Product-level. |
| category | string | No | Product category / type. Product-level. |
| imageUrl | string | No | Product image URL. Product-level. |
| sku | string | No | Stock keeping unit. Variant-level. |
| hsCode | string | No | Harmonized System code for customs. Variant-level. |
| countryOfOrigin | string | No | ISO 3166-1 alpha-2 country of origin. Variant-level. |
| priceAmount | decimal | No | Unit price (default: 0). Variant-level. |
| priceCurrency | string | No | ISO 4217 currency code (default: USD). Variant-level. |
Response 201 Created
{
"productId": "TSHIRT-001-M",
"productGroupId": "TSHIRT-001",
"title": "Classic T-Shirt",
"requiresShipping": true,
"status": "IMPORTED",
"approvedTaxCode": null,
"predictedTaxCode": null
}Response Fields
| Parameter | Type | Required | Description |
|---|---|---|---|
| productId | string | - | The variant identifier you supplied. |
| productGroupId | string | null | - | Parent product id. null when the product is its own group (standalone). |
| title | string | - | Product title. |
| requiresShipping | boolean | - | Whether the item ships physically. |
| status | string | - | Classification status: IMPORTED, EXTRACTED, READY_FOR_CLASSIFICATION, APPROVED, or ARCHIVED. A fresh import returns IMPORTED. |
| approvedTaxCode | string | null | - | Operator-approved tax code. null until a tax code is approved. |
| predictedTaxCode | string | null | - | Classifier-predicted tax code. null until classification runs. |
Product grouping
Two fields control grouping: productId identifies the variant (the sellable unit), and productGroupId identifies the parent product. There are two ways to structure a product:
- Standalone product - omit
productGroupId. The variant becomes its own group and the response returnsproductGroupIdasnull. Use this for single-option items such as an eBook or a one-off SKU. - Multi-variant product - send several requests that share one
productGroupId, each with a distinctproductId. Every call attaches one variant to the same parent product - for example groupTSHIRT-001with variantsTSHIRT-001-S,TSHIRT-001-MandTSHIRT-001-L.
Standalone product example:
{
"productId": "EBOOK-101",
"title": "Tax Compliance Handbook (eBook)",
"requiresShipping": false,
"priceAmount": 9.99,
"priceCurrency": "USD"
}Product-level vs variant-level fields
- Product-level (
title,description,category,imageUrl) is keyed by the group. Keep these identical across all variants of one group. - Variant-level (
sku,priceAmount,priceCurrency,taxable,requiresShipping,hsCode,countryOfOrigin) is specific to each variant.
Re-importing
- Re-sending a variant with identical content is a no-op.
- Changing
titleordescriptionwhile the variant isAPPROVEDclears the approval and returns the variant toREADY_FOR_CLASSIFICATIONfor re-review. - Changing only
priceAmount,hsCodeorcountryOfOriginupdates the variant silently and keeps any approval. - Importing a new
productIdunder an existingproductGroupIdadds a variant. Tax codes are not inherited; each variant is classified and approved on its own.
Classification lifecycle
The status field moves through IMPORTED → EXTRACTED → READY_FOR_CLASSIFICATION → APPROVED → ARCHIVED. predictedTaxCode is set by the classifier after import; approvedTaxCode is set when an operator approves. A fresh import returns IMPORTED with both codes null.
approvedTaxCode; tax is still determined per line item at calculation time.Error Responses
| HTTP | Code | Description |
|---|---|---|
| 400 | validation_error | Missing required field - productId, title, or requiresShipping |
| 401 | - | Invalid or missing Authorization token |
| 409 | - | Conflict - the product could not be imported in its current state |
Code Example
# Import a product variant into the catalogue
curl -X POST "https://api.outpostanywhere.com/api/catalogue/products" \
-H "Authorization: Bearer $OUTPOST_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"productId": "TSHIRT-001-M",
"productGroupId": "TSHIRT-001",
"title": "Classic T-Shirt",
"requiresShipping": true,
"sku": "TSHIRT-001-M",
"priceAmount": 24.99,
"priceCurrency": "USD"
}' | jq
# Response: 201 Created with the product status and tax codes
# Omit productGroupId to import a standalone productResource
Proforma Invoices
Issue a proforma invoice for a B2B customer paying by bank transfer.
Create Proforma Invoice
/api/proforma-invoicesGenerates a proforma invoice for a B2B customer who is paying by bank transfer. A proforma invoice is a non-fiscal request for payment: it states what is owed, to whom, and how to pay, before the money has been received. Outpost renders the PDF, stores it, and returns a link to it directly in the response so you can surface it on your checkout.
How it works
- Your checkout collects the B2B customer details, line items, and jurisdiction.
- Your backend calls POST /api/proforma-invoices with that payload.
- Outpost generates the PDF, stores it, and returns invoice.url in the response.
- You display or email the proforma invoice to the customer with bank-transfer instructions.
- When the transfer settles, Outpost sends a proforma_invoice.settled webhook so you can fulfill the order.
Request Headers
| Parameter | Type | Required | Description |
|---|---|---|---|
| Authorization | - | Yes | Bearer token for authentication |
| Content-Type | - | Yes | application/json |
Request Body Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| jurisdiction | string | Yes | Tax jurisdiction the invoice is issued under (ISO 3166-1 alpha-2). Determines numbering, format, and legal text. |
| total | object | Yes | Invoice total |
| total.amount | string | Yes | Total amount as a decimal string (e.g., "1111.00") |
| total.currency | string | Yes | ISO 4217 currency code (e.g., GBP, EUR, USD) |
| recipient | object | Yes | The B2B customer being invoiced |
| recipient.legalName | string | Yes | Registered legal name of the customer |
| recipient.address | object | Yes | Customer billing address |
| recipient.address.line1 | string | Yes | Primary address line |
| recipient.address.line2 | string | No | Secondary address line |
| recipient.address.locality | string | Yes | City or locality |
| recipient.address.region | string | Yes | State, province, or region |
| recipient.address.postalCode | string | Yes | Postal or ZIP code |
| recipient.address.country | string | Yes | ISO 3166-1 alpha-2 country code |
| recipient.taxIdentifiers | array | Yes | Customer tax identifiers (e.g., CNPJ, VAT) |
| recipient.taxIdentifiers[].type | string | Yes | Identifier type (e.g., VAT, EIN) |
| recipient.taxIdentifiers[].code | string | Yes | The identifier value |
| recipient.taxIdentifiers[].country | string | Yes | Issuing country (ISO 3166-1 alpha-2) |
| recipient.contact | object | Yes | Customer contact details |
| recipient.contact.email | string | Yes | Email address for invoice delivery |
| recipient.buyerTaxRegime | string | Conditional | Brazilian tax regime of the customer: SIMPLES_NACIONAL, LUCRO_PRESUMIDO, or LUCRO_REAL. Required when the customer has a CNPJ tax identifier. |
| provider | object | Yes | The entity supplying the goods or services |
| provider.providerName | string | Yes | Legal name of the supplying entity |
| lineItems | array | Yes | One or more invoice line items |
| lineItems[].description | string | Yes | Description of the item |
| lineItems[].quantity | string | Yes | Quantity as a decimal string |
| lineItems[].unitPrice | object | Yes | Price per unit |
| lineItems[].unitPrice.amount | string | Yes | Unit price as a decimal string |
| lineItems[].unitPrice.currency | string | Yes | ISO 4217 currency code |
| lineItems[].classifications | array | No | Tax or product classification codes for the item |
| issueDate | string | Yes | Issue date (ISO 8601 date, YYYY-MM-DD) |
| dueDate | string | Yes | Payment due date (ISO 8601 date, YYYY-MM-DD) |
| notices | array | No | Free-text notices to print on the invoice (e.g., payment instructions) |
| merchantReference | string | No | Your own reference for this invoice, such as an order ID. Echoed back in the response so you can correlate the invoice with your order. |
Response 201 Created
{
"proformaInvoiceId": "019d4d40-15cf-7764-ad6e-b2ec47736bb9",
"status": "ISSUED",
"invoice": {
"fileName": "proforma-invoice-2026-000123.pdf",
"url": "https://storage.outpostanywhere.com/proforma-invoices/019d4d40-...pdf",
"expiresAt": "2026-06-01T12:15:00Z"
},
"total": { "amount": "1200.00", "currency": "GBP" },
"issueDate": "2026-06-01",
"dueDate": "2026-06-15",
"createdAt": "2026-06-01T12:00:00Z",
"merchantReference": "order-8842"
}Response Fields
| Parameter | Type | Required | Description |
|---|---|---|---|
| proformaInvoiceId | string (UUID) | - | Outpost identifier for the proforma invoice. Use it to reconcile settlement webhooks. |
| status | string | - | Document status: ISSUED, INVOICED, BOOKED, CANCELLATION_REQUESTED, or CANCELLED. ISSUED on creation. Payment is tracked separately, so listen for the proforma_invoice.settled webhook to learn when the transfer clears. |
| invoice | object | - | Generated invoice file information |
| invoice.fileName | string | - | Name of the generated proforma invoice PDF |
| invoice.url | string | - | Pre-signed link to the proforma invoice PDF. Show it on your checkout or email it to the customer. |
| invoice.expiresAt | string | - | ISO 8601 timestamp when the download link expires (15 minutes). Re-fetch the invoice to mint a fresh link. |
| total | object | - | Echoed invoice total |
| issueDate | string | - | Echoed issue date |
| dueDate | string | - | Echoed due date |
| createdAt | string | - | ISO 8601 creation timestamp |
| merchantReference | string | - | Your reference, echoed back from the request. Null when you did not send one. |
Error Responses
| HTTP | Code | Description |
|---|---|---|
| 400 | invalid_argument | Missing or invalid required field (e.g., jurisdiction, recipient, or lineItems) |
| 401 | - | Invalid or missing Authorization token |
| 422 | unsupported_jurisdiction | Proforma invoices are not yet available for the requested jurisdiction |
| 422 | no_payment_destination | No settlement account is configured for the invoice currency, so there is nowhere for the customer to pay |
| 422 | nfse_validation_failed | The recipient details do not pass Brazilian e-invoicing (NFS-e) validation. The errors list names the fields to correct. |
Code Example
# Create a proforma invoice for a B2B customer paying by bank transfer
curl -X POST \
"https://api.outpostanywhere.com/api/proforma-invoices" \
-H "Authorization: Bearer $OUTPOST_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jurisdiction": "GB",
"total": { "amount": "1200.00", "currency": "GBP" },
"recipient": {
"legalName": "Acme Corp",
"address": {
"line1": "123 Example Street",
"line2": "",
"locality": "London",
"region": "Greater London",
"postalCode": "EC1A 1BB",
"country": "GB"
},
"taxIdentifiers": [
{ "type": "VAT", "code": "GB123456789", "country": "GB" }
],
"contact": { "email": "billing@acme.example" }
},
"provider": { "providerName": "Example Ltd" },
"lineItems": [
{
"description": "Annual subscription",
"quantity": "1",
"unitPrice": { "amount": "1200.00", "currency": "GBP" },
"classifications": []
}
],
"issueDate": "2026-06-01",
"dueDate": "2026-06-15",
"notices": [],
"merchantReference": "order-8842"
}' | jq
# Response: 201 Created - body contains invoice.url, the link to the PDFResource
Invoices
Retrieve the tax invoice for a payment or refund, by Outpost ID or PSP reference, and fetch the invoice for a tax transaction.
Get Payment Invoice by ID
/api/payments/{paymentId}/invoiceRetrieves the B2C tax invoice for a payment using the Outpost payment ID.
Request Headers
| Parameter | Type | Required | Description |
|---|---|---|---|
| Authorization | - | Yes | Bearer token for authentication |
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| paymentId | string (UUID) | Yes | Outpost payment identifier |
Response 200 OK
{
"invoice": {
"fileName": "invoice-7110000000023059375.pdf",
"url": "https://storage.outpostanywhere.com/invoices/...",
"expiresAt": "2026-01-05T12:15:00Z"
}
}Response Fields
| Parameter | Type | Required | Description |
|---|---|---|---|
| invoice | object | - | Invoice file information |
| invoice.fileName | string | - | Name of the invoice PDF file |
| invoice.url | string | - | Pre-signed URL to download the invoice |
| invoice.expiresAt | string | - | ISO 8601 timestamp when the download URL expires (15 minutes) |
Response 202 Accepted - Invoice is still being generated. Retry after a short delay.
Error Responses
| HTTP | Code | Description |
|---|---|---|
| 404 | - | Payment/refund not found or does not belong to your merchant account |
| 401 | - | Invalid or missing Authorization token |
Code Example
# Get payment invoice by Outpost payment ID
curl -X GET \
"https://api.outpostanywhere.com/api/payments/${PAYMENT_ID}/invoice" \
-H "Authorization: Bearer $OUTPOST_ACCESS_TOKEN" | jq
# Response: 200 OK with invoice download URL
# Response: 202 Accepted if invoice is still being generatedGet Payment Invoice by PSP Reference
/api/payments/invoice?psp_reference={psp_reference}Retrieves the B2C tax invoice for a payment using the PSP reference (e.g., Adyen pspReference or Stripe charge ID).
Request Headers
| Parameter | Type | Required | Description |
|---|---|---|---|
| Authorization | - | Yes | Bearer token for authentication |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| psp_reference | string | Yes | Payment reference from your PSP |
Response 200 OK
{
"invoice": {
"fileName": "invoice-7110000000023059375.pdf",
"url": "https://storage.outpostanywhere.com/invoices/...",
"expiresAt": "2026-01-05T12:15:00Z"
}
}Response 202 Accepted - Invoice is still being generated. Retry after a short delay.
Error Responses
| HTTP | Code | Description |
|---|---|---|
| 404 | - | Payment/refund not found or does not belong to your merchant account |
| 401 | - | Invalid or missing Authorization token |
Code Example
# Get payment invoice by PSP reference
curl -X GET \
"https://api.outpostanywhere.com/api/payments/invoice?psp_reference=7110000000023059375" \
-H "Authorization: Bearer $OUTPOST_ACCESS_TOKEN" | jqGet Refund Invoice by ID
/api/refunds/{refundId}/invoiceRetrieves the B2C tax invoice for a refund using the Outpost refund ID.
Request Headers
| Parameter | Type | Required | Description |
|---|---|---|---|
| Authorization | - | Yes | Bearer token for authentication |
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| refundId | string (UUID) | Yes | Outpost refund identifier |
Response 200 OK
{
"invoice": {
"fileName": "invoice-7110000000023059375.pdf",
"url": "https://storage.outpostanywhere.com/invoices/...",
"expiresAt": "2026-01-05T12:15:00Z"
}
}Response 202 Accepted - Invoice is still being generated. Retry after a short delay.
Error Responses
| HTTP | Code | Description |
|---|---|---|
| 404 | - | Payment/refund not found or does not belong to your merchant account |
| 401 | - | Invalid or missing Authorization token |
Code Example
# Get refund invoice by Outpost refund ID
curl -X GET \
"https://api.outpostanywhere.com/api/refunds/${REFUND_ID}/invoice" \
-H "Authorization: Bearer $OUTPOST_ACCESS_TOKEN" | jqGet Refund Invoice by PSP Reference
/api/refunds/invoice?psp_reference={psp_reference}Retrieves the B2C tax invoice for a refund using the PSP reference.
Request Headers
| Parameter | Type | Required | Description |
|---|---|---|---|
| Authorization | - | Yes | Bearer token for authentication |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| psp_reference | string | Yes | Refund reference from your PSP |
Response 200 OK
{
"invoice": {
"fileName": "invoice-7110000000023059375.pdf",
"url": "https://storage.outpostanywhere.com/invoices/...",
"expiresAt": "2026-01-05T12:15:00Z"
}
}Response 202 Accepted - Invoice is still being generated. Retry after a short delay.
Error Responses
| HTTP | Code | Description |
|---|---|---|
| 404 | - | Payment/refund not found or does not belong to your merchant account |
| 401 | - | Invalid or missing Authorization token |
Code Example
# Get refund invoice by PSP reference
curl -X GET \
"https://api.outpostanywhere.com/api/refunds/invoice?psp_reference=7110000000023060251" \
-H "Authorization: Bearer $OUTPOST_ACCESS_TOKEN" | jqGet Transaction Invoice
GET/api/tax/transactions/{transaction_id}/invoiceRetrieves the invoice file for a transaction.
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| transaction_id | string | Yes | ID of the transaction |
Response 200 OK
{
"b2cInvoice": {
"fileName": "invoice-e83a9c47-2b5d-4f8a-9c12-3d4e5f6a7b8c.pdf",
"url": "https://storage.outpostnow.com/invoices/..."
}
}Response Fields
| Parameter | Type | Required | Description |
|---|---|---|---|
| b2cInvoice | object | - | B2C invoice information |
| b2cInvoice.fileName | string | - | Name of the invoice file |
| b2cInvoice.url | string | - | URL to download the invoice |
Response 202 Accepted - Invoice is still being generated. Retry after a short delay.
The url is a signed link that expires one hour after the response. Download the file rather than storing the link.
Error Responses
| HTTP | Code | Description |
|---|---|---|
| 400 | invalid_argument | transaction_id is not a valid transaction id |
| 404 | not_found | Transaction not found |
Resource
Stripe Billing
Outpost-as-MoR endpoints used when you run Stripe Billing on your own account. Conceptual walkthrough lives under Integrations → Stripe Billing.
Three outbound endpoints covering the payment lifecycle (provision a SetupIntent, confirm the payment, refund), plus inbound webhook events on a single channel. Authentication uses the same Bearer token described in Authentication.
Create Setup Intent
/api/payments/stripe/setup-intentsStripeProvisions a SetupIntent on Outpost’s PSP account with usage=off_session.
The returned client_secret is passed to Stripe Elements in the browser so the customer can confirm payment details directly against Stripe - no sensitive payment data ever touches the merchant or Outpost servers.
The set of payment methods offered to the customer is controlled by payment_method_types on the request body (defaults to ["card"]).
Request Headers
| Parameter | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer access token - see Authentication above. |
Request Body
application/json
{
"payment_method_types": ["card"],
"tax_calculation_id": "taxc_a1b2c3d4e5f6789"
}Request Fields
| Parameter | Type | Required | Description |
|---|---|---|---|
| payment_method_types | string[] | No | Array of Stripe PM types to enable on the SetupIntent. Defaults to ["card"]. Supported: card, us_bank_account, sepa_debit, bacs_debit, au_becs_debit. Mandate text for direct debits is rendered automatically by Payment Element. |
| tax_calculation_id | string | No | Reference to a tax calculation returned by POST /api/tax/calculate. Binds the tax breakdown to this SetupIntent so it carries through to the first chargeable invoice (e.g. at trial end). |
Response 200 OKStripe
{
"client_secret": "seti_1QwxyzABC_secret_xyzXYZ123",
"setup_intent_id": "seti_1QwxyzABC",
"mor_customer_id": "cus_a1b2c3d4e5f6789",
"status": "requires_payment_method"
}Response Fields
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_secret | string | - | Pass to Stripe Elements via loadStripe(OUTPOST_PUBLISHABLE_KEY) and confirmSetup({ clientSecret, ... }). |
| setup_intent_id | string | - | SetupIntent ID on Outpost’s PSP account. |
| mor_customer_id | string | - | Outpost’s Stripe Customer ID (empty Customer with metadata pointing back at your merchant_customer.stripe_id). Persist for future reference. |
| status | string | - | One of requires_payment_method, requires_confirmation, requires_action, processing, succeeded. |
Error Responses
| HTTP | Code | Description |
|---|---|---|
| 400 | invalid_request | Malformed body, or invalid value in payment_method_types. |
| 401 | - | Missing or invalid Bearer token. |
| 403 | merchant_suspended | Merchant account is suspended, or the token lacks the required scope. |
| 404 | tax_calculation_not_found | The tax_calculation_id does not resolve to a known calculation. |
| 409 | idempotency_conflict | Same Idempotency-Key replayed with a different request body. |
| 422 | validation_error | A payment_method_types value is not enabled for your account on Outpost’s PSP. |
| 429 | rate_limited | Burst exceeded your quota. Retry after backoff. |
| 503 | psp_unavailable | Upstream PSP (Stripe) returned 5xx or timed out. Safe to retry with the same Idempotency-Key. |
Create Payment Intent
/api/payments/stripe/payment-intentsStripeUsed for the immediate-payment signup variant - the customer is charged at signup (no free trial). Outpost provisions a PaymentIntent on its PSP account with setup_future_usage=off_session so the same card can be charged for renewals later.
The browser confirms with stripe.confirmPayment() (atomic card collection + 3DS + charge). Outpost learns the outcome via its own payment_intent.succeeded webhook - no follow-up Outpost API call is required.
Request Headers
| Parameter | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer access token - see Authentication above. |
Request Body
application/json
{
"amount": 1990,
"currency": "BRL",
"merchant_customer": {
"stripe_id": "cus_QmerchantSide",
"email": "jordi@example.com.br",
"name": "Jordi Silva",
"country": "BR",
"address": { "line1": "Av. Paulista 1234", "city": "São Paulo" }
},
"description": "Pro Monthly subscription - first payment",
"tax_calculation_id": "taxc_a1b2c3d4e5f6789"
}Request Fields
| Parameter | Type | Required | Description |
|---|---|---|---|
| amount | integer | Yes | Smallest currency unit (e.g. cents for USD, centavos for BRL). Minimum 1. |
| currency | string (ISO 4217) | Yes | Three-letter currency code, e.g. BRL. |
| merchant_customer.stripe_id | string | Yes | Customer ID on your own Stripe account. Stored as metadata on the empty MoR-side Customer. |
| merchant_customer.email | string | No | Customer email. Used for receipt routing and fraud signals. |
| merchant_customer.name | string | No | Customer name. |
| merchant_customer.country | string (ISO 3166-1 alpha-2) | No | Customer country code. |
| merchant_customer.address | object | No | Billing address (line1, city, postal_code, etc.). |
| description | string | No | Free-form description shown on the underlying PaymentIntent. Max 255 chars. |
| tax_calculation_id | string | No | Reference to a tax calculation returned by POST /api/tax/calculate. Binds the calculated tax breakdown to this payment. |
Response 200 OKStripe
{
"client_secret": "pi_3QzABC_secret_xyzXYZ123",
"payment_intent_id": "pi_3QzABC",
"mor_customer_id": "cus_a1b2c3d4e5f6789",
"status": "requires_payment_method"
}Response Fields
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_secret | string | - | Pass to Stripe Elements via loadStripe(OUTPOST_PUBLISHABLE_KEY) and confirm with stripe.confirmPayment({ clientSecret, … }). |
| payment_intent_id | string | - | PaymentIntent ID on Outpost’s PSP account. |
| mor_customer_id | string | - | Outpost’s Stripe Customer ID (empty Customer with metadata pointing back at your merchant_customer.stripe_id). Persist for future reference. |
| status | string | - | PaymentIntent status - typically requires_payment_method at this point, and the browser will transition it via confirmPayment(). |
Error Responses
| HTTP | Code | Description |
|---|---|---|
| 400 | invalid_request | Missing or malformed amount / currency / merchant_customer.stripe_id. |
| 401 | - | Missing or invalid Bearer token. |
| 403 | merchant_suspended | Merchant account is suspended, or the token lacks the required scope. |
| 404 | tax_calculation_not_found | The tax_calculation_id does not resolve to a known calculation. |
| 409 | idempotency_conflict | Same Idempotency-Key replayed with a different request body. |
| 422 | unsupported_currency / amount_out_of_range | Currency not enabled on Outpost’s PSP, amount below the PSP’s minimum, or amount above your per-transaction cap. |
| 429 | rate_limited | Burst exceeded your quota. Retry after backoff. |
| 503 | psp_unavailable | Upstream PSP (Stripe) returned 5xx or timed out. Safe to retry with the same Idempotency-Key. |
After stripe.confirmPayment() succeeds in the browser, you receive payment_intent.id and latest_charge (Stripe ch_…). Use that ch_… as processor_details.custom.payment_reference on payment_records/report_payment on your Stripe account.
Refund Payment
/api/payments/{paymentId}/refundRefunds a previously confirmed payment, in full or in part. Outpost executes the refund on its PSP account and emits a payment.refunded webhook so you can update the corresponding PaymentRecord on Stripe.
Partial refunds are supported - supply amount to refund less than the full charge. Multiple refunds are allowed until the cumulative amount equals the original payment.
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| paymentId | string | Yes | The Outpost payment_id from the Confirm Payment response. |
Request Headers
| Parameter | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer access token. |
| Idempotency-Key | string | Yes | Required to safely retry refund attempts. Outpost derives downstream PSP idempotency keys from this value. |
Request Body
application/json - body is optional - omit for a full refund.
{
"amount": 990,
"reason": "requested_by_customer",
"metadata": {
"merchant_invoice_id": "in_1QzABC"
}
}Request Fields
| Parameter | Type | Required | Description |
|---|---|---|---|
| amount | integer | No | Smallest currency unit. Defaults to the unrefunded remainder of the payment, i.e. a full refund. Must be ≤ remaining refundable amount. |
| reason | string | No | One of requested_by_customer, duplicate, fraudulent. Free-form strings are also accepted and forwarded to the PSP. |
| metadata | object | No | Arbitrary key/value pairs persisted on the refund and forwarded to the PSP refund object. |
Response 200 OK
{
"id": "rfnd_a1b2c3d4e5f6789",
"payment_id": "pay_a1b2c3d4e5f6789",
"amount": 990,
"currency": "BRL",
"status": "succeeded",
"reason": "requested_by_customer",
"processor_references": {
"refund_id": "re_3QzABC..."
},
"created_at": "2026-05-12T11:00:00Z"
}Response Fields
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string | - | Outpost-owned refund identifier (rfnd_…). |
| payment_id | string | - | The payment this refund applies to. |
| amount | integer | - | Refunded amount in smallest currency unit. |
| currency | string | - | Echoes the payment currency. |
| status | string | - | One of succeeded, pending, failed. For cards and Link this is synchronous - for bank debits the refund starts as pending and transitions via webhook. |
| reason | string | null | - | Echoes the request reason. |
| processor_references.refund_id | string | - | Refund ID on Outpost’s PSP account (e.g. re_… on Stripe). |
| created_at | string (ISO 8601) | - | When the refund was created. |
Error Responses
| HTTP | Code | Description |
|---|---|---|
| 400 | invalid_request | amount exceeds the unrefunded remainder, or currency mismatch. |
| 401 | - | Missing or invalid Bearer token. |
| 404 | payment_not_found | No payment exists for the given paymentId. |
| 422 | payment_not_refundable | Payment is not in a refundable state (e.g. already fully refunded, never succeeded, or charged back). |
On success, also call Stripe payment_records.report_refund on your account with processor_details.custom.refund_reference = processor_references.refund_id so the PaymentRecord reflects the refund. The payment.refunded webhook (below) also fires - use whichever signal fits your reconciliation flow.
Verifying webhooks
Outpost signs every webhook delivery with HMAC-SHA256.
The header is Mor-Signature: t=<unix_seconds>,v1=<hex>.
The signed payload is the literal byte string "{t}.{raw_request_body}", keyed with the shared webhook secret Outpost gave you during onboarding.
Recommended: reject deliveries where t is older than five minutes, and dedupe on event.id.
Webhooks - payment.*
POST <your_webhook_url>During each renewal cycle Outpost posts exactly one event to the webhook URL you registered.
All three event types share the same envelope and the same HMAC verification - see Verifying webhooks.
Request Headers
| Parameter | Type | Required | Description |
|---|---|---|---|
| Mor-Signature | string | Yes | t=<unix_seconds>,v1=<hex_hmac_sha256>. HMAC computed over "{t}.{raw_request_body}" using the shared webhook secret. Reject deliveries older than ~5 minutes. |
Payload Fields
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string | - | Outpost event ID. Use to dedupe. |
| type | string | - | One of payment.succeeded, payment.failed, payment.requires_action, payment.refunded. |
| created | integer | - | Unix seconds. |
| data.object.payment_id | string | - | Outpost-owned payment identifier for this renewal cycle (pay_…). Use as the {paymentId} path param on POST /api/payments/{paymentId}/refund. |
| data.object.merchant_invoice_id | string | - | Stripe Invoice ID on your account that triggered this renewal. Pass to attach_payment. |
| data.object.merchant_customer_id | string | - | Customer ID on your Stripe account. |
| data.object.processor_charge_id | string | - | Charge ID on Outpost’s PSP account. Use as payment_reference on report_payment. Empty on failed / requires_action. |
| data.object.processor_payment_intent_id | string | - | PaymentIntent ID on Outpost’s PSP account. |
| data.object.amount | integer | - | For payment.succeeded / failed / requires_action: the payment amount. For payment.refunded: the amount refunded by this event. |
| data.object.currency | string | - | Three-letter currency code. |
| data.object.status | string | - | One of succeeded, failed, requires_action, partially_refunded, refunded. |
| data.object.failure_message | string | No | Human-readable decline reason (present on failed / requires_action). |
| data.object.decline_code | string | No | PSP decline code, e.g. insufficient_funds. |
| data.object.refund_id | string | No | Outpost-owned refund identifier. Only on payment.refunded. |
| data.object.processor_refund_id | string | No | Refund ID on Outpost’s PSP account. Only on payment.refunded. |
| data.object.amount_refunded_total | integer | No | Cumulative amount refunded across all refunds on this payment. Only on payment.refunded. |
| data.object.reason | string | No | Refund reason. Only on payment.refunded. |
Example - payment.succeeded
After verifying the signature, call Stripe POST /v1/payment_records/report_payment on the merchant’s account with outcome=guaranteed and processor_details.custom.payment_reference = processor_charge_id, then POST /v1/invoices/{merchant_invoice_id}/attach_payment to mark the invoice paid.
{
"id": "evt_a1b2c3d4",
"type": "payment.succeeded",
"created": 1747051200,
"data": {
"object": {
"payment_id": "pay_a1b2c3",
"merchant_invoice_id": "in_1QzABC",
"merchant_customer_id": "cus_QmerchantSide",
"processor_charge_id": "ch_3QzABC",
"processor_payment_intent_id": "pi_3QzABC",
"amount": 1990,
"currency": "BRL",
"status": "succeeded"
}
}
}Example - payment.failed
Leave the merchant invoice open - Stripe Billing’s Smart Retries will trigger another invoice.created on the next attempt, which Outpost will pick up and retry.
{
"id": "evt_failed1",
"type": "payment.failed",
"created": 1747051200,
"data": {
"object": {
"payment_id": "pay_b2c3d4",
"merchant_invoice_id": "in_1QzABC",
"merchant_customer_id": "cus_QmerchantSide",
"processor_charge_id": "",
"processor_payment_intent_id": "pi_3QzABC",
"amount": 1990,
"currency": "BRL",
"status": "failed",
"failure_message": "Your card was declined.",
"decline_code": "insufficient_funds"
}
}
}Example - payment.requires_action
Triggered when the renewal PaymentIntent requires customer authentication - SCA / 3DS for cards, mandate re-confirmation for direct debits, etc.
As with payment.failed, leave the invoice open and let Stripe Billing’s dunning surface the action to the customer.
{
"id": "evt_action1",
"type": "payment.requires_action",
"created": 1747051200,
"data": {
"object": {
"payment_id": "pay_c3d4e5",
"merchant_invoice_id": "in_1QzABC",
"merchant_customer_id": "cus_QmerchantSide",
"processor_charge_id": "",
"processor_payment_intent_id": "pi_3QzABC",
"amount": 1990,
"currency": "BRL",
"status": "requires_action",
"failure_message": "Authentication required.",
"decline_code": "authentication_required"
}
}
}Example - payment.refunded
Fired after every successful refund - full or partial. Use amount_refunded_total to decide whether the payment is now fully or partially refunded and call payment_records.report_refund on Stripe to keep the PaymentRecord in sync.
{
"id": "evt_refund1",
"type": "payment.refunded",
"created": 1747051200,
"data": {
"object": {
"payment_id": "pay_a1b2c3",
"merchant_invoice_id": "in_1QzABC",
"merchant_customer_id": "cus_QmerchantSide",
"refund_id": "rfnd_a1b2c3d4e5f6789",
"amount": 990,
"amount_refunded_total": 990,
"currency": "BRL",
"status": "partially_refunded",
"reason": "requested_by_customer",
"processor_refund_id": "re_3QzABC"
}
}
}Response Codes
| HTTP | Code | Description |
|---|---|---|
| 200 | - | Acknowledged. Outpost will not retry. |
| 4XX | - | Outpost gives up after 4xx (treat as permanent failure on your side). |
| 5XX | - | Outpost retries with exponential backoff. |
Async-settled payment methods
For cards and Link, payment.succeeded arrives within seconds of the PaymentIntent confirming.
For bank-debit methods (ACH, SEPA, BACS, BECS) the PaymentIntent first transitions through processing and the succeeded event only fires after settlement (days later).
Returns and mandate disputes that arrive after settlement are not yet exposed as a dedicated event - contact Outpost before rolling out async methods in production.
Resource
Webhooks
Register endpoints, verify delivery signatures, and consume the full event catalog across every domain.
Webhooks
Bank transfers settle asynchronously - sometimes minutes, sometimes days after you issue a proforma invoice. Rather than polling, register a webhook endpoint and Outpost will notify your backend the moment the money settles, so you can release the order or activate the subscription. The same endpoint also receives dispute and pre-chargeback alert events, so you subscribe once and pick the event types you need.
Register Webhook
/api/webhooksRegisters an endpoint to receive event notifications. The response includes a secret that is shown only once - store it to verify incoming signatures.
Request Headers
| Parameter | Type | Required | Description |
|---|---|---|---|
| Authorization | - | Yes | Bearer token for authentication |
| Content-Type | - | Yes | application/json |
Request Body Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| url | string | Yes | HTTPS endpoint that will receive event POSTs. Must resolve to a public address. |
| events | string[] | Yes | Event types to subscribe to. Must contain at least one of the event types listed below. |
| description | string | No | Human-readable label for this endpoint. Stored for your reference and not returned by the API. |
Response 201 Created
{
"id": "wh_019d4d40-15cf-7764-ad6e-b2ec47736bb9",
"url": "https://your-app.example.com/webhooks/outpost",
"events": ["proforma_invoice.settled"],
"secret": "whsec_8f2b...d41a",
"status": "ACTIVE",
"createdAt": "2026-06-01T12:00:00Z"
}Response Fields
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string | Webhook endpoint identifier. Use it to list or delete the endpoint. | |
| url | string | The registered destination URL | |
| events | string[] | Subscribed event types | |
| secret | string | Signing secret. Returned once on creation - store it securely to verify the Outpost-Signature header. | |
| status | string | ACTIVE or DISABLED | |
| createdAt | string | ISO 8601 creation timestamp |
Error Responses
| HTTP | Code | Description |
|---|---|---|
| 400 | unknown_event | events is empty or contains an event type that does not exist |
| 400 | invalid_url | url is not a valid HTTPS URI, or its host cannot be resolved or is not public |
| 401 | - | Invalid or missing Authorization token |
| 409 | webhook_endpoint_limit_exceeded | You already have the maximum of 10 active endpoints. Delete one before registering another. |
Code Example
# Register an endpoint to receive settlement notifications
curl -X POST \
"https://api.outpostanywhere.com/api/webhooks" \
-H "Authorization: Bearer $OUTPOST_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example.com/webhooks/outpost",
"events": ["proforma_invoice.settled"],
"description": "Settlement notifications for B2B bank transfers"
}' | jq
# Response: 201 Created - store the returned "secret" to verify signaturesList Webhooks
/api/webhooksReturns all webhook endpoints registered for your merchant account.
Response 200 OK
[
{
"id": "wh_019d4d40-15cf-7764-ad6e-b2ec47736bb9",
"url": "https://your-app.example.com/webhooks/outpost",
"events": ["proforma_invoice.settled"],
"status": "ACTIVE",
"createdAt": "2026-06-01T12:00:00Z"
}
]Code Example
# List all registered webhook endpoints
curl -X GET \
"https://api.outpostanywhere.com/api/webhooks" \
-H "Authorization: Bearer $OUTPOST_ACCESS_TOKEN" | jqDelete Webhook
/api/webhooks/{webhookId}Removes a webhook endpoint. It immediately stops receiving events.
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| webhookId | string | Identifier returned when the webhook was registered |
Response 204 No Content - The webhook was deleted.
Error Responses
| HTTP | Code | Description |
|---|---|---|
| 401 | - | Invalid or missing Authorization token |
| 404 | not_found | No such webhook endpoint, or it belongs to another merchant. Deleting an already-deleted endpoint succeeds. |
Code Example
# Delete a webhook endpoint so it stops receiving events
curl -X DELETE \
"https://api.outpostanywhere.com/api/webhooks/${WEBHOOK_ID}" \
-H "Authorization: Bearer $OUTPOST_ACCESS_TOKEN"
# Response: 204 No ContentWebhook events
Each subscribed event is delivered as an HTTP POST to your endpoint with a JSON body.
Delivery Semantics
- Acknowledge a delivery with any 2xx status. Each attempt has 30 seconds to respond.
- A 429 or 5xx response is retried. Any other 4xx response is a permanent failure and stops delivery of that event.
- Retries use exponential backoff: at most 8 attempts, the first 10 seconds after the failure, tripling each time, capped at 4 hours between attempts and 9 hours in total.
- Deliveries are at-least-once - deduplicate on the event id.
- The event id is deterministic, so a redelivered event always carries the same id.
- Always verify the Outpost-Signature header before acting on a payload.
Verifying Signature
Every delivery includes an Outpost-Signature header of the form t=<timestamp>,v1=<hmac>. Compute an HMAC-SHA256 of {t}.{rawBody} with your webhook secret and compare it against v1 in constant time. Reject requests whose timestamp is older than five minutes to prevent replay.
// Verify the Outpost-Signature header before trusting the payload
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
boolean verify(String rawBody, String header, String secret) throws Exception {
// header looks like: t=1717243200,v1=5257a869e7...
Map<String, String> parts = parseHeader(header);
String signedPayload = parts.get("t") + "." + rawBody;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
byte[] digest = mac.doFinal(signedPayload.getBytes());
String expected = HexFormat.of().formatHex(digest);
return MessageDigest.isEqual(expected.getBytes(), parts.get("v1").getBytes());
}Events
proforma_invoice.settledThe bank transfer for a proforma invoice has been received and reconciled. Safe to fulfill the order.
dispute.action_neededA dispute was opened on one of your payments and evidence is needed. The payload carries the deadline and a link to the dispute in the Outpost dashboard.
dispute.outcomeA dispute has been resolved. The payload carries the final outcome.
pre_chargeback_alert.receivedA pre-chargeback alert was raised on one of your payments and matched to it. Delivered only once the alert is matched to a payment.
pre_chargeback_alert.outcomeOutpost finished handling a pre-chargeback alert. The payload says whether the payment was refunded, and repeats the alert details.
payment.settledComing soon
A card payment has been settled. The externalRef ties this event back to the original tax calculation. Safe to grant access.
Not delivered yet. This event is documented so you can design for it, but Outpost does not send it today and the payload may still change. Do not make fulfillment depend on it.
e_invoice.issuedComing soon
An e-invoice (NFS-e) has been issued and filed with tax authorities. The payload includes a pre-signed URL to download the document.
Not delivered yet. This event is documented so you can design for it, but Outpost does not send it today and the payload may still change. Do not make fulfillment depend on it.
report.availableComing soon
A proceeds report has been finalized. The payload includes a pre-signed URL to download the report file.
Not delivered yet. This event is documented so you can design for it, but Outpost does not send it today and the payload may still change. Do not make fulfillment depend on it.
Common fields
Every event shares these top-level fields. Event-specific fields live inside data.
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string | Event identifier, in the form evt_<entityId> with an event-specific suffix. It is the same on every retry of the same event, so you can use it to deduplicate. | |
| type | string | Event type | |
| created | string | ISO 8601 timestamp the event was generated |