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 application, over as many calls as you like
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.
APPROVEDThe merchant is live for that product. Nothing more to do.
CHANGES_REQUESTEDRead audit.reviewMessage and comments, PATCH the application, then submit again.
REJECTEDFinal. This outcome stands.
The flow
- 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.
- 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.
- 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.
- 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" | 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/whoamiReturns 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/regionsReturns 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. | |
| torSupported | 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",
"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
/partner/api/onboarding/initiateThis 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. Required here, and correctable later with PATCH. |
| 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 here, with the same rules in both modes. If something changes or arrives wrong, PATCH replaces the whole object under the same rules.
| 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_1BcomplianceSummary
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": ["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"
}
}| 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 region ids 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. | |
| comments | object[] | 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. | |
| editableSections | string[] | Which sections you may still change: businessModel, productSelection, contacts and company. The set is the same on every response. | |
| 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. To learn what is still outstanding, call submit and read the 400. | |
| audit | object | createdAt 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
| Parameter | Type | Required | Description |
|---|---|---|---|
| comments[].id | string | Identifier for the comment. Use it to avoid showing the same note twice. | |
| comments[].target.sectionKey | string | The section the reviewer commented on: company, contacts, businessModel or productSelection. | |
| comments[].target.fieldKey | string | The specific field, when the comment is about one. Absent when the comment is about the whole section. | |
| comments[].message | string | What the reviewer wrote, meant to be read by a person. | |
| comments[].createdAt | string (ISO 8601) | When the comment was left. |
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}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
| Parameter | Type | Required | Description |
|---|---|---|---|
| selectedRegions | string[] | No | The 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. |
| 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. |
| company | object | No | Corrects 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
| 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}/submitHands 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
- 1Ownership. The application belongs to your partner account. Otherwise
404 not_found. - 2Status. It is
DRAFTorCHANGES_REQUESTED. Otherwise409 invalid_status. - 3Completeness. Every required field holds a usable value. Otherwise
400 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
| HTTP | Code | Description |
|---|---|---|
| 400 | incomplete_application | A required field is still missing. The body names every outstanding key at once. |
| 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/merchantsReturns 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
/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 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 key | Endpoint | Request field | Notes |
|---|---|---|---|
| selectedRegions | PATCH /onboarding/applications/{applicationId} | selectedRegions | The regions the merchant wants the product enabled in. Send them before you submit, as the ids returned by GET /partner/api/regions. |
| 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. |
| company.* | POST /onboarding/initiate, or PATCH to correct it | company | One 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. |
| 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 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.
| 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 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_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.comments— the specific problems, one entry per open reviewer note, each pointing at the section and field it concerns. 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"],
"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.
| 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 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.