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 Parameters

One endpoint, two product cases: the request shape below covers both digital goods and physical goods with cross-border import duty. Declare the case per line with productType. Worked examples for each case follow the parameter table.

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[].titlestringConditionalProduct title - the primary classification text. On the cross-border duty path (a PHYSICAL line shipping from a country that differs from the destination) at least one of title or description is required.
lineItems[].descriptionstringConditionalDetailed product description. Improves HS code prediction together with title, and is printed on fiscal documents (falls back to title when absent). On the cross-border duty path at least one of title or description is required.
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 enumRecommendedAllowed values: PHYSICAL, DIGITAL. Pass it explicitly on every line. Lines without productType are treated as DIGITAL for backward compatibility - do not rely on that. Drives tax engine selection: physical goods are priced with import duty on the cross-border path.
lineItems[].countryOfOriginstringConditionalISO 3166-1 alpha-2 country of manufacture. Requires productType PHYSICAL. Required per PHYSICAL line on the cross-border duty path; digital lines in a mixed cart do not need it.
lineItems[].hsCodestringNoDestination-qualified full HS code to classify the line with: 6-14 digits, dot separators allowed (normalized to digits). Requires productType PHYSICAL. When absent, Outpost predicts the code from title, description, and category.
lineItems[].categorystringNoFree-text product category (e.g. "Watches", "Trading Cards"). Improves HS code prediction accuracy when hsCode is not provided; ignored otherwise.
lineItems[].imagesarrayNoPublic product image URLs. Stored with the product classification for future model training; not sent to the classification engine.
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.

Two product cases - digital and physical goods. Pass productType explicitly on every line: DIGITAL for digital products and services, PHYSICAL for goods that ship. Any PHYSICAL line requires customer.shippingAddress (physical-goods tax is destination-based).

Physical goods classification: pass as much product data as you can - title, description, category, images, countryOfOrigin. The richer the product text, the more accurate the predicted destination-qualified HS code - and therefore the duty and tax rates. If you already know the full HS code for the country you ship to, pass it as hsCode to skip prediction and optimize classification time; duty is then priced directly from your code.

Adding shipFrom with an origin country that differs from the destination puts the cart on the cross-border duty path: each PHYSICAL line then needs countryOfOrigin and at least one of title or description, and the response prices import duty per line. Mixed carts are allowed - digital lines carry no duty and skip these requirements. Physical goods to the US cannot be priced yet (409 physical_goods_not_supported).

Digital Goods Example

For digital goods pass productType: DIGITAL per line. No shipping address, no duty fields; tax is quoted from the customer's location and evidence. A Dutch consumer buying an annual subscription: 21% VAT on the discounted net amount.

{
  "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",
      "productType": "DIGITAL",
      "unitPrice": 99.99,
      "quantity": 1,
      "discountAmount": 10.00
    }
  ],
  "evidence": {
    "billingCountry": "NL",
    "ipAddress": "185.23.108.42",
    "paymentMethodCountry": "NL"
  }
}

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",
      "productType": "DIGITAL",
      "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"
    }
  ]
}

Physical Goods Example (Cross-Border Duty)

A Japanese wristwatch shipped to a customer in the Netherlands. The cart is on the cross-border duty path: shipFrom (JP) differs from the shipping destination (NL) and the line is PHYSICAL. The line carries no hsCode, so Outpost classifies the product from its title, description, and category and prices duty from the resulting destination-qualified HS code (dutyBasis CLASSIFIED).

Duty (4.5% of the €200.00 net amount = €9.00) is added to the customs value before VAT, so the 21% Dutch VAT applies to goods + duty (21% of €209.00 = €43.89). itemTotalAmount and totalAmount include tax and duty (€252.89); totalDutyAmount reports duty separately and is never folded into taxAmount.

