API onboarding for partners

This page describes how a partner (a payment provider or a platform) onboards its merchants to Outpost through the API. Your systems hold the merchant data and call Outpost server to server. The merchant never leaves your product. If you would rather hand the merchant to an Outpost-hosted form, use the Hosted Onboarding API instead.

Merchants and applications

The API splits the merchant from the application, and the split matters because the two have different lifetimes.

  • A merchant is the business you are onboarding. Outpost creates it for you when you initiate. It is identified by amerchantId and it carries your own reference, so you can match it back to your records. Submitting, polling and every tax call are scoped to it.
  • An application is a request to activate one Outpost product for that merchant in a set of regions. It has a status, a set of requirements and a review outcome. A merchant has at most one application.

Who does what

Every call is yours. The merchant stays inside your product, and Outpost appears at review. You poll the application for the outcome.

Your platform

  1. 1

    Create the merchant and the application, with the company details

    POST /partner/api/onboarding/initiate

    status becomes DRAFT

  2. 2

    Fill in the application, over as many calls as you like

    PATCH /partner/api/onboarding/applications/{applicationId}

  3. 3

    Submit for review

    POST /partner/api/onboarding/applications/{applicationId}/submit

    status becomes IN_REVIEW

Outpost

  1. 4

    An ops manager reviews the application. Poll for the result.

APPROVED

The merchant is live for that product. Nothing more to do.

CHANGES_REQUESTED

Read audit.reviewMessage and comments, PATCH the application, then submit again.

REJECTED

Final. This outcome stands.

On CHANGES_REQUESTED you return to step 2: PATCH the application, then submit again. Poll GET /partner/api/onboarding/applications/{applicationId} on a slow schedule until the status leaves IN_REVIEW.

The flow

  1. 1Create the merchant and the application. POST /partner/api/onboarding/initiate with mode set to API, your own reference, one product code and the company details. One call creates both the merchant and its application, and returns the merchantId and the applicationId you need for everything after this.
  2. 2Fill in the application. PATCH /partner/api/onboarding/applications/{applicationId} with the regions, the trading description, the store URLs, the category and the legal representative. Every field is optional, so send them in one call or spread them across as many as your product needs.
  3. 3Submit for review. POST /partner/api/onboarding/applications/{applicationId}/submit. This is the one call that checks the application is complete. Outpost then reviews it.
  4. 4Poll for the outcome. GET /partner/api/onboarding/applications/{applicationId} and read status, audit.reviewMessage and comments.

Every call after initiate is addressed by the applicationId that initiate returns. Initiate also returns a merchantId, which this flow does not need: store it for the tax endpoints, which are scoped to the merchant.

Base URL

All paths on this page are relative to https://api.outpostanywhere.com. Responses omit null fields rather than returning them as null, so treat a missing key as "not set".

Authentication

Every endpoint on this page uses the same authentication as the other partner APIs: OAuth2 client_credentials. Your backend exchanges a client ID and secret for an access token, then sends that token as a Bearer token on each call. Your credentials come from the Partner Portal. Ask your Outpost contact for access.

Get a token

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

Call the API

curl "https://api.outpostanywhere.com/partner/api/whoami" \
  -H "Authorization: Bearer $OUTPOST_ACCESS_TOKEN"

Cache the token server side until it expires, with a small buffer. Refresh it and retry once on a 401. Run all of this from your server — never put the client secret in a browser or a mobile app.

Scope

A token is scoped to one partner. You can only see and change merchants that were created under that partner. A merchant that belongs to another partner, or a merchant id that does not exist, returns 404 — never 403. That is deliberate: it stops anyone from probing merchant ids.

Reference data

Two read-only endpoints help you set up. Neither is scoped to a merchant.

Who am I

GET/partner/api/whoami

Returns the partner your token belongs to. Use it to check credentials after a rotation, and to tell your staging and production keys apart.

{
  "partnerId": "3d7c2e19-84af-4c0b-9f61-5b8e2a7d1c03",
  "name": "Example Payments"
}

Regions

GET/partner/api/regions

Returns every region where Outpost supports at least one product. Regions that support neither Tax of Record nor Merchant of Record are left out.

ParameterTypeRequiredDescription
idstring (uuid)The internal region id. It is a random UUID and differs between environments, so look it up rather than hard-coding it.
namestringHuman-readable name, for example "United Kingdom" or "California".
flagCodestringShort code, for example EU, GB or CA. Not unique on its own. CA is both Canada’s neighbour California and, in another row, Canada.
countryCodestringISO 3166-1 alpha-2 country the region sits in. Absent for regions that are not a single country, such as the European Union.
torSupportedbooleanWhether Tax of Record is available in this region.
morSupportedbooleanWhether Merchant of Record is available in this region.

200 OK

