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

  1. Request an access_token with your client_id and api_token
  2. Read access_token and expires_in from the response
  3. Cache the token server-side with TTL expires_in - 300s
  4. Refresh on expiry or on a 401, then retry the request once
  5. Send Authorization: Bearer <access_token> on every API call

Get access token

POST/oauth2/token

Host: 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" | jq

Response

{
  "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/calculations

Creates 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

ParameterTypeRequiredDescription
Authorization-YesBearer token for authentication
Content-Type-Yesapplication/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

ParameterTypeRequiredDescription
currencystringYesISO 4217 currency code (e.g., EUR, USD, GBP, BRL). Brazil B2C NFS-e requires BRL.
taxBehaviorstring enumNoAllowed values: EXCLUSIVE, INCLUSIVE. Defaults to EXCLUSIVE. With INCLUSIVE, unitPrice is the gross amount and tax is extracted from it.
customerobjectYesCustomer information object
customer.merchantCustomerReferencestringNoYour internal customer identifier
customer.firstNamestringConditionalCustomer's first name. Required for Brazil B2C NFS-e.
customer.lastNamestringConditionalCustomer's last name. Required for Brazil B2C NFS-e.
customer.emailstringNoCustomer's email address
customer.taxIdentifiersarrayConditionalCustomer tax identifiers, discriminated on type. Required for Brazil NFS-e calculations.
customer.taxIdentifiers[].typestring enumConditionalIdentifier 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[].codestringConditionalIdentifier 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[].buyerTaxRegimestring enumConditionalBR_CNPJ only. Allowed values: SIMPLES_NACIONAL, LUCRO_PRESUMIDO, LUCRO_REAL. Required for Brazil B2B NFS-e calculations.
customer.billingAddressobjectYesCustomer's billing address
customer.billingAddress.line1stringYesPrimary address line
customer.billingAddress.line2stringNoSecondary address line
customer.billingAddress.citystringYesCity name
customer.billingAddress.statestringConditionalState/province code (required for US and CA)
customer.billingAddress.postalCodestringYesPostal/ZIP code
customer.billingAddress.countrystringYesISO 3166-1 alpha-2 country code
customer.shippingAddressobjectConditionalCustomer's shipping address. Required when any line item is PHYSICAL. The destination drives US sales-tax sourcing.
customer.shippingAddress.line1stringConditionalPrimary address line. Required when the shipping address is present and the country is outside the EU.
customer.shippingAddress.line2stringNoSecondary address line
customer.shippingAddress.citystringConditionalCity name. Required when the shipping address is present and the country is outside the EU.
customer.shippingAddress.statestringConditionalState/province code (required for US and CA)
customer.shippingAddress.postalCodestringConditionalPostal/ZIP code. Required when the shipping address is present and the country is outside the EU.
customer.shippingAddress.countrystringConditionalISO 3166-1 alpha-2 country code. Required whenever the shipping address is present.
lineItemsarrayYesArray of line items (minimum 1)
lineItems[].merchantLineItemReferencestringYesYour internal line item identifier
lineItems[].productCodestringYesProduct/service code for tax classification
lineItems[].descriptionstringYesHuman-readable item description
lineItems[].unitPricedecimalYesPrice per unit (must be ≥ 0)
lineItems[].quantityintegerNoQuantity (default: 1, must be > 0)
lineItems[].discountAmountdecimalNoDiscount amount per unit. For quantity 3 and discountAmount 2.00, the total discount is 6.00; applied before tax calculation.
lineItems[].productTypestring enumNoAllowed values: PHYSICAL, DIGITAL. Defaults to DIGITAL. Physical goods are priced with import duty on the cross-border path.
lineItems[].countryOfOriginstringNoISO 3166-1 alpha-2 country of manufacture. Requires productType PHYSICAL. Needed per line on the cross-border duty path.
lineItems[].hsCodestringNoDestination-qualified HS code used to classify the line for duty. Requires productType PHYSICAL and must not be blank when present.
shipFromobjectNoShip-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.line1stringNoPrimary address line
shipFrom.line2stringNoSecondary address line
shipFrom.citystringNoCity name
shipFrom.statestringNoState/province code
shipFrom.postalCodestringNoPostal/ZIP code
shipFrom.countrystringConditionalISO 3166-1 alpha-2 country code. Required whenever shipFrom is present.
evidenceobjectConditionalTax evidence (required for non-US/non-BR billing addresses; optional for Brazil)
evidence.billingCountrystringConditionalISO 3166-1 alpha-2 billing country code. Required for non-US/non-BR evidence; if provided for Brazil, must be BR.
evidence.ipAddressstringConditionalCustomer's IP address. Required for non-US/non-BR billing addresses; optional for Brazil.
evidence.paymentMethodCountrystringNoPayment method country code
Note: For non-US/non-BR billing addresses, 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.
Brazil B2C NFS-e: use 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

ParameterTypeRequiredDescription
taxCalculationIdstring-Unique identifier for the calculation
createdAtstring-ISO 8601 timestamp when calculation was created
expiresAtstring-ISO 8601 timestamp when calculation expires
currencystring-Currency code
subTotalAmountdecimal-Sum of line item amounts before discounts
discountAmountdecimal-Total discount amount applied
netAmountdecimal-Amount after discounts, before tax
taxAmountdecimal-Total tax amount
totalAmountdecimal-Final amount including tax
customerobject-Customer information
totalDutyAmountdecimal-Total import duty across all line items. Omitted when no line carries duty.
lineItemsarray-Calculated line items with tax details
lineItems[].tax.ratedecimal-Effective tax rate applied to the line
lineItems[].tax.taxAmountdecimal-Tax amount for the line
lineItems[].tax.dutyAmountdecimal-Import duty for the line. Omitted when the line carries no duty.
lineItems[].tax.dutyRatedecimal-Duty rate applied to the line
lineItems[].tax.dutyBasisstring enum-How the duty rate was derived: CLASSIFIED (from the HS code) or FALLBACK_ESTIMATE.
lineItems[].tax.appliedHsCodestring-HS code the line was actually classified with
lineItems[].tax.chargesarray-Jurisdiction-level breakdown. Always present. See the charges note below before using it.
evidenceobject-Tax evidence used for calculation
entityobject-The Outpost legal entity acting as the recording seller (present when Tax of Record / liability transfer applies).
entity.companyNamestring-Legal company name of the recording entity.
Amount and rate fields in responses are returned as decimal strings to preserve precision. Request examples may send decimal values as JSON numbers.
Fields with no value are omitted from the response rather than returned as null. Check whether a key is present, not whether it equals null.

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.

ParameterTypeRequiredDescription
kindstring enum-TAX, IMPORT_DUTY, or IMPORT_FEE
namestring-Charge name
jurisdictionstring-Jurisdiction the charge belongs to
jurisdictionLevelstring enum-COUNTRY, STATE, COUNTY, CITY, or DISTRICT
countrystring-ISO 3166-1 alpha-2 country code
taxTypestring enum-VAT, GST, SALES_TAX, IMPORT_VAT, or OTHER
providerTaxTypestring-The tax provider's own type label, passed through unchanged
ratedecimal-Rate for this charge
amountdecimal-Amount for this charge
taxableAmountdecimal-Amount this charge was calculated on
Use 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

HTTPCodeDescription
400invalid_argumentMissing 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).
400missing_country_of_originA physical line item on a cross-border order has no countryOfOrigin. The field names the line index.
400shipping_address_requiredThe cart contains a PHYSICAL line item but no customer.shippingAddress
400invalid_statePlease activate Outpost Taxes / Tax of Record to process transactions
409region_not_activatedMerchant not activated for the customer's tax jurisdiction
409region_not_supportedCustomer location outside supported jurisdictions
409physical_goods_not_supportedPhysical goods cannot be priced for that destination yet
409integration_credentials_invalidThe tax provider rejected the configured integration credentials
409integration_not_configuredTax compliance integration is not configured
422nfse_validation_failedBrazil NFS-e pre-payment validation rejected the customer data. Each reason is returned on the field "customer".
502tax_provider_unavailableThe 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 step