{
  "currency": "EUR",
  "taxBehavior": "EXCLUSIVE",
  "customer": {
    "merchantCustomerReference": "CUST-88712",
    "email": "anna.devries@example.com",
    "billingAddress": {
      "line1": "Prinsengracht 263",
      "city": "Amsterdam",
      "postalCode": "1016 GV",
      "country": "NL"
    },
    "shippingAddress": {
      "line1": "Prinsengracht 263",
      "city": "Amsterdam",
      "postalCode": "1016 GV",
      "country": "NL"
    }
  },
  "shipFrom": {
    "line1": "2-11-3 Meguro",
    "city": "Tokyo",
    "postalCode": "153-0063",
    "country": "JP"
  },
  "lineItems": [
    {
      "merchantLineItemReference": "ITEM-JP-001",
      "productCode": "WATCH-SKU-210",
      "title": "Automatic wristwatch",
      "description": "Stainless steel automatic wristwatch, made in Japan",
      "category": "Watches",
      "images": ["https://cdn.example.com/products/watch-210.jpg"],
      "productType": "PHYSICAL",
      "countryOfOrigin": "JP",
      "unitPrice": 200.00,
      "quantity": 1
    }
  ],
  "evidence": {
    "billingCountry": "NL",
    "ipAddress": "85.146.21.7",
    "paymentMethodCountry": "NL"
  }
}

Response 201 Created:

{
  "taxCalculationId": "b3d9f2a1-4c8e-4b6f-9a21-7e5d0c3f8a42",
  "createdAt": "2026-01-05T12:00:00Z",
  "expiresAt": "2026-01-12T12:00:00Z",
  "currency": "EUR",
  "subTotalAmount": "200.00",
  "discountAmount": "0.00",
  "netAmount": "200.00",
  "taxAmount": "43.89",
  "totalAmount": "252.89",
  "totalDutyAmount": "9.00",
  "customer": {
    "email": "anna.devries@example.com",
    "billingAddress": {
      "line1": "Prinsengracht 263",
      "city": "Amsterdam",
      "postalCode": "1016 GV",
      "country": "NL"
    },
    "shippingAddress": {
      "line1": "Prinsengracht 263",
      "city": "Amsterdam",
      "postalCode": "1016 GV",
      "country": "NL"
    }
  },
  "lineItems": [
    {
      "lineItemId": "6f1c9a3e-8d24-47b5-b1f0-2a9c5e7d4b83",
      "merchantLineItemReference": "ITEM-JP-001",
      "productCode": "WATCH-SKU-210",
      "title": "Automatic wristwatch",
      "description": "Stainless steel automatic wristwatch, made in Japan",
      "images": ["https://cdn.example.com/products/watch-210.jpg"],
      "unitPrice": "200.00",
      "quantity": 1,
      "discountAmount": "0.00",
      "itemSubTotalAmount": "200.00",
      "itemNetAmount": "200.00",
      "itemTotalAmount": "252.89",
      "tax": {
        "rate": "21.00",
        "taxAmount": "43.89",
        "currency": "EUR",
        "dutyAmount": "9.00",
        "dutyRate": "4.50",
        "dutyBasis": "CLASSIFIED",
        "appliedHsCode": "9102110000",
        "charges": [
          {
            "kind": "TAX",
            "name": "Standard Rate",
            "jurisdiction": "Netherlands",
            "jurisdictionLevel": "COUNTRY",
            "country": "NL",
            "taxType": "IMPORT_VAT",
            "providerTaxType": "Output",
            "rate": "21.00",
            "amount": "43.89",
            "taxableAmount": "209.00"
          },
          {
            "kind": "IMPORT_DUTY",
            "name": "Import Duty",
            "country": "NL",
            "providerTaxType": "ImportDuty",
            "rate": "4.50",
            "amount": "9.00",
            "taxableAmount": "200.00"
          }
        ]
      },
      "refunded": false
    }
  ],
  "evidence": {
    "billingCountry": "NL",
    "ipAddress": "85.146.21.7",
    "paymentMethodCountry": "NL"
  },
  "entity": {
    "companyName": "Outpost Commerce B.V."
  }
}

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 was priced: CLASSIFIED (from a destination-qualified HS code, supplied or predicted) or FALLBACK_ESTIMATE (a conservative per-destination rate used when the line could not be classified).
lineItems[].tax.appliedHsCodestring-Destination-qualified HS code the line was actually priced with. Omitted on the FALLBACK_ESTIMATE tier.
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 display name as reported by the provider (e.g. "CA STATE TAX", "Standard Rate", "Import Duty")
jurisdictionstring-Jurisdiction the charge belongs to. Omitted for duty and fees.
jurisdictionLevelstring enum-COUNTRY, STATE, COUNTY, CITY, or DISTRICT. Omitted for duty and fees.
countrystring-ISO 3166-1 alpha-2 country code
taxTypestring enum-Normalized tax type: VAT, GST, SALES_TAX, IMPORT_VAT, or OTHER. Omitted for duty and fees.
providerTaxTypestring-The tax provider's own type label (e.g. "Output", "ImportDuty", "MPF"), 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.
400missing_classification_textA physical line item on a cross-border order has neither title nor description - at least one is required to classify the product. 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",
        "productType": "DIGITAL",
        "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.