[
  {
    "id": "5f4e3d2c-1b0a-4c9d-8e7f-6a5b4c3d2e1f",
    "name": "European Union",
    "flagCode": "EU",
    "torSupported": true,
    "morSupported": true
  },
  {
    "id": "2a9c8b7d-6e5f-4a3b-9c8d-7e6f5a4b3c2d",
    "name": "United Kingdom",
    "flagCode": "GB",
    "countryCode": "GB",
    "torSupported": true,
    "morSupported": true
  }
]

The European Union row has no countryCode because it is not one country, so the field is left out of the response entirely.

Product codes

An application activates one product. Two codes are meaningful today, both lowercase:

  • tor — Tax of Record. Outpost calculates, files and remits the merchant’s sales tax and VAT.
  • mor — Merchant of Record. Outpost becomes the seller of record for the merchant’s transactions.

The API stores whatever product code you send, and an unrecognised one behaves like mor, adding no product requirements. Send tor or mor, exactly as written.

Applications

An application is a request to activate one product for one merchant in one or more regions. Creating it is the first call you make: it creates the merchant, links it to your partner account and opens the application in a single step.

Create the merchant and the application

POST/partner/api/onboarding/initiate

This is the same endpoint the Hosted Onboarding API uses. Set mode to API and it creates the merchant, the link to your partner account and the application in one call, with the company details attached. No hosted link is generated, so the onboardingUrl in the response is not usable, so ignore it.

Request body

ParameterTypeRequiredDescription
merchantReferencestringYesYour own identifier for this merchant. Unique per partner, and permanent. Calling initiate again with the same reference returns the existing application.
productCodestringYestor or mor. Fixed once set. The application keeps this product.
modestring enumFor this flowHOSTED or API. Optional in the schema and defaults to HOSTED, so you must send API explicitly. Omitting it gives you a hosted application and a link for the merchant to complete.
companyobjectYesLegal entity details, broken out below. Required here, and correctable later with PATCH.
returnUrlstringNoWhere the merchant returns after a hosted flow. Ignored in API mode.
complianceSummaryobjectNoChecks you have already run, broken out below. Stored and shown to the Outpost reviewer.
merchantContextobjectNoFree-form string map carried through to the Outpost reviewer.

company

Required here, with the same rules in both modes. If something changes or arrives wrong, PATCH replaces the whole object under the same rules.

ParameterTypeRequiredDescription
company.legalNamestringYesRegistered legal name.
company.tradingNamestringNoTrading or brand name, if it differs from the legal name.
company.websitestringYesThe business's website.
company.taxIdstringYesTax identifier in the jurisdiction of registration.
company.registrationNumberstringYesCompany registration number.
company.jurisdictionstringYesCountry of incorporation, ISO 3166-1 alpha-2.
company.gmvTierstring enumNoExpected annual gross merchandise value band. One of the values listed below. Any other value is rejected as a malformed body, not as a field error.
company.registeredAddress.line1stringYesPrimary address line.
company.registeredAddress.line2stringNoSecondary address line.
company.registeredAddress.citystringYesCity.
company.registeredAddress.statestringNoState or province.
company.registeredAddress.postalCodestringYesPostal code.
company.registeredAddress.countrystringYesISO 3166-1 alpha-2 country code.

Accepted values for company.gmvTier:

UP_TO_100KFROM_100K_TO_250KFROM_250K_TO_500KFROM_500K_TO_1MFROM_1M_TO_10MFROM_10M_TO_50MFROM_50M_TO_100MFROM_100M_TO_250MFROM_250M_TO_500MFROM_500M_TO_1B

complianceSummary

Optional. Checks you have already run, stored against the application and shown to the Outpost reviewer. Every field is free text.

ParameterTypeRequiredDescription
complianceSummary.verificationStatusstringNoFree text. Your own verification outcome for this business.
complianceSummary.riskLevelstringNoFree text. Your own risk rating.
complianceSummary.countryOfIncorporationstringNoISO 3166-1 alpha-2 country code.
complianceSummary.hasOpenComplianceFlagsbooleanNoWhether you have unresolved compliance flags on this business.

Example request

curl -X POST \
  "https://api.outpostanywhere.com/partner/api/onboarding/initiate" \
  -H "Authorization: Bearer $OUTPOST_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "merchantReference": "psp-merchant-4821",
    "productCode": "tor",
    "mode": "API",
    "company": {
      "legalName": "Northwind Digital Ltd",
      "tradingName": "Northwind",
      "registrationNumber": "09123456",
      "taxId": "GB123456789",
      "jurisdiction": "GB",
      "website": "https://northwind.example",
      "gmvTier": "FROM_1M_TO_10M",
      "registeredAddress": {
        "line1": "12 Example Street",
        "line2": "Floor 3",
        "city": "London",
        "state": "Greater London",
        "postalCode": "EC1A 1AA",
        "country": "GB"
      }
    },
    "complianceSummary": {
      "verificationStatus": "verified",
      "riskLevel": "low",
      "countryOfIncorporation": "GB",
      "hasOpenComplianceFlags": false
    },
    "merchantContext": {
      "accountManager": "avery.diaz@acme-psp.example",
      "segment": "mid-market"
    }
  }'