Create Tax Transaction

POST/api/tax/transactions

Converts 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.

On create, Outpost validates structure and required fields synchronously and rejects malformed requests immediately. Authority clearance (signed submission / e-invoice authorization) is asynchronous. Retrieve invoices and e-invoice documents from Invoicing, and explicit pending/cleared/rejected status and webhooks are on the roadmap.

Request Headers

ParameterTypeRequiredDescription
Authorization-YesBearer token for authentication
Content-Type-Yesapplication/json

Request Body

{
  "taxCalculationId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "merchantTransactionReference": "ORDER-2026-001234",
  "payment": {
    "processor": "stripe",
    "paymentReference": "ch_3Q1234567890abcdef"
  }
}

Request Body Parameters

ParameterTypeRequiredDescription
taxCalculationIdstringYesID of the tax calculation generated by Outpost to convert
merchantTransactionReferencestringYesYour unique order/transaction reference
paymentobjectNoPayment processor information
payment.processorstringNoPayment processor name (e.g., "stripe", "adyen")
payment.paymentReferencestringNoPayment 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

HTTPCodeDescription
400invalid_argumentMissing or invalid field - check field requirements. taxCalculationId must be a valid UUID.
400invalid_stateTax calculation has expired. Returned on the field taxCalculationId - create a new calculation.
400invalid_stateTax calculation has already been used. Returned on the field taxCalculationId.
400invalid_stateTransaction with that merchantTransactionReference already exists. Returned on the field merchantTransactionReference - see Idempotency below.
400invalid_statePlease activate Outpost Taxes / Tax of Record to process transactions. Returned on the field status.
400invalid_stateThe 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.
400invalid_stateMoR product is already enabled in this region
404not_foundTax 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 TaxTransactionResponse