Idempotent by merchantTransactionReference: retrying with the same taxCalculationId and reference returns the already-created transaction with 200 - safe to resend after a timeout or lost response, and no duplicate is recorded. Reusing the reference with a different calculation returns 400 invalid_state.

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. Also the idempotency key: retrying with the same reference and taxCalculationId returns the existing transaction with 200; reusing it with a different calculation returns 400.
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 - under a different merchantTransactionReference. Returned on the field taxCalculationId. An identical retry (same calculation, same reference) returns 200 with the existing transaction instead.
400invalid_stateTransaction with that merchantTransactionReference already exists - created from a different taxCalculationId. Returned on the field merchantTransactionReference. An identical retry returns 200 with the existing transaction instead - see Idempotency.
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

List Tax Transactions

GET/api/tax/transactions

Lists the tax transactions committed on your account, newest first. Results are cursor-paginated, and can be narrowed to a single order with merchantTransactionReference.

Request Headers

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

Query Parameters

ParameterTypeRequiredDescription
merchantTransactionReferencestringNoFilter by the merchant transaction reference the transactions were created with. Omit to list every transaction.
sizeintegerNoPage size, between 1 and 100. Defaults to 20.
cursorstringNoCursor returned as nextCursor by the previous call. Omit it to fetch the first page.

Response 200 OK

{
  "transactions": [
    {
      "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",
          "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"
      }
    }
  ],
  "nextCursor": "MjAyNi0wMS0wNVQxMjowNTowMFo6ZTgzYTljNDc"
}

Response Fields

ParameterTypeRequiredDescription
transactionsarray-Transactions on this page. Each entry is a full TaxTransactionResponse - the same object Create Tax Transaction and Get Tax Transaction return.
transactions[].transactionIdstring-Outpost identifier of the tax transaction
transactions[].merchantTransactionReferencestring-Your reference for the order the transaction was committed under
transactions[].refundedboolean-Whether the transaction has been refunded
transactions[].createdAtstring-ISO 8601 timestamp when the transaction was created
transactions[].confirmedAtstring-ISO 8601 timestamp when the transaction was confirmed
transactions[].transactionDatestring-ISO 8601 date the sale is booked on
transactions[].currencystring-ISO 4217 currency code
transactions[].subTotalAmountdecimal-Sum of line item amounts before discounts
transactions[].discountAmountdecimal-Total discount applied across line items
transactions[].netAmountdecimal-Taxable base after discounts
transactions[].taxAmountdecimal-Total tax on the transaction
transactions[].totalAmountdecimal-Net amount plus tax
transactions[].customerobject-Customer, addresses and tax identifiers recorded on the transaction
transactions[].paymentobject-Processor and payment reference supplied when the transaction was created
transactions[].lineItemsarray-Line items with their per-line tax breakdown
transactions[].refundsOnTransactionarray-Line items refunded against this transaction. Empty when nothing has been refunded.
transactions[].evidenceobject-Location evidence used to source the sale
nextCursorstring-Cursor for the next page. Pass it back as cursor. Omitted on the last page - stop paging when it is absent.
Page through by passing the nextCursor of one response as the cursor of the next request, keeping every other parameter identical. The last page omits nextCursor - do not page on a count of returned rows, a full page can still be the last one.