Response 200 OK

{
  "applicationId": "3f8c1e07-5a44-4b91-9d2e-77a0c6b41e58",
  "merchantId": "9b1f4c8e-6d2a-4f7b-8e3c-1a5d0f2b7c94",
  "status": "DRAFT",
  "mode": "API",
  "isExistingApplication": false,
  "onboardingUrl": "https://onboarding.outpostanywhere.com/acme-psp/onboarding?app=3f8c1e07-5a44-4b91-9d2e-77a0c6b41e58"
}
ParameterTypeRequiredDescription
applicationIdstring (uuid)Identifier for the application. Use it for the PATCH that fills the application in.
merchantIdstring (uuid)Identifier for the merchant Outpost created. Use it for submit, for polling and for every tax call. Returned whether the application is new or already existed.
statusstringApplication status. DRAFT on creation. These are the raw internal values, not the lowercase ones the merchant-scoped endpoints return.
modestringHOSTED or API, as stored.
isExistingApplicationbooleantrue when an application already existed for this merchantReference. Company details in your request were discarded.
onboardingUrlstringHosted link for the merchant. In API mode this is returned without a token and is not usable in this mode, so ignore it.

Errors

HTTPCodeDescription
400invalid_argumentA required field is missing or blank. Each missing field is named, for example company.registeredAddress.city.
500internal_errorThe body could not be parsed, for example an unrecognised gmvTier or mode. Validate those values before sending; retrying the same body gets the same result.
401Invalid or missing Authorization token.

Call it before anything else

If an application already exists for that merchantReference, initiate returns the existing one with isExistingApplication: true and keeps the company details it already has, so send yours on the first call.

Get the application

GET/partner/api/onboarding/applications/{applicationId}

Returns the current state. This is the call you poll after submitting, and every endpoint in this section returns this same shape.

Example request

curl "https://api.outpostanywhere.com/partner/api/onboarding/applications/{applicationId}" \
  -H "Authorization: Bearer $OUTPOST_ACCESS_TOKEN"

Response 200 OK

{
  "applicationId": "3f8c1e07-5a44-4b91-9d2e-77a0c6b41e58",
  "merchantId": "9b1f4c8e-6d2a-4f7b-8e3c-1a5d0f2b7c94",
  "status": "IN_REVIEW",
  "mode": "API",
  "partnerId": "3d7c2e19-84af-4c0b-9f61-5b8e2a7d1c03",
  "partnerMerchantReference": "psp-merchant-4821",
  "productCode": "tor",
  "company": {
    "legalName": "Northwind Digital Ltd",
    "jurisdiction": "GB"
  },
  "selectedRegions": ["5f4e3d2c-1b0a-4c9d-8e7f-6a5b4c3d2e1f"],
  "businessDescription": "Subscription meal kits sold to consumers in the EU",
  "storeUrls": ["https://shop.example.com"],
  "legalRepresentative": {
    "fullName": "Jane Doe",
    "email": "jane.doe@northwind.example",
    "role": "Managing Director"
  },
  "contacts": {
    "financeContact": {
      "fullName": "Frank Smith",
      "email": "finance@northwind.example",
      "role": "CFO"
    },
    "technicalContact": {
      "fullName": "Tara Jones",
      "email": "tech@northwind.example",
      "role": "CTO"
    }
  },
  "comments": [],
  "editableSections": ["businessModel", "productSelection", "contacts", "company"],
  "requiredFields": [
    "company.legalName",
    "company.website",
    "company.taxId",
    "company.registrationNumber",
    "company.jurisdiction",
    "company.registeredAddress",
    "company.registeredAddress.line1",
    "company.registeredAddress.city",
    "company.registeredAddress.postalCode",
    "company.registeredAddress.country",
    "legalRepresentative.fullName",
    "legalRepresentative.email"
  ],
  "audit": {
    "createdAt": "2026-07-21T09:02:11.004Z",
    "updatedAt": "2026-07-21T09:14:52.118Z",
    "submittedAt": "2026-07-21T09:14:52.118Z"
  }
}
ParameterTypeRequiredDescription
applicationIdstring (uuid)The application you addressed.
merchantIdstring (uuid)The Outpost merchant this application belongs to. Store it for the merchant-scoped endpoints, such as the tax APIs.
statusstringOne of DRAFT, IN_REVIEW, CHANGES_REQUESTED, APPROVED, SIGNED_BY_MERCHANT, SIGNED_BY_OUTPOST, REJECTED. See the status lifecycle below.
modestringHOSTED or API, as stored.
partnerIdstringYour partner identifier.
partnerMerchantReferencestringThe merchantReference you sent at initiate, echoed back. Your own reference, not to be confused with merchantId above, which is the Outpost merchant UUID.
productCodestringThe productCode you sent at initiate, echoed back under the same name.
companyobjectThe company object as stored. See OnboardingCompany at initiate.
complianceSummaryobjectThe compliance summary you sent at initiate, when you sent one.
merchantContextobjectThe metadata you sent at initiate, when you sent any.
selectedRegionsstring[]The region ids currently on the application.
businessDescriptionstringWhat you last sent as businessDescription.
storeUrlsstring[]What you last sent as storeUrls.
legalRepresentativeobjectThe legal representative on the application, with fullName, email and role. Absent until someone sends one, whether that is you or the merchant in the hosted flow.
contactsobjectfinanceContact and technicalContact, each with fullName, email and role. Absent until at least one of them is set.
commentsobject[]Reviewer feedback. Filled only while the status is CHANGES_REQUESTED or REJECTED, and only with comments a reviewer has left open. Empty in every other status, and empty again once you resubmit. See the fields below.
editableSectionsstring[]Which sections you may still change: businessModel, productSelection, contacts and company. The set is the same on every response.
requiredFieldsstring[]A fixed list of the fields the flow requires: the company fields, plus legalRepresentative.fullName and legalRepresentative.email. It is the same on every response whatever the product, so a mor application lists the legalRepresentative entries too even though only tor requires them, and it stays the same as you fill the application in. To learn what is still outstanding, call submit and read the 400.
auditobjectcreatedAt and updatedAt, plus submittedAt, reviewedAt and reviewMessage once they exist.

