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 a
merchantIdand it carries your ownreference, 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
Create the merchant and the application, with the company details
POST /partner/api/onboarding/initiate
status becomes
DRAFT - 2
Fill in the regions and what the merchant sells
PATCH /partner/api/onboarding/applications/{applicationId}
- 3
Submit for review
POST /partner/api/onboarding/applications/{applicationId}/submit
status becomes
IN_REVIEW
Outpost
- 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 requirements.errors, PATCH the application, then submit again.
REJECTED Final. This outcome stands.
The flow
- 1 Create 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 Fill in the application. PATCH /partner/api/onboarding/applications/{applicationId} with the regions, the trading description, the store URLs and the category. One call covers all of it, and you can repeat it as often as you need. This is the merchant’s business model, not its legal details. Those were sent at initiate.
- 3 Submit for review. POST /partner/api/onboarding/applications/{applicationId}/submit. Outpost reviews the application.
- 4 Poll for the outcome. GET /partner/api/onboarding/applications/{applicationId} and read status, audit.reviewMessage and requirements.errors.
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" | jqCall 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
/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
/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.
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string (uuid) | The internal region id. It is a random UUID and differs between environments, so look it up rather than hard-coding it. | |
| name | string | Human-readable name, for example "United Kingdom" or "California". | |
| flagCode | string | Short code, for example EU, GB or CA. Not unique on its own. CA is both Canada’s neighbour California and, in another row, Canada. | |
| countryCode | string | ISO 3166-1 alpha-2 country the region sits in. Absent for regions that are not a single country, such as the European Union. | |
| taxSupported | boolean | Whether Tax of Record is available in this region. | |
| morSupported | boolean | Whether Merchant of Record is available in this region. |
200 OK
[
{
"id": "5f4e3d2c-1b0a-4c9d-8e7f-6a5b4c3d2e1f",
"name": "European Union",
"flagCode": "EU",
"taxSupported": true,
"morSupported": true
},
{
"id": "2a9c8b7d-6e5f-4a3b-9c8d-7e6f5a4b3c2d",
"name": "United Kingdom",
"flagCode": "GB",
"countryCode": "GB",
"taxSupported": 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
/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
| Parameter | Type | Required | Description |
|---|---|---|---|
| merchantReference | string | Yes | Your own identifier for this merchant. Unique per partner, and permanent. Calling initiate again with the same reference returns the existing application. |
| productCode | string | Yes | tor or mor. Fixed once set. The application keeps this product. |
| mode | string enum | For this flow | HOSTED 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. |
| company | object | Yes | Legal entity details, broken out below. This is the only endpoint that writes them. |
| returnUrl | string | No | Where the merchant returns after a hosted flow. Ignored in API mode. |
| complianceSummary | object | No | Checks you have already run, broken out below. Stored and shown to the Outpost reviewer. |
| merchantContext | object | No | Free-form string map carried through to the Outpost reviewer. |
company
Required, and written only here, so send it correctly the first time. The rules are the same in both modes.
| Parameter | Type | Required | Description |
|---|---|---|---|
| company.legalName | string | Yes | Registered legal name. |
| company.tradingName | string | No | Trading or brand name, if it differs from the legal name. |
| company.website | string | Yes | The business's website. |
| company.taxId | string | Yes | Tax identifier in the jurisdiction of registration. |
| company.registrationNumber | string | Yes | Company registration number. |
| company.jurisdiction | string | Yes | Country of incorporation, ISO 3166-1 alpha-2. |
| company.gmvTier | string enum | No | Expected 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.line1 | string | Yes | Primary address line. |
| company.registeredAddress.line2 | string | No | Secondary address line. |
| company.registeredAddress.city | string | Yes | City. |
| company.registeredAddress.state | string | No | State or province. |
| company.registeredAddress.postalCode | string | Yes | Postal code. |
| company.registeredAddress.country | string | Yes | ISO 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.
| Parameter | Type | Required | Description |
|---|---|---|---|
| complianceSummary.verificationStatus | string | No | Free text. Your own verification outcome for this business. |
| complianceSummary.riskLevel | string | No | Free text. Your own risk rating. |
| complianceSummary.countryOfIncorporation | string | No | ISO 3166-1 alpha-2 country code. |
| complianceSummary.hasOpenComplianceFlags | boolean | No | Whether 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"
}| Parameter | Type | Required | Description |
|---|---|---|---|
| applicationId | string (uuid) | Identifier for the application. Use it for the PATCH that fills the application in. | |
| merchantId | string (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. | |
| status | string | Application status. DRAFT on creation. These are the raw internal values, not the lowercase ones the merchant-scoped endpoints return. | |
| mode | string | HOSTED or API, as stored. | |
| isExistingApplication | boolean | true when an application already existed for this merchantReference. Company details in your request were discarded. | |
| onboardingUrl | string | Hosted link for the merchant. In API mode this is returned without a token and is not usable in this mode, so ignore it. |
Errors
| HTTP | Code | Description |
|---|---|---|
| 400 | invalid_argument | A required field is missing or blank. Each missing field is named, for example company.registeredAddress.city. |
| 500 | internal_error | The 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. |
| 401 | — | Invalid 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
/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": ["EU"],
"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"
}
},
"requirements": {},
"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"
}
}| Parameter | Type | Required | Description |
|---|---|---|---|
| applicationId | string (uuid) | The application you addressed. | |
| merchantId | string (uuid) | The Outpost merchant this application belongs to. Store it for the merchant-scoped endpoints, such as the tax APIs. | |
| status | string | One of DRAFT, IN_REVIEW, CHANGES_REQUESTED, APPROVED, SIGNED_BY_MERCHANT, SIGNED_BY_OUTPOST, REJECTED. See the status lifecycle below. | |
| mode | string | HOSTED or API, as stored. | |
| partnerId | string | Your partner identifier. | |
| partnerMerchantReference | string | The merchantReference you sent at initiate, echoed back. Your own reference, not to be confused with merchantId above, which is the Outpost merchant UUID. | |
| productCode | string | The productCode you sent at initiate, echoed back under the same name. | |
| company | object | The company object as stored. See OnboardingCompany at initiate. | |
| complianceSummary | object | The compliance summary you sent at initiate, when you sent one. | |
| merchantContext | object | The metadata you sent at initiate, when you sent any. | |
| selectedRegions | string[] | The regions currently on the application. | |
| businessDescription | string | What you last sent as businessDescription. | |
| storeUrls | string[] | What you last sent as storeUrls. | |
| legalRepresentative | object | The 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. | |
| contacts | object | financeContact and technicalContact, each with fullName, email and role. Absent until at least one of them is set. | |
| requirements | object | What still blocks submit, and any reviewer comments. See the fields below. Serialised as an empty object when there is nothing to report. | |
| requiredFields | string[] | 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. For what is still outstanding, read requirements.currentlyDue. | |
| audit | object | createdAt and updatedAt, plus submittedAt, reviewedAt and reviewMessage once they exist. |
Fields holding their default value are left out of the response, so an empty requirements object means nothing is due and there are no reviewer comments.
requirements
| Parameter | Type | Required | Description |
|---|---|---|---|
| requirements.currentlyDue | string[] | Requirement keys you still have to satisfy before you can submit: products, selectedRegions, businessDescription, checkoutUrl, legalRepresentative. Omitted when nothing is due. | |
| requirements.errors | object[] | Reviewer comments, each with field and message. Only filled while the status is CHANGES_REQUESTED or REJECTED. Omitted otherwise. |
Errors
| HTTP | Code | Description |
|---|---|---|
| 400 | invalid_argument | The applicationId in the path is not a UUID. |
| 404 | not_found | No application with that id belongs to your partner account. You never get a 403. |
| 401 | — | Invalid or missing Authorization token. |
Fill in the application
/partner/api/onboarding/applications/{applicationId} One call sets everything the application still needs: the regions, what the merchant sells, its storefront URLs, its category and the people to contact. The legal details are separate. Legal name, tax id, registration number and registered address were sent once at initiate and stay as sent.
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. selectedRegions, legalRepresentative and
contacts replace what is there
rather than merging into it, so send each of them whole.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
| selectedRegions | string[] | No | Where the merchant wants the product. Sending it attaches the product code from initiate, which clears the products requirement. Send at least one region: the values are not checked and an empty list is stored as sent. See Requirements. Replaces the whole list rather than merging. |
| businessDescription | string | No | What the merchant sells. Satisfies the businessDescription requirement. |
| storeUrls | string[] | No | The merchant’s storefront or checkout URLs. Satisfies the checkoutUrl requirement. |
| category | string | No | Free-text business category. Not part of any requirement today. |
| legalRepresentative | object | No | The 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.fullName | string | No | Full name. Needed, together with email, to clear the requirement. |
| legalRepresentative.email | string | No | Email address. Checked on the way in: a malformed address is rejected with 400 invalid_argument and nothing is stored. |
| legalRepresentative.role | string | No | Job title, for example Managing Director. Free text and not part of any requirement. |
| contacts | object | No | financeContact 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.financeContact | object | No | Who to contact about tax filings and invoices. Same fullName, email and role fields, all optional. The email is checked the same way. |
| contacts.technicalContact | object | No | Who to contact about the integration. Same fullName, email and role fields, all optional. The email is checked the same way. |
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": ["EU"],
"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": ["EU"],
"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"
}
},
"requirements": {},
"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. Read requirements.currentlyDue on the way back to see what still blocks submit.
Errors
| HTTP | Code | Description |
|---|---|---|
| 400 | invalid_argument | The 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. |
| 404 | not_found | No application with that id belongs to your partner account. You never get a 403. |
| 401 | — | Invalid or missing Authorization token. |
| 409 | invalid_status | The application is not in DRAFT or CHANGES_REQUESTED, so it can no longer be edited. |
Submit for review
/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.
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": ["EU"],
"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"
}
},
"requirements": {},
"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
| HTTP | Code | Description |
|---|---|---|
| 400 | incomplete_application | Something in requirements.currentlyDue is still outstanding. The body names each missing key. |
| 400 | invalid_argument | The applicationId in the path is not a UUID. |
| 404 | not_found | No application with that id belongs to your partner account. |
| 409 | invalid_status | Already submitted, or past the point where it can be submitted again. |
| 401 | — | Invalid 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
/partner/api/merchants
Returns every merchant linked to your partner account, as an array of id and reference. There
is no pagination and no filter: the whole list comes back in one response. 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
/partner/api/merchants/{merchantId} | Parameter | Type | Required | Description |
|---|---|---|---|
| id | string (uuid) | The Outpost merchant id. Use it in every other path on this page. | |
| reference | string | The merchantReference you sent at initiate. | |
| company | object | The company object you sent at initiate. | |
| businessDescription | string | What you last sent as businessDescription on the application. | |
| storeUrls | string[] | 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 and
storeUrls appear once you
have sent them on the application, so a merchant you have just created returns
id,
reference and
company alone.
Requirements
requirements.currentlyDue lists
what is still missing before the application can be submitted. Read it after every write
and stop calling submit until the list is gone.
{
"merchantId": "9b1f4c8e-6d2a-4f7b-8e3c-1a5d0f2b7c94",
"status": "created",
"products": [
{ "code": "tor", "regions": ["EU"] }
],
"requirements": {
"currentlyDue": ["businessDescription", "checkoutUrl", "legalRepresentative"]
}
}The keys and what satisfies them
| Requirement key | Endpoint | Request field | Notes |
|---|---|---|---|
| products | PATCH /onboarding/applications/{applicationId} | selectedRegions | Initiate stores the product code but does not attach the product itself, so this is due until your first PATCH. Sending selectedRegions attaches the product from initiate and clears it. |
| selectedRegions | PATCH /onboarding/applications/{applicationId} | selectedRegions | The same field clears this one, as soon as the list holds at least one value. An empty list is accepted and stored, so send a real region. |
| businessDescription | PATCH /onboarding/applications/{applicationId} | businessDescription | Satisfied by any non-blank string. |
| checkoutUrl | PATCH /onboarding/applications/{applicationId} | storeUrls | The requirement key is checkoutUrl and the request field is storeUrls. Send a storeUrls array with at least one entry. |
| legalRepresentative | PATCH /onboarding/applications/{applicationId} | legalRepresentative | Send 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, so once the product is attached the list is empty. The products requirement still applies either way, so mor still needs one PATCH with selectedRegions before it can be submitted.
Submit still checks status and ownership. 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.
Open question: what to put in regions
selectedRegions is a list
of free-form strings. The only check is that the list is not empty. Nothing compares the
values against GET /partner/api/regions, so a typo is accepted, stored and echoed back to you unchanged.
Outpost’s own tests for this API send short codes that match the flagCode field — for example "EU",
"GB", "DE". Other parts of the platform put the region id UUID in the same field. Both are accepted because neither is checked.
This is a known gap, not a documented rule. Agree the exact values with your Outpost contact before you go live, and keep them in one place in your code so they are easy to change.
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.
| Status | What it means |
|---|---|
| DRAFT | The application exists and you can still change it. This is the status right after initiate. |
| IN_REVIEW | You have submitted it and Outpost is reviewing. The application is read-only. |
| CHANGES_REQUESTED | A reviewer wants something changed. You can edit again, then submit again. |
| APPROVED | Outpost accepted the application. Outpost can still move it back to CHANGES_REQUESTED. |
| SIGNED_BY_MERCHANT | The merchant has signed the Outpost agreement in the Outpost merchant dashboard. |
| SIGNED_BY_OUTPOST | Outpost has counter-signed. The merchant is onboarded. |
| REJECTED | Outpost declined the application. This outcome stands. |
What each call does in each status
| Status | PATCH | POST /submit |
|---|---|---|
| DRAFT | 200 | 200, or 400 if requirements are outstanding |
| IN_REVIEW | 409 invalid_status | 409 invalid_status |
| CHANGES_REQUESTED | 200 | 200, or 400 if requirements are outstanding |
| APPROVED | 409 invalid_status | 409 invalid_status |
| SIGNED_BY_MERCHANT | 409 invalid_status | 409 invalid_status |
| SIGNED_BY_OUTPOST | 409 invalid_status | 409 invalid_status |
| REJECTED | 409 invalid_status | 409 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 to
CHANGES_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_REVIEWmeans 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. -
requirements.errors— the specific problems, one entry per open reviewer comment. Only filled while the status isCHANGES_REQUESTEDorREJECTED.
Handling CHANGES_REQUESTED
{
"applicationId": "3f8c1e07-5a44-4b91-9d2e-77a0c6b41e58",
"status": "CHANGES_REQUESTED",
"mode": "API",
"productCode": "tor",
"selectedRegions": ["EU"],
"requirements": {
"errors": [
{
"field": "businessDescription",
"message": "Describe what the merchant sells, not just the industry."
},
{
"field": "BUSINESS_MODEL",
"message": "The store URL returns a holding page."
}
]
},
"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."
}
}
Each entry in requirements.errors has a field and a message. When the reviewer commented on one field, field is that field’s name, such as businessDescription. When the comment is about a whole section, it is the section name in upper case,
such as BUSINESS_MODEL.
Treat the value as a label to display, not as a key to switch on.
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. Comments a reviewer has resolved drop out of the list, so an empty errors list means nothing is outstanding.
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.
| HTTP | Code | Description |
|---|---|---|
| 401 | — | Missing, expired or invalid access token, or a merchant credential used on a partner endpoint. Refresh the token and retry once. |
| 400 | invalid_argument | A 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. |
| 400 | incomplete_application | Submit was called while requirements are outstanding. One entry per missing requirement key. |
| 404 | not_found | The 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. |
| 409 | invalid_status | A write or a submit was attempted while the application is not in DRAFT or CHANGES_REQUESTED. |
Which endpoint returns what
| Endpoint | Errors |
|---|---|
| GET /partner/api/whoami | 401 |
| GET /partner/api/regions | 401 |
| POST /partner/api/onboarding/initiate | 400 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}/submit | 400 incomplete_application, 400 invalid_argument, 404 not_found, 409 invalid_status, 401 |
| GET /partner/api/merchants | 401 |
| 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 written once, at initiate. POST /partner/api/onboarding/initiate with mode set to API is the one endpoint that writes them, so send them correctly the first time.
- 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.