Error Responses

HTTPCodeDescription
400invalid_argumentsize is outside 1-100, or cursor is not a cursor issued by a previous call

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.

Each refund line refunds the full remaining amount of the original line by default. To refund only part of a line, set amount on that line - a line may be refunded several times, cumulatively up to its original total. The request-level taxBehavior decides how those amounts are read: EXCLUSIVE (the default) treats them as net of tax, INCLUSIVE as gross including tax. It is ignored for lines without an amount.

Idempotent by merchantRefundReference: retrying with the same reference, transaction, and refund line items returns the already-created refund with 200. Reusing the reference against a different transaction, or with different line items or amounts, returns 400 invalid_state.

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. Also the idempotency key: an identical retry (same transaction and refund line items) returns the existing refund with 200; reusing it with different content returns 400.
refundDatestringNoISO 8601 date/time of the refund
taxBehaviorstring enumNoAllowed values: EXCLUSIVE, INCLUSIVE. Defaults to EXCLUSIVE. Sets how refund line amount values are read: EXCLUSIVE treats them as net of tax, INCLUSIVE as gross including tax. Ignored for lines without an amount.
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
refundLineItems[].amountstringNoAmount to refund for this line, interpreted per the request-level taxBehavior. Omit to refund the line's full remaining amount. A line may be refunded multiple times cumulatively up to its total.

Partial Refund Example

Refunds 40.00 net of tax against a line that was originally sold for 99.99. The remaining 59.99 stays refundable under a later refund reference.

{
  "merchantRefundReference": "REFUND-2026-000457",
  "refundDate": "2026-01-12T09:15:00Z",
  "taxBehavior": "EXCLUSIVE",
  "refundLineItems": [
    {
      "lineItemId": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
      "refundLineItemReference": "REFUND-ITEM-002",
      "refundReason": "Partial credit for service downtime",
      "amount": "40.00"
    }
  ]
}

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 - on a different transaction or with different line items/amounts. Returned on the field merchantRefundReference. An identical retry returns 200 with the existing refund instead.
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. Creation is asynchronous: submit, then poll or wait for the issuance webhook before fetching the document.

Create Proforma Invoice

POST/api/proforma-invoices

Requests 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. Issuance is asynchronous - the call returns 202 Accepted with a handle, and Outpost links the tax transaction and renders the PDF afterwards. The document is fetched from its own endpoint once issuance completes.

How it works

  • Your checkout collects the B2B customer details, line items, and jurisdiction.
  • Your backend calls POST /api/proforma-invoices with that payload and a fresh Idempotency-Key. The call returns 202 Accepted with the id, number, and status PENDING_TAX_TRANSACTION - no document and no amounts.
  • Outpost links the tax transaction, renders the PDF, and attaches it. This normally takes seconds.
  • Outpost sends proforma_invoice.issued once the document is available, or proforma_invoice.issuance_failed if issuance failed permanently.
  • You fetch the PDF from GET /api/proforma-invoices/{id}/document and display or email it to the customer with bank-transfer instructions.
  • When the transfer settles, Outpost sends a proforma_invoice.settled webhook so you can fulfill the order.

Waiting for completion

  • Register proforma_invoice.issued and proforma_invoice.issuance_failed and treat the webhook as your completion signal.
  • Poll GET /api/proforma-invoices/{id} as the fallback, honouring Retry-After. The create response carries Retry-After: 2 and its Location header is the path to poll.
  • The create response body is a handle, not a state snapshot: its status is the constant PENDING_TAX_TRANSACTION and never changes. Only the poll reports current state.
  • PENDING_TAX_TRANSACTION normally lasts seconds, so a short backoff is enough.

Request Headers

ParameterTypeRequiredDescription
Authorization-YesBearer token for authentication
Content-Type-Yesapplication/json
Idempotency-KeystringYesUnique key for this submission attempt, at most 255 characters. Replaying the same key with the same body returns the original handle instead of creating a second proforma invoice.

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. Only one live proforma invoice may hold a given reference.

Response 202 Accepted