Fields holding their default value are left out of the response. An empty comments array means the reviewer has left nothing open.

comments

ParameterTypeRequiredDescription
comments[].idstringIdentifier for the comment. Use it to avoid showing the same note twice.
comments[].target.sectionKeystringThe section the reviewer commented on: company, contacts, businessModel or productSelection.
comments[].target.fieldKeystringThe specific field, when the comment is about one. Absent when the comment is about the whole section.
comments[].messagestringWhat the reviewer wrote, meant to be read by a person.
comments[].createdAtstring (ISO 8601)When the comment was left.

Errors

HTTPCodeDescription
400invalid_argumentThe applicationId in the path is not a UUID.
404not_foundNo application with that id belongs to your partner account. You never get a 403.
401Invalid or missing Authorization token.

Fill in the application

PATCH/partner/api/onboarding/applications/{applicationId}

Fills in the rest of the application: the regions, what the merchant sells, its storefront URLs, its category, the people to contact, and a correction to the company if you need one.

Every field is optional, so shape the flow to your product

Send one field or all of them, in whatever order suits you, across as many calls as you like. Collect the regions on one screen and the contacts on another, save each step as the merchant completes it, and let them come back to it tomorrow. The application simply holds whatever you have sent so far. Completeness is checked once, at submit.

A tor application also needs a legalRepresentative before it can be submitted, so send one here. The finance and technical contacts under contacts are optional. In the hosted flow the merchant fills all three contacts in the onboarding UI, so anything you send now is what they see, and anything they change comes back to you on the next read.

This call is addressed by the applicationId that initiate returned, so you can make it straight away. PATCH keeps what you do not send: a field you leave out keeps its current value, and sending null also keeps it. To change a value, send the new one. What PATCH does check is the shape of what you send: any email must be a valid address, and a company must arrive complete. Both answer 400 invalid_argument and store nothing. selectedRegions, legalRepresentative andcontacts replace what is there rather than merging into it, so send each of them whole.

Request body

ParameterTypeRequiredDescription
selectedRegionsstring[]NoThe regions the merchant wants the product enabled in — the partner sends what the merchant requested. The values correspond to the ids returned by GET /partner/api/regions. Replaces the whole list rather than merging.
businessDescriptionstringNoWhat the merchant sells. Satisfies the businessDescription requirement.
storeUrlsstring[]NoThe merchant’s storefront or checkout URLs. Satisfies the checkoutUrl requirement.
categorystringNoFree-text business category. Not part of any requirement today.
legalRepresentativeobjectNoThe person authorised to act for the merchant. A tor application cannot be submitted without one. Replaces the whole object rather than merging, so send every field you want to keep.
legalRepresentative.fullNamestringNoFull name. Needed, together with email, to clear the requirement.
legalRepresentative.emailstringNoEmail address. Checked on the way in: a malformed address is rejected with 400 invalid_argument and nothing is stored.
legalRepresentative.rolestringNoJob title, for example Managing Director. Free text and not part of any requirement.
contactsobjectNofinanceContact and technicalContact. Sending it replaces both: leave one out and it is cleared, so send the pair every time. Leave contacts out of the request entirely and both keep their current values.
contacts.financeContactobjectNoWho to contact about tax filings and invoices. Same fullName, email and role fields, all optional. The email is checked the same way.
contacts.technicalContactobjectNoWho to contact about the integration. Same fullName, email and role fields, all optional. The email is checked the same way.
companyobjectNoCorrects the company sent at initiate. Same fields, same rules. It replaces the stored company outright, and it is the one field checked for completeness on the way in: send it whole or leave it out.