Get Tax Transaction

GET/api/tax/transactions/{transaction_id}

Retrieves the details of an existing tax transaction.

Path Parameters

ParameterTypeRequiredDescription
transaction_idstringYesID of the transaction

Response 200 OK

Returns a TaxTransactionResponse object (same structure as Create Tax Transaction response).

Error Responses

HTTPCodeDescription
400invalid_argumenttransaction_id is not a valid transaction id
404not_foundTransaction not found

Get Transaction Refunds

GET/api/tax/transactions/{transaction_id}/refunds

Retrieves all refunds associated with a transaction.

Path Parameters

ParameterTypeRequiredDescription
transaction_idstringYesID 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

HTTPCodeDescription
400invalid_argumenttransaction_id is not a valid transaction id
404not_foundTransaction not found

Create Refund Transaction

POST/api/tax/transactions/{transaction_id}/refunds

Creates a refund for one or more line items in an existing transaction.

Path Parameters

ParameterTypeRequiredDescription
transaction_idstringYesID 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

ParameterTypeRequiredDescription
merchantRefundReferencestringYesYour unique refund reference
refundDatestringNoISO 8601 date/time of the refund
refundLineItemsarrayYesArray of line items to refund (minimum 1)
refundLineItems[].lineItemIdstringYesID of the original line item
refundLineItems[].refundLineItemReferencestringYesYour unique reference for this refund item
refundLineItems[].refundReasonstringYesReason 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

HTTPCodeDescription
400invalid_argumentMissing or invalid field. refundDate must match yyyy-MM-ddTHH:mm:ssZ and cannot be later than today.
400invalid_stateRefund with that merchantRefundReference already exists. Returned on the field merchantRefundReference.
404not_foundTransaction or line items not found

Import Catalogue Product

POST/api/catalogue/products

Imports 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

ParameterTypeRequiredDescription
Authorization-YesBearer token for authentication
Content-Type-Yesapplication/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

ParameterTypeRequiredDescription
productIdstringYesUnique identifier for this variant (the sellable unit). Used as the variant key.
productGroupIdstringNoParent product identifier. Share across variants to group them under one product. Defaults to productId (standalone) when omitted.
titlestringYesProduct title. Product-level - keep identical across variants of a group.
requiresShippingbooleanYesWhether the item ships physically.
taxablebooleanNoWhether the item is taxable (default: true).
descriptionstringNoProduct description. Product-level.
categorystringNoProduct category / type. Product-level.
imageUrlstringNoProduct image URL. Product-level.
skustringNoStock keeping unit. Variant-level.
hsCodestringNoHarmonized System code for customs. Variant-level.
countryOfOriginstringNoISO 3166-1 alpha-2 country of origin. Variant-level.
priceAmountdecimalNoUnit price (default: 0). Variant-level.
priceCurrencystringNoISO 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