{
  "id": "7b1f2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
  "number": "10026-OP-001",
  "status": "PENDING_TAX_TRANSACTION",
  "merchantReference": "order-1042"
}

Response Headers

ParameterTypeRequiredDescription
Locationstring-Path of the proforma invoice to poll: /api/proforma-invoices/{id}. It is the only authority on current state.
Retry-Afterstring-Seconds to wait before the first poll. Always 2.

Response Fields

ParameterTypeRequiredDescription
idstring (UUID)-Outpost identifier for the proforma invoice. Use it to poll, fetch the document, delete, and to match webhook events.
numberstring-Proforma invoice number assigned at creation
statusstring-Always the constant PENDING_TAX_TRANSACTION. It is never refreshed, replays included, so do not read progress from it - poll GET /api/proforma-invoices/{id} instead.
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
422fx_rate_unavailableNo FX rate is available for the requested currency pair, so the invoice cannot be priced
422proforma_tax_input_invalidThe tax inputs are incomplete or inconsistent. The errors list names the fields to correct.
422nfse_validation_failedThe recipient details do not pass Brazilian e-invoicing (NFS-e) validation. The errors list names the fields to correct.
422merchant_reference_conflictA live proforma invoice already holds this merchantReference. The body carries existingProformaInvoiceId. Delete that proforma invoice to release the reference, or use a different one.
422idempotency_key_reusedThis Idempotency-Key was already used with a different request body. Nothing was created.

Code Example

# Create a proforma invoice for a B2B customer paying by bank transfer
# The Idempotency-Key is required and must be fresh for each submission attempt
curl -X POST \
  "https://api.outpostanywhere.com/api/proforma-invoices" \
  -H "Authorization: Bearer $OUTPOST_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -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: 202 Accepted - body is {id, number, status, merchantReference}
# Location header holds the path to poll, Retry-After is 2

Idempotency

Create is idempotent on the Idempotency-Key header, so a retried or duplicated submission can never produce two proforma invoices for the same order.

Rules

  • Send an Idempotency-Key on every create. The header is required and may be at most 255 characters.
  • Same key, same body - the original handle is replayed. No second proforma invoice is created.
  • Same key, different body - the request is rejected with 422 idempotency_key_reused. Nothing is created.
  • Generate the key per submission attempt. Never derive it from the document contents, and never reuse it for a different document.
  • A replay always returns status PENDING_TAX_TRANSACTION, even when the proforma invoice has long since been issued or failed. Poll GET /api/proforma-invoices/{id} for current state.
  • A spent key replays its proforma invoice handle forever, including after that proforma invoice has been deleted. Recreating after a delete needs a new key.

Replacing a proforma invoice

A proforma invoice that has not been issued yet can be deleted and recreated under the same merchantReference, which is how you correct a mistake before the customer ever sees the document.

  • DELETE /api/proforma-invoices/{id} - 204. The proforma invoice disappears from all reads and releases its merchantReference.
  • POST /api/proforma-invoices with the corrected body, the same merchantReference, and a NEW Idempotency-Key.
  • Reusing the old key here replays the deleted proforma invoice handle instead of creating the replacement.

Error envelope

Every error on this resource shares one shape: a machine-readable code and an errors list of field-level messages.

{
  "code": "proforma_tax_input_invalid",
  "errors": [
    { "field": "recipient", "message": "Recipient is required" }
  ]
}

merchant_reference_conflict adds a top-level existingProformaInvoiceId naming the live proforma invoice that holds the reference.

{
  "code": "merchant_reference_conflict",
  "existingProformaInvoiceId": "019d4d40-15cf-7764-ad6e-b2ec47736bb9",
  "errors": [
    { "field": "merchantReference", "message": "merchantReference is already in use" }
  ]
}

Status Lifecycle

Every proforma invoice starts at PENDING_TAX_TRANSACTION and either reaches ISSUED or ends at the terminal ISSUANCE_FAILED. Read the current status from GET /api/proforma-invoices/{id} - never from the create response.