Example request

curl -X PATCH \
  "https://api.outpostanywhere.com/partner/api/onboarding/applications/{applicationId}" \
  -H "Authorization: Bearer $OUTPOST_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "selectedRegions": ["5f4e3d2c-1b0a-4c9d-8e7f-6a5b4c3d2e1f"],
    "businessDescription": "Subscription meal kits sold to consumers in the EU",
    "storeUrls": ["https://shop.example.com"],
    "category": "retail",
    "legalRepresentative": {
      "fullName": "Jane Doe",
      "email": "jane.doe@northwind.example",
      "role": "Managing Director"
    },
    "contacts": {
      "financeContact": {
        "fullName": "Frank Smith",
        "email": "finance@northwind.example",
        "role": "CFO"
      },
      "technicalContact": {
        "fullName": "Tara Jones",
        "email": "tech@northwind.example",
        "role": "CTO"
      }
    }
  }'

Response 200 OK

{
  "applicationId": "3f8c1e07-5a44-4b91-9d2e-77a0c6b41e58",
  "merchantId": "9b1f4c8e-6d2a-4f7b-8e3c-1a5d0f2b7c94",
  "status": "DRAFT",
  "mode": "API",
  "partnerId": "3d7c2e19-84af-4c0b-9f61-5b8e2a7d1c03",
  "partnerMerchantReference": "psp-merchant-4821",
  "productCode": "tor",
  "company": {
    "legalName": "Northwind Digital Ltd",
    "jurisdiction": "GB"
  },
  "selectedRegions": ["5f4e3d2c-1b0a-4c9d-8e7f-6a5b4c3d2e1f"],
  "businessDescription": "Subscription meal kits sold to consumers in the EU",
  "storeUrls": ["https://shop.example.com"],
  "legalRepresentative": {
    "fullName": "Jane Doe",
    "email": "jane.doe@northwind.example",
    "role": "Managing Director"
  },
  "contacts": {
    "financeContact": {
      "fullName": "Frank Smith",
      "email": "finance@northwind.example",
      "role": "CFO"
    },
    "technicalContact": {
      "fullName": "Tara Jones",
      "email": "tech@northwind.example",
      "role": "CTO"
    }
  },
  "comments": [],
  "editableSections": ["businessModel", "productSelection", "contacts", "company"],
  "requiredFields": [
    "company.legalName",
    "company.website",
    "company.taxId",
    "company.registrationNumber",
    "company.jurisdiction",
    "company.registeredAddress",
    "company.registeredAddress.line1",
    "company.registeredAddress.city",
    "company.registeredAddress.postalCode",
    "company.registeredAddress.country",
    "legalRepresentative.fullName",
    "legalRepresentative.email"
  ],
  "audit": {
    "createdAt": "2026-07-21T09:02:11.004Z",
    "updatedAt": "2026-07-21T09:07:36.512Z"
  }
}

Same shape as Get the application.

Errors

HTTPCodeDescription
400invalid_argumentThe applicationId in the path is not a UUID, or a contact email is not a valid address. For a bad email the body names the field, for example legalRepresentative.email or contacts.financeContact.email, and nothing in the request is stored.
404not_foundNo application with that id belongs to your partner account. You never get a 403.
401Invalid or missing Authorization token.
409invalid_statusThe application is not in DRAFT or CHANGES_REQUESTED, so it can no longer be edited.

Submit for review

POST/partner/api/onboarding/applications/{applicationId}/submit

Hands the application to Outpost for review. Send it with an empty body. On success the status becomes IN_REVIEW and audit.submittedAt is set.

This is the one gate in the flow. Everything before it accepts partial data, so submit is where Outpost checks that the application is complete. It runs three checks, in this order.

What submit checks

  1. 1Ownership. The application belongs to your partner account. Otherwise404 not_found.
  2. 2Status. It is DRAFT or CHANGES_REQUESTED. Otherwise 409 invalid_status.
  3. 3Completeness. Every required field holds a usable value. Otherwise400 incomplete_application, listing each one. See Requirements for the full set and the field that satisfies each.

The completeness check reports everything at once rather than stopping at the first problem, so one rejected submit tells you the whole list. Submitting early is a fair way to ask what is left: a rejected submit changes nothing.

Example request

curl -X POST \
  "https://api.outpostanywhere.com/partner/api/onboarding/applications/{applicationId}/submit" \
  -H "Authorization: Bearer $OUTPOST_ACCESS_TOKEN"