ParameterTypeRequiredDescription
productIdstring-The variant identifier you supplied.
productGroupIdstring | null-Parent product id. null when the product is its own group (standalone).
titlestring-Product title.
requiresShippingboolean-Whether the item ships physically.
statusstring-Classification status: IMPORTED, EXTRACTED, READY_FOR_CLASSIFICATION, APPROVED, or ARCHIVED. A fresh import returns IMPORTED.
approvedTaxCodestring | null-Operator-approved tax code. null until a tax code is approved.
predictedTaxCodestring | 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 returns productGroupId as null. 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 distinct productId. Every call attaches one variant to the same parent product - for example group TSHIRT-001 with variants TSHIRT-001-S, TSHIRT-001-M and TSHIRT-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 title or description while the variant is APPROVED clears the approval and returns the variant to READY_FOR_CLASSIFICATION for re-review.
  • Changing only priceAmount, hsCode or countryOfOrigin updates the variant silently and keeps any approval.
  • Importing a new productId under an existing productGroupId adds 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.

Note: catalogue tax codes are operator-facing today. Tax calculations do not automatically resolve a stored approvedTaxCode; tax is still determined per line item at calculation time.

Error Responses

HTTPCodeDescription
400validation_errorMissing 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 product

Resource

Proforma Invoices

Issue a proforma invoice for a B2B customer paying by bank transfer.

Create Proforma Invoice

POST/api/proforma-invoices

Generates 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

ParameterTypeRequiredDescription
Authorization-YesBearer token for authentication
Content-Type-Yesapplication/json

Request Body Parameters

ParameterTypeRequiredDescription
jurisdictionstringYesTax jurisdiction the invoice is issued under (ISO 3166-1 alpha-2). Determines numbering, format, and legal text.
totalobjectYesInvoice total
total.amountstringYesTotal amount as a decimal string (e.g., "1111.00")
total.currencystringYesISO 4217 currency code (e.g., GBP, EUR, USD)
recipientobjectYesThe B2B customer being invoiced
recipient.legalNamestringYesRegistered legal name of the customer
recipient.addressobjectYesCustomer billing address
recipient.address.line1stringYesPrimary address line
recipient.address.line2stringNoSecondary address line
recipient.address.localitystringYesCity or locality
recipient.address.regionstringYesState, province, or region
recipient.address.postalCodestringYesPostal or ZIP code
recipient.address.countrystringYesISO 3166-1 alpha-2 country code
recipient.taxIdentifiersarrayYesCustomer tax identifiers (e.g., CNPJ, VAT)
recipient.taxIdentifiers[].typestringYesIdentifier type (e.g., VAT, EIN)
recipient.taxIdentifiers[].codestringYesThe identifier value
recipient.taxIdentifiers[].countrystringYesIssuing country (ISO 3166-1 alpha-2)
recipient.contactobjectYesCustomer contact details
recipient.contact.emailstringYesEmail address for invoice delivery
recipient.buyerTaxRegimestringConditionalBrazilian tax regime of the customer: SIMPLES_NACIONAL, LUCRO_PRESUMIDO, or LUCRO_REAL. Required when the customer has a CNPJ tax identifier.
providerobjectYesThe entity supplying the goods or services
provider.providerNamestringYesLegal name of the supplying entity
lineItemsarrayYesOne or more invoice line items
lineItems[].descriptionstringYesDescription of the item
lineItems[].quantitystringYesQuantity as a decimal string
lineItems[].unitPriceobjectYesPrice per unit
lineItems[].unitPrice.amountstringYesUnit price as a decimal string
lineItems[].unitPrice.currencystringYesISO 4217 currency code
lineItems[].classificationsarrayNoTax or product classification codes for the item
issueDatestringYesIssue date (ISO 8601 date, YYYY-MM-DD)
dueDatestringYesPayment due date (ISO 8601 date, YYYY-MM-DD)
noticesarrayNoFree-text notices to print on the invoice (e.g., payment instructions)
merchantReferencestringNoYour 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