StatusMeaning
PENDING_TAX_TRANSACTIONAccepted and queued. The tax transaction is not linked yet and there is no document. Normally lasts seconds. Deletable.
ISSUEDThe tax transaction is linked and the PDF is attached, so the document endpoint returns 200. No longer deletable.
INVOICEDA tax invoice has been raised against the proforma invoice.
BOOKEDThe proforma invoice has been booked in the ledger.
CANCELLATION_REQUESTEDCancellation has been requested and is being processed.
CANCELLEDThe proforma invoice has been cancelled.
ISSUANCE_FAILEDTerminal. Issuance failed permanently and no document will ever exist. issuanceFailure carries the reason; the only code today is proforma_tax_input_invalid. Create a new proforma invoice with corrected inputs.

Get Proforma Invoice

GET/api/proforma-invoices/{id}

Returns the proforma invoice. This is the poll target named by the create response’s Location header and the only authority on current state. It answers 200 OK at every stage of the lifecycle, including PENDING_TAX_TRANSACTION and ISSUANCE_FAILED.

Request Headers

ParameterTypeRequiredDescription
Authorization-YesBearer token for authentication

Path Parameters

ParameterTypeRequiredDescription
idstring (UUID)YesOutpost identifier for the proforma invoice, returned by create

Response 200 OK

{
  "id": "7b1f2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
  "number": "10026-OP-001",
  "status": "ISSUED",
  "merchantReference": "order-1042"
}

Response 200 OK - issuance failed

A permanent failure is still a 200. The failure detail lives in issuanceFailure.

{
  "id": "7b1f2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
  "number": "10026-OP-001",
  "status": "ISSUANCE_FAILED",
  "merchantReference": "order-1042",
  "issuanceFailure": {
    "code": "proforma_tax_input_invalid",
    "message": "Recipient is required",
    "failedAt": "2026-09-08T12:00:00Z"
  }
}

Response Fields

ParameterTypeRequiredDescription
idstring (UUID)-Outpost identifier for the proforma invoice
numberstring-Proforma invoice number
statusstring-Current status: PENDING_TAX_TRANSACTION, ISSUED, INVOICED, BOOKED, CANCELLATION_REQUESTED, CANCELLED, or ISSUANCE_FAILED
merchantReferencestring-Your reference. Null when you did not send one.
issuanceFailureobject-Present only when status is ISSUANCE_FAILED
issuanceFailure.codestring-Why issuance failed. The only code today is proforma_tax_input_invalid.
issuanceFailure.messagestring-Human-readable description of the failure
issuanceFailure.failedAtstring-ISO 8601 timestamp the failure was recorded

Error Responses

HTTPCodeDescription
400invalid_argumentid is not a valid UUID
401-Invalid or missing Authorization token
404not_foundNo such proforma invoice, it belongs to another merchant, or it has been deleted

Code Example

# Poll the proforma invoice - the only authority on current state
curl -X GET \
  "https://api.outpostanywhere.com/api/proforma-invoices/${PROFORMA_INVOICE_ID}" \
  -H "Authorization: Bearer $OUTPOST_ACCESS_TOKEN" | jq

# 200 OK at every stage, including PENDING_TAX_TRANSACTION and ISSUANCE_FAILED
# 404 if the proforma invoice is unknown, foreign, or deleted

Get Proforma Invoice Document

GET/api/proforma-invoices/{id}/document

Returns a pre-signed link to the proforma invoice PDF. The link is valid for 15 minutes and the endpoint may be called any number of times, so fetch a fresh link whenever you need to show or send the document rather than storing the URL.

Request Headers

ParameterTypeRequiredDescription
Authorization-YesBearer token for authentication

Path Parameters

ParameterTypeRequiredDescription
idstring (UUID)YesOutpost identifier for the proforma invoice, returned by create

Response 200 OK

{
  "fileName": "proforma-invoice-10026-OP-001.pdf",
  "url": "https://storage.outpostanywhere.com/proforma-invoices/7b1f2c3d-...pdf",
  "expiresAt": "2026-09-08T12:15:00Z"
}

Response Fields