Response 200 OK

{
  "applicationId": "3f8c1e07-5a44-4b91-9d2e-77a0c6b41e58",
  "merchantId": "9b1f4c8e-6d2a-4f7b-8e3c-1a5d0f2b7c94",
  "status": "IN_REVIEW",
  "mode": "API",
  "partnerId": "3d7c2e19-84af-4c0b-9f61-5b8e2a7d1c03",
  "partnerMerchantReference": "psp-merchant-4821",
  "productCode": "tor",
  "company": {
    "legalName": "Northwind Digital Ltd",
    "jurisdiction": "GB"
  },
  "selectedRegions": ["5f4e3d2c-1b0a-4c9d-8e7f-6a5b4c3d2e1f"],
  "businessDescription": "Subscription meal kits sold to consumers in the EU",
  "storeUrls": ["https://shop.example.com"],
  "legalRepresentative": {
    "fullName": "Jane Doe",
    "email": "jane.doe@northwind.example",
    "role": "Managing Director"
  },
  "contacts": {
    "financeContact": {
      "fullName": "Frank Smith",
      "email": "finance@northwind.example",
      "role": "CFO"
    },
    "technicalContact": {
      "fullName": "Tara Jones",
      "email": "tech@northwind.example",
      "role": "CTO"
    }
  },
  "comments": [],
  "editableSections": ["businessModel", "productSelection", "contacts", "company"],
  "requiredFields": [
    "company.legalName",
    "company.website",
    "company.taxId",
    "company.registrationNumber",
    "company.jurisdiction",
    "company.registeredAddress",
    "company.registeredAddress.line1",
    "company.registeredAddress.city",
    "company.registeredAddress.postalCode",
    "company.registeredAddress.country",
    "legalRepresentative.fullName",
    "legalRepresentative.email"
  ],
  "audit": {
    "createdAt": "2026-07-21T09:02:11.004Z",
    "updatedAt": "2026-07-21T09:14:52.118Z",
    "submittedAt": "2026-07-21T09:14:52.118Z"
  }
}

Same shape as Get the application.

Errors

HTTPCodeDescription
400incomplete_applicationA required field is still missing. The body names every outstanding key at once.
400invalid_argumentThe applicationId in the path is not a UUID.
404not_foundNo application with that id belongs to your partner account.
409invalid_statusAlready submitted, or past the point where it can be submitted again.
401Invalid or missing Authorization token.

A 400 incomplete_application names every outstanding key:

{
  "code": "incomplete_application",
  "errors": [
    { "field": "checkoutUrl", "message": "checkoutUrl is required" },
    { "field": "legalRepresentative", "message": "legalRepresentative is required" }
  ]
}

Submit is not idempotent. A second call while the application is already IN_REVIEW returns 409 invalid_status. Treat that as "already submitted", not as an error to retry.

Merchants

A merchant is the business you are onboarding. These two endpoints cover every merchant in your partner account, whichever way it was onboarded: through this API flow or through Hosted Onboarding. Use them to reconcile your own records against what Outpost holds.

List merchants

GET/partner/api/merchants

Returns every merchant linked to your partner account, as an array of id and reference. The list is scoped to your partner account.

[
  {
    "id": "9b1f4c8e-6d2a-4f7b-8e3c-1a5d0f2b7c94",
    "reference": "psp-merchant-4821"
  },
  {
    "id": "5c2a7f10-3b94-4de6-a1c8-7f60d3e91b25",
    "reference": "psp-merchant-4822"
  }
]

Get a merchant

GET/partner/api/merchants/{merchantId}
ParameterTypeRequiredDescription
idstring (uuid)The Outpost merchant id. Use it in every other path on this page.
referencestringThe merchantReference you sent at initiate.
companyobjectThe company object you sent at initiate.
businessDescriptionstringWhat you last sent as businessDescription on the application.
storeUrlsstring[]What you last sent as storeUrls on the application.

200 OK

{
  "id": "9b1f4c8e-6d2a-4f7b-8e3c-1a5d0f2b7c94",
  "reference": "psp-merchant-4821",
  "company": {
    "legalName": "Kitchen Table Foods B.V.",
    "registrationNumber": "NL852749301",
    "taxId": "NL852749301B01",
    "jurisdiction": "NL",
    "website": "https://shop.example.com",
    "registeredAddress": {
      "line1": "Keizersgracht 241",
      "city": "Amsterdam",
      "postalCode": "1016 EA",
      "country": "NL"
    }
  },
  "businessDescription": "Subscription meal kits sold to consumers in the EU",
  "storeUrls": ["https://shop.example.com"]
}

businessDescription andstoreUrls appear once you have sent them on the application, so a merchant you have just created returnsid,reference andcompany alone.

Requirements

An application has to hold a value for each of these keys before Outpost will take it.Submit is where they are checked, and a 400 incomplete_application names every key that is still missing:

{
  "code": "incomplete_application",
  "errors": [
    { "field": "selectedRegions", "message": "selectedRegions is required" },
    { "field": "businessDescription", "message": "businessDescription is required" },
    { "field": "checkoutUrl", "message": "checkoutUrl is required" },
    { "field": "legalRepresentative", "message": "legalRepresentative is required" }
  ]
}

The keys and what satisfies them

Requirement keyEndpointRequest fieldNotes
selectedRegionsPATCH /onboarding/applications/{applicationId}selectedRegionsThe regions the merchant wants the product enabled in. Send them before you submit, as the ids returned by GET /partner/api/regions.
businessDescriptionPATCH /onboarding/applications/{applicationId}businessDescriptionSatisfied by any non-blank string.
checkoutUrlPATCH /onboarding/applications/{applicationId}storeUrlsThe requirement key is checkoutUrl and the request field is storeUrls. Send a storeUrls array with at least one entry.
company.*POST /onboarding/initiate, or PATCH to correct itcompanyOne key per missing company field, for example company.taxId or company.registeredAddress.city. These apply to every application whatever the product, and they are normally satisfied at initiate.
legalRepresentativePATCH /onboarding/applications/{applicationId}legalRepresentativeSend a non-blank fullName and a valid email. The role is optional and does not affect the requirement. A malformed email is rejected on the PATCH with 400 invalid_argument, so the requirement never clears on bad data.

The key checkoutUrl is the one that catches people out: the request field is called storeUrls. Send it on the PATCH and the requirement clears.

Requirements depend on the product

Only tor adds product-specific requirements, including legalRepresentative. An application for mor adds none of those. Two things still apply to every application: the products key, which one PATCH with selectedRegions clears, and the company keys.

Submit checks status and ownership as well as completeness. It answers 409 invalid_status if the application is not in DRAFT or CHANGES_REQUESTED, and 404 if the application belongs to another partner. Handle both cases.

What to put in regions

selectedRegions takes regionid values from GET /partner/api/regions. Anything else — a flag code like "EU", a typo, an unknown UUID — is rejected with 400 invalid_argument naming the offending entry.

Region ids differ between staging and production, so look them up rather than hard-coding them.

Status lifecycle

An application has seven statuses, in upper case. Every endpoint in this document reports the same set, so one parser covers the whole flow.

StatusWhat it means
DRAFTThe application exists and you can still change it. This is the status right after initiate.
IN_REVIEWYou have submitted it and Outpost is reviewing. The application is read-only.
CHANGES_REQUESTEDA reviewer wants something changed. You can edit again, then submit again.
APPROVEDOutpost accepted the application. Outpost can still move it back to CHANGES_REQUESTED.
SIGNED_BY_MERCHANTThe merchant has signed the Outpost agreement in the Outpost merchant dashboard.
SIGNED_BY_OUTPOSTOutpost has counter-signed. The merchant is onboarded.
REJECTEDOutpost declined the application. This outcome stands.

What each call does in each status

StatusPATCHPOST /submit
DRAFT200200, or 400 if requirements are outstanding
IN_REVIEW409 invalid_status409 invalid_status
CHANGES_REQUESTED200200, or 400 if requirements are outstanding
APPROVED409 invalid_status409 invalid_status
SIGNED_BY_MERCHANT409 invalid_status409 invalid_status
SIGNED_BY_OUTPOST409 invalid_status409 invalid_status
REJECTED409 invalid_status409 invalid_status

In short: you can write while the application is DRAFT or CHANGES_REQUESTED. Everything else is read-only and answers 409.

Three statuses mean the merchant is through

APPROVED is the decision you are waiting for. The two signing statuses that follow it, SIGNED_BY_MERCHANT and SIGNED_BY_OUTPOST, cover the agreement the merchant signs in the Outpost merchant dashboard. Treat all three as a successful outcome and stop polling.

APPROVED can move back toCHANGES_REQUESTED if Outpost reopens the application, so read the status again before you rely on it much later.

After you submit

Poll for the outcome

Once you submit, the application goes to Outpost for review and the result appears on the application itself. Read it on a schedule. Webhooks are on the roadmap; until they land, polling is how you learn the outcome.

Poll the application

curl "https://api.outpostanywhere.com/partner/api/onboarding/applications/{applicationId}" \
  -H "Authorization: Bearer $OUTPOST_ACCESS_TOKEN"

An ops manager reviews each application. Poll on a slow schedule — minutes rather than seconds — and stop once the status is APPROVED, SIGNED_BY_OUTPOST or REJECTED. Watch three fields:

  • status — the outcome. IN_REVIEW means the review is still open.
  • audit.reviewMessage — free text from the reviewer, meant to be read by a person. Show it to whoever manages the merchant.
  • comments — the specific problems, one entry per open reviewer note, each pointing at the section and field it concerns. Only filled while the status is CHANGES_REQUESTED or REJECTED.