ParameterTypeRequiredDescription
proformaInvoiceIdstring (UUID)-Outpost identifier for the proforma invoice. Use it to reconcile settlement webhooks.
statusstring-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.
invoiceobject-Generated invoice file information
invoice.fileNamestring-Name of the generated proforma invoice PDF
invoice.urlstring-Pre-signed link to the proforma invoice PDF. Show it on your checkout or email it to the customer.
invoice.expiresAtstring-ISO 8601 timestamp when the download link expires (15 minutes). Re-fetch the invoice to mint a fresh link.
totalobject-Echoed invoice total
issueDatestring-Echoed issue date
dueDatestring-Echoed due date
createdAtstring-ISO 8601 creation timestamp
merchantReferencestring-Your reference, echoed back from the request. Null when you did not send one.

Error Responses

HTTPCodeDescription
400invalid_argumentMissing or invalid required field (e.g., jurisdiction, recipient, or lineItems)
401-Invalid or missing Authorization token
422unsupported_jurisdictionProforma invoices are not yet available for the requested jurisdiction
422no_payment_destinationNo settlement account is configured for the invoice currency, so there is nowhere for the customer to pay
422nfse_validation_failedThe 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 PDF

Resource

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

GET/api/payments/{paymentId}/invoice

Retrieves the B2C tax invoice for a payment using the Outpost payment ID.

Request Headers

ParameterTypeRequiredDescription
Authorization-YesBearer token for authentication

Path Parameters

ParameterTypeRequiredDescription
paymentIdstring (UUID)YesOutpost payment identifier

Response 200 OK

{
  "invoice": {
    "fileName": "invoice-7110000000023059375.pdf",
    "url": "https://storage.outpostanywhere.com/invoices/...",
    "expiresAt": "2026-01-05T12:15:00Z"
  }
}

Response Fields

ParameterTypeRequiredDescription
invoiceobject-Invoice file information
invoice.fileNamestring-Name of the invoice PDF file
invoice.urlstring-Pre-signed URL to download the invoice
invoice.expiresAtstring-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

HTTPCodeDescription
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 generated

Get Payment Invoice by PSP Reference

GET/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

ParameterTypeRequiredDescription
Authorization-YesBearer token for authentication

Query Parameters

ParameterTypeRequiredDescription
psp_referencestringYesPayment 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

HTTPCodeDescription
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" | jq

Get Refund Invoice by ID

GET/api/refunds/{refundId}/invoice

Retrieves the B2C tax invoice for a refund using the Outpost refund ID.

Request Headers

ParameterTypeRequiredDescription
Authorization-YesBearer token for authentication

Path Parameters

ParameterTypeRequiredDescription
refundIdstring (UUID)YesOutpost 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

HTTPCodeDescription
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" | jq

Get Refund Invoice by PSP Reference

GET/api/refunds/invoice?psp_reference={psp_reference}

Retrieves the B2C tax invoice for a refund using the PSP reference.

Request Headers

ParameterTypeRequiredDescription
Authorization-YesBearer token for authentication

Query Parameters

ParameterTypeRequiredDescription
psp_referencestringYesRefund 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

HTTPCodeDescription
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" | jq

Get Transaction Invoice

GET/api/tax/transactions/{transaction_id}/invoice

Retrieves the invoice file for a transaction.

Path Parameters

ParameterTypeRequiredDescription
transaction_idstringYesID of the transaction

Response 200 OK

{
  "b2cInvoice": {
    "fileName": "invoice-e83a9c47-2b5d-4f8a-9c12-3d4e5f6a7b8c.pdf",
    "url": "https://storage.outpostnow.com/invoices/..."
  }
}

Response Fields

ParameterTypeRequiredDescription
b2cInvoiceobject-B2C invoice information
b2cInvoice.fileNamestring-Name of the invoice file
b2cInvoice.urlstring-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

HTTPCodeDescription
400invalid_argumenttransaction_id is not a valid transaction id
404not_foundTransaction 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

POST/api/payments/stripe/setup-intentsStripe

Provisions 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

ParameterTypeRequiredDescription
AuthorizationstringYesBearer access token - see Authentication above.

Request Body

application/json

{
  "payment_method_types": ["card"],
  "tax_calculation_id": "taxc_a1b2c3d4e5f6789"
}

Request Fields

ParameterTypeRequiredDescription
payment_method_typesstring[]NoArray 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_idstringNoReference 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