ParameterTypeRequiredDescription
fileNamestring-Name of the generated proforma invoice PDF
urlstring-Pre-signed link to the PDF. Show it on your checkout or email it to the customer.
expiresAtstring-ISO 8601 timestamp when the link expires (15 minutes). Call this endpoint again for a fresh link - there is no limit on how many times you may fetch it.

Response 202 Accepted - The proforma invoice is still being issued and the document does not exist yet. The response carries a Retry-After header - wait that many seconds and call again.

Error Responses

HTTPCodeDescription
401-Invalid or missing Authorization token
404not_foundNo such proforma invoice, it belongs to another merchant, or it has been deleted
409issuance_failedIssuance failed permanently, so there is no document and never will be. Read issuanceFailure on the proforma invoice for the reason.

Code Example

# Fetch a fresh 15-minute link to the proforma invoice PDF
curl -X GET \
  "https://api.outpostanywhere.com/api/proforma-invoices/${PROFORMA_INVOICE_ID}/document" \
  -H "Authorization: Bearer $OUTPOST_ACCESS_TOKEN" | jq

# 200 OK with {fileName, url, expiresAt}
# 202 Accepted with Retry-After while the proforma invoice is still being issued
# 409 issuance_failed once issuance has failed permanently

Delete Proforma Invoice

DELETE/api/proforma-invoices/{id}

Deletes a proforma invoice that has not been issued yet. Use it to withdraw a submission you got wrong, then create the replacement under the same merchantReference.

Request Headers

ParameterTypeRequiredDescription
Authorization-YesBearer token for authentication

Path Parameters

ParameterTypeRequiredDescription
idstring (UUID)YesOutpost identifier for the proforma invoice, returned by create

Response 204 No Content - The proforma invoice was deleted.

Behaviour

  • Deletion is allowed until the tax transaction is linked. After that the endpoint returns 409 not_deletable and the proforma invoice must be cancelled instead.
  • Deleting an already-deleted proforma invoice returns 204 as well - the call is idempotent.
  • Deletion is soft on Outpost’s side and permanent from yours: the proforma invoice disappears from every read, its document is no longer retrievable, and it never comes back.
  • A deleted proforma invoice releases its merchantReference, so a fresh create may reuse it. merchant_reference_conflict therefore only ever names a live proforma invoice.

Error Responses

HTTPCodeDescription
400invalid_argumentid is not a valid UUID
401-Invalid or missing Authorization token
404not_foundNo such proforma invoice, or it belongs to another merchant
409not_deletableThe tax transaction is already linked, so the proforma invoice can no longer be deleted. Cancel it instead.

Code Example

# Withdraw a proforma invoice that has not been issued yet
curl -X DELETE \
  "https://api.outpostanywhere.com/api/proforma-invoices/${PROFORMA_INVOICE_ID}" \
  -H "Authorization: Bearer $OUTPOST_ACCESS_TOKEN" -i

# 204 No Content on success, and on repeat - the call is idempotent
# 409 not_deletable once the tax transaction is linked - cancel it instead
# Recreate with the same merchantReference and a NEW Idempotency-Key

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

Proforma invoices are issued asynchronously, and the bank transfers that pay them settle asynchronously too - sometimes minutes, sometimes days later. Rather than polling, register a webhook endpoint and Outpost will notify your backend when the proforma invoice is issued or its issuance fails, and again 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 full event id.
  • The event id is deterministic per entity and topic, so a redelivered event always carries the same id, and two topics about the same entity never collide.
  • 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.issued

A proforma invoice has been issued and its PDF is attached, so the document endpoint already returns 200. This is the completion signal for an asynchronous create.

proforma_invoice.issuance_failed

Issuance failed permanently. No document exists for this proforma invoice and none ever will - create a new one with corrected inputs.

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. Deterministic per entity and topic, so it is the same on every retry of the same event. The topic is part of the id - one proforma invoice emits evt_issued_<proformaInvoiceId>, evt_issuance_failed_<proformaInvoiceId>, and evt_<proformaInvoiceId> for proforma_invoice.settled - so deduplicate on the full id, never on the entity id inside it.
typestringEvent type
createdstringISO 8601 timestamp the event was generated