Handling CHANGES_REQUESTED

{
  "applicationId": "3f8c1e07-5a44-4b91-9d2e-77a0c6b41e58",
  "status": "CHANGES_REQUESTED",
  "mode": "API",
  "productCode": "tor",
  "selectedRegions": ["EU"],
  "comments": [
    {
      "id": "7c1d4a92-3e08-4f51-b6a2-0d9e5c83f174",
      "target": { "sectionKey": "businessModel", "fieldKey": "businessDescription" },
      "message": "Describe what the merchant sells, not just the industry.",
      "createdAt": "2026-07-22T14:03:07.442Z"
    },
    {
      "id": "b48f0c37-92a1-4d6e-8f05-2c7a1e94b306",
      "target": { "sectionKey": "businessModel" },
      "message": "The store URL returns a holding page.",
      "createdAt": "2026-07-22T14:03:07.442Z"
    }
  ],
  "editableSections": ["businessModel", "productSelection", "contacts", "company"],
  "audit": {
    "createdAt": "2026-07-21T09:02:11.004Z",
    "updatedAt": "2026-07-22T14:03:07.442Z",
    "submittedAt": "2026-07-21T09:14:52.118Z",
    "reviewedAt": "2026-07-22T14:03:07.442Z",
    "reviewMessage": "We need a little more detail before we can approve this."
  }
}

The reviewer’s notes arrive in comments, and the covering note in audit.reviewMessage. Both appear only in CHANGES_REQUESTED and REJECTED, so an emptycomments array in any other status is expected rather than a sign that feedback is missing.

Each comment carries target.sectionKey and, when the note is about one field, target.fieldKey. Use the section to route the merchant to the right screen and the field to highlight the input. Show message as written: it is meant for a person. id is stable, so you can track which notes you have already shown.

The application is writable again in this status. Fix what was raised with PATCH, then call submit again. The status goes back to IN_REVIEW and audit.submittedAt is updated and comments empties out. Resolved notes drop off the list as the reviewer closes them, so what you see is always what is still open. editableSections tells you which parts you may change: businessModel, productSelection, contacts and company.

Errors

Errors come back as a code and a list of field-level messages. Switch on code, and use errors[].field to point your own user at the right input.

{
  "code": "invalid_argument",
  "errors": [
    { "field": "company.registeredAddress.city", "message": "Must not be blank" }
  ]
}

The errors array can be empty — the ownership check that produces a 404 does not name a field.

HTTPCodeDescription
401Missing, expired or invalid access token, or a merchant credential used on a partner endpoint. Refresh the token and retry once.
400invalid_argumentA required field is missing or blank at initiate, or the applicationId in the path is not a UUID. Initiate names the offending field in errors[].field; a malformed applicationId leaves that field empty and puts the detail in the message.
400incomplete_applicationSubmit was called while requirements are outstanding. One entry per missing requirement key.
404not_foundThe application or merchant belongs to another partner, or it does not exist. Also returned when a merchantId in the path is not a valid UUID.
409invalid_statusA write or a submit was attempted while the application is not in DRAFT or CHANGES_REQUESTED.

Which endpoint returns what

EndpointErrors
GET /partner/api/whoami401
GET /partner/api/regions401
POST /partner/api/onboarding/initiate400 invalid_argument, 401, 500
GET /partner/api/onboarding/applications/{applicationId}400 invalid_argument, 404 not_found, 401
PATCH /partner/api/onboarding/applications/{applicationId}400 invalid_argument, 404 not_found, 409 invalid_status, 401
POST /partner/api/onboarding/applications/{applicationId}/submit400 incomplete_application, 400 invalid_argument, 404 not_found, 409 invalid_status, 401
GET /partner/api/merchants401
GET /partner/api/merchants/{merchantId}404 not_found, 401

404, never 403

An application or merchant that belongs to another partner answers 404, exactly as one that does not exist. You never get a 403, so a 404 is not proof that the record is missing. This is deliberate: it stops anyone from discovering ids by trying them. GET /partner/api/merchants is scoped to your partner account and is not affected.

Limits to know about

Four things are fixed by your first call. Everything else can be corrected as you go.

  • One application per merchantReference. The reference is the key. Calling initiate a second time with a reference you have already used returns the application you already have, along with isExistingApplication set to true. The product code you send on that second call is ignored. To onboard the same business for a second product, or for a separate set of regions, send a different merchantReference and you get a second merchant alongside the first.
  • One product per application. The product code is set at initiate and fixed from then on. Nothing later in the flow can change it.
  • Company details are always sent whole. Initiate requires them and PATCH can replace them, but neither accepts a partial company. Whichever call you use, send every required field together.
  • Merchant references are permanent. The reference you send at initiate stays with the merchant for good. Use an identifier that is already stable in your own system.