ParameterTypeRequiredDescription
client_secretstring-Pass to Stripe Elements via loadStripe(OUTPOST_PUBLISHABLE_KEY) and confirmSetup({ clientSecret, ... }).
setup_intent_idstring-SetupIntent ID on Outpost’s PSP account.
mor_customer_idstring-Outpost’s Stripe Customer ID (empty Customer with metadata pointing back at your merchant_customer.stripe_id). Persist for future reference.
statusstring-One of requires_payment_method, requires_confirmation, requires_action, processing, succeeded.

Error Responses

HTTPCodeDescription
400invalid_requestMalformed body, or invalid value in payment_method_types.
401-Missing or invalid Bearer token.
403merchant_suspendedMerchant account is suspended, or the token lacks the required scope.
404tax_calculation_not_foundThe tax_calculation_id does not resolve to a known calculation.
409idempotency_conflictSame Idempotency-Key replayed with a different request body.
422validation_errorA payment_method_types value is not enabled for your account on Outpost’s PSP.
429rate_limitedBurst exceeded your quota. Retry after backoff.
503psp_unavailableUpstream PSP (Stripe) returned 5xx or timed out. Safe to retry with the same Idempotency-Key.

Create Payment Intent

POST/api/payments/stripe/payment-intentsStripe

Used 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

ParameterTypeRequiredDescription
AuthorizationstringYesBearer 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

ParameterTypeRequiredDescription
amountintegerYesSmallest currency unit (e.g. cents for USD, centavos for BRL). Minimum 1.
currencystring (ISO 4217)YesThree-letter currency code, e.g. BRL.
merchant_customer.stripe_idstringYesCustomer ID on your own Stripe account. Stored as metadata on the empty MoR-side Customer.
merchant_customer.emailstringNoCustomer email. Used for receipt routing and fraud signals.
merchant_customer.namestringNoCustomer name.
merchant_customer.countrystring (ISO 3166-1 alpha-2)NoCustomer country code.
merchant_customer.addressobjectNoBilling address (line1, city, postal_code, etc.).
descriptionstringNoFree-form description shown on the underlying PaymentIntent. Max 255 chars.
tax_calculation_idstringNoReference 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

ParameterTypeRequiredDescription
client_secretstring-Pass to Stripe Elements via loadStripe(OUTPOST_PUBLISHABLE_KEY) and confirm with stripe.confirmPayment({ clientSecret, … }).
payment_intent_idstring-PaymentIntent ID on Outpost’s PSP account.
mor_customer_idstring-Outpost’s Stripe Customer ID (empty Customer with metadata pointing back at your merchant_customer.stripe_id). Persist for future reference.
statusstring-PaymentIntent status - typically requires_payment_method at this point, and the browser will transition it via confirmPayment().

Error Responses

HTTPCodeDescription
400invalid_requestMissing or malformed amount / currency / merchant_customer.stripe_id.
401-Missing or invalid Bearer token.
403merchant_suspendedMerchant account is suspended, or the token lacks the required scope.
404tax_calculation_not_foundThe tax_calculation_id does not resolve to a known calculation.
409idempotency_conflictSame Idempotency-Key replayed with a different request body.
422unsupported_currency / amount_out_of_rangeCurrency not enabled on Outpost’s PSP, amount below the PSP’s minimum, or amount above your per-transaction cap.
429rate_limitedBurst exceeded your quota. Retry after backoff.
503psp_unavailableUpstream 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

POST/api/payments/{paymentId}/refund

Refunds 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

ParameterTypeRequiredDescription
paymentIdstringYesThe Outpost payment_id from the Confirm Payment response.

Request Headers

ParameterTypeRequiredDescription
AuthorizationstringYesBearer access token.
Idempotency-KeystringYesRequired 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

ParameterTypeRequiredDescription
amountintegerNoSmallest currency unit. Defaults to the unrefunded remainder of the payment, i.e. a full refund. Must be ≤ remaining refundable amount.
reasonstringNoOne of requested_by_customer, duplicate, fraudulent. Free-form strings are also accepted and forwarded to the PSP.
metadataobjectNoArbitrary 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

ParameterTypeRequiredDescription
idstring-Outpost-owned refund identifier (rfnd_…).
payment_idstring-The payment this refund applies to.
amountinteger-Refunded amount in smallest currency unit.
currencystring-Echoes the payment currency.
statusstring-One of succeeded, pending, failed. For cards and Link this is synchronous - for bank debits the refund starts as pending and transitions via webhook.
reasonstring | null-Echoes the request reason.
processor_references.refund_idstring-Refund ID on Outpost’s PSP account (e.g. re_… on Stripe).
created_atstring (ISO 8601)-When the refund was created.

Error Responses

HTTPCodeDescription
400invalid_requestamount exceeds the unrefunded remainder, or currency mismatch.
401-Missing or invalid Bearer token.
404payment_not_foundNo payment exists for the given paymentId.
422payment_not_refundablePayment 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.*

WEBHOOKPOST <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

ParameterTypeRequiredDescription
Mor-SignaturestringYest=<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

ParameterTypeRequiredDescription
idstring-Outpost event ID. Use to dedupe.
typestring-One of payment.succeeded, payment.failed, payment.requires_action, payment.refunded.
createdinteger-Unix seconds.
data.object.payment_idstring-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_idstring-Stripe Invoice ID on your account that triggered this renewal. Pass to attach_payment.
data.object.merchant_customer_idstring-Customer ID on your Stripe account.
data.object.processor_charge_idstring-Charge ID on Outpost’s PSP account. Use as payment_reference on report_payment. Empty on failed / requires_action.
data.object.processor_payment_intent_idstring-PaymentIntent ID on Outpost’s PSP account.
data.object.amountinteger-For payment.succeeded / failed / requires_action: the payment amount. For payment.refunded: the amount refunded by this event.
data.object.currencystring-Three-letter currency code.
data.object.statusstring-One of succeeded, failed, requires_action, partially_refunded, refunded.
data.object.failure_messagestringNoHuman-readable decline reason (present on failed / requires_action).
data.object.decline_codestringNoPSP decline code, e.g. insufficient_funds.
data.object.refund_idstringNoOutpost-owned refund identifier. Only on payment.refunded.
data.object.processor_refund_idstringNoRefund ID on Outpost’s PSP account. Only on payment.refunded.
data.object.amount_refunded_totalintegerNoCumulative amount refunded across all refunds on this payment. Only on payment.refunded.
data.object.reasonstringNoRefund 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

HTTPCodeDescription
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

POST/api/webhooks

Registers an endpoint to receive event notifications. The response includes a secret that is shown only once - store it to verify incoming signatures.

Request Headers

ParameterTypeRequiredDescription
Authorization-YesBearer token for authentication
Content-Type-Yesapplication/json

Request Body Parameters

ParameterTypeRequiredDescription
urlstringYesHTTPS endpoint that will receive event POSTs. Must resolve to a public address.
eventsstring[]YesEvent types to subscribe to. Must contain at least one of the event types listed below.
descriptionstringNoHuman-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

ParameterTypeRequiredDescription
idstringWebhook endpoint identifier. Use it to list or delete the endpoint.
urlstringThe registered destination URL
eventsstring[]Subscribed event types
secretstringSigning secret. Returned once on creation - store it securely to verify the Outpost-Signature header.
statusstringACTIVE or DISABLED
createdAtstringISO 8601 creation timestamp

Error Responses

HTTPCodeDescription
400unknown_eventevents is empty or contains an event type that does not exist
400invalid_urlurl is not a valid HTTPS URI, or its host cannot be resolved or is not public
401-Invalid or missing Authorization token
409webhook_endpoint_limit_exceededYou 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 signatures

List Webhooks

GET/api/webhooks

Returns 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" | jq

Delete Webhook

DELETE/api/webhooks/{webhookId}

Removes a webhook endpoint. It immediately stops receiving events.

Path Parameters

ParameterTypeRequiredDescription
webhookIdstringIdentifier returned when the webhook was registered

Response 204 No Content - The webhook was deleted.

Error Responses

HTTPCodeDescription
401-Invalid or missing Authorization token
404not_foundNo 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 Content

Webhook 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.settled

The bank transfer for a proforma invoice has been received and reconciled. Safe to fulfill the order.

dispute.action_needed

A 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.outcome

A dispute has been resolved. The payload carries the final outcome.

pre_chargeback_alert.received

A 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.outcome

Outpost finished handling a pre-chargeback alert. The payload says whether the payment was refunded, and repeats the alert details.

payment.settled

Coming 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.issued

Coming 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.available

Coming 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.

ParameterTypeRequiredDescription
idstringEvent 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.
typestringEvent type
createdstringISO 8601 timestamp the event was generated