Graded Onboarding
Individual Customers Only · API (V2)
Harbor's Graded Onboarding (V2) provides a frictionless, step-by-step verification flow for individual customers. Instead of requiring a complete and comprehensive KYC submission in a single, all-or-nothing call, Graded Onboarding allows your customers to onboard and transact quickly with minimal initial data, and then sequentially upgrade to higher tiers when needed.
Required Step: Customer Creation FirstBefore calling any Graded Onboarding (V2) endpoints, you must first create an individual customer object with
type: "individual"viaPOST /api/v1/customers. This returns thecustomer_uuidused as the path parameter in all onboarding APIs. Please refer to our detailed Customer guide for creation request and response details.
Direct Level 2 Entry AllowedYou can onboard a customer directly into Level 2 in your initial submission. There is no requirement to start with Level 1 or onboard sequentially; if your customer has all documentation ready, you can submit Level 2 onboarding directly.
Level 3 cannot be applied for directly, and
kyc_level: 3is not an accepted value here. Level 3 is reached only by upgrading from an approved Level 2 — see Upgrade Tier.
Jump directly to the Level 1 onboarding JSON payload example for US residents.
Jump directly to the Level 2 onboarding JSON payload example for US residents.
Jump directly to the Level 2 onboarding JSON payload example for non-US residents.
1. (V1) vs. (V2) Comparison
Before integrating, determine which onboarding flow is right for your application:
| Feature | Standard Onboarding (V1) | Graded Onboarding (V2) |
|---|---|---|
| Target Customers | Business or Individual Customers | Individual Customers Only |
| Submission Model | All-or-nothing (Full KYC collected upfront) | Incremental (Level 1 → Level 2 → Level 3) |
| Typical Use Case | When full, standard individual or corporate verification is required at sign-up. | When you want to "start light" with a low friction sign-up process (Level 1 is US-only). |
Customer Scheme IsolationThis is an architectural choice, not a migration path. Customers onboarded on (V1) stay on the (V1) scheme; customers who start on (V2) remain on (V2). You can check which onboarding scheme a customer is assigned to by calling:
GET /api/v1/customers/{uuid}The response will contain either
"kyc_scheme": "v1"or"kyc_scheme": "v2".
Looking for Standard Onboarding (V1)?If your individual customer is using the Standard (V1) scheme, or you prefer a single-call full verification flow, please refer to the Onboard Individual via API guide instead.
2. Customer Levels & Limits
The Graded Onboarding flow assigns individual customers to one of three progressive levels (Level 1, Level 2, or Level 3). Each level has its own residential criteria, required document parts, transaction limits, and supported fiat payment methods.
Detailed Level & Limits GuideFor a comprehensive breakdown of each onboarding level's requirements, exact transaction limit rules, and restricted payment methods (such as the Level 1 trial allowance and Debit Card limitations), please refer to the Customer Level guide.
3. Setup, Endpoints, & Lookups
To avoid duplicate configurations, please refer to our standard integration guidelines:
-
Environments & Authentication: See API Keys for Base URLs, authentication headers (
X-API-KEY). For theX-Idempotency-Keyheader, see Idempotency. -
Customer Creation: Before calling any Graded Onboarding endpoint, you must first create an individual customer with
type: "individual". Please refer to our detailed Customer guide for request and response examples forPOST /api/v1/customers. -
Reference-Data Lookups: Some input fields (such as state codes, occupations, and identity document types) only accept dynamically validated options, and guessing them does not work. Call the lookup APIs documented in Onboarding Meta APIs:
GET /api/v2/customers/individual/occupationsGET /api/v2/customers/individual/countries/{country}/subdivisionsGET /api/v2/customers/individual/countries/{country}/identity_documents
The identity-document lookup also tells you which parts each document type requires. When the response lists
BACKfor the type you intend to send,identity_document.backbecomes mandatory.
Endpoints (V2)
All Graded Onboarding endpoints use the /api/v2 path prefix:
| Method | Path | Scope | Purpose |
|---|---|---|---|
POST | /api/v2/customers/{uuid}/individual/onboarding | CUSTOMER_CREATE | Initial onboarding submission (Level 1 or Level 2). |
GET | /api/v2/customers/{uuid}/individual/onboarding | CUSTOMER_READ | Poll current onboarding status & details. |
PATCH | /api/v2/customers/{uuid}/individual/onboarding | CUSTOMER_CREATE | Correct and resend a submission that was sent back. |
POST | /api/v2/customers/{uuid}/individual/onboarding/upgrade | CUSTOMER_CREATE | Submit a request to upgrade to a higher level. |
4. Submitting Initial Onboarding (POST)
Submit the initial onboarding payload for either Level 1 or Level 2.
4.1 Submitting Level 1 (US Residents Only)
curl -X POST "https://harbor-sandbox.owlpay.com/api/v2/customers/{{customer_uuid}}/individual/onboarding" \
-H "X-API-KEY: ***" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "X-Idempotency-Key: {{Idempotency-Key}}" \
-d '{
"kyc_level": 1,
"nationality": "US",
"residence": {
"country": "US",
"state": "WA"
}
}'Response: 202 Accepted with a status of processing.
4.2 Submitting Level 2 (US Resident)
{
"kyc_level": 2,
"nationality": "US",
"residence": {
"country": "US",
"state": "WA",
"street": "200 Pine St",
"sub_street": "Apt 5",
"city": "Seattle",
"postal_code": "98101"
},
"occupation": "legislators_and_senior_officials",
"purpose_of_use": ["payments"],
"ssn": "123456789",
"phone_country_code": "US",
"phone_number": "+14045550101",
"identity_document": {
"type": "PASSPORT",
"country": "US",
"front": "<base64_bytes>"
}
}Notes on this payload
phone_country_codeandphone_numberare required only when the customer record does not already carry a phone number. A phone number is mandatory for US residents, so if neither the request nor the customer record has one, the request is rejected.residence.sub_streetis genuinely required at Level 2 — an empty string does not satisfy it.
4.3 Submitting Level 2 (Non-US Resident)
{
"kyc_level": 2,
"nationality": "GB",
"residence": {
"country": "GB",
"street": "10 High St",
"sub_street": "Flat 2",
"city": "London",
"postal_code": "SW1A1AA"
},
"occupation": "software_and_applications_developers_and_analysts",
"purpose_of_use": ["payments"],
"tax_id": "AB123456C",
"identity_document": {
"type": "PASSPORT",
"country": "GB",
"front": "<base64_bytes>"
}
}Notes on this payload
residence.statemust be omitted outside the US. It is rejected, not merely optional.- Send
tax_id, notssn. Sendingssnfor a non-US residence is rejected — see Customer Level for the full tax-identifier rules.
Accepted Values for Fixed-Choice Fields
purpose_of_use(array, at least one value) —investmentTrading,savingsHolding,payments,salaryFunds,businessPayments,remittancesSupport,other. There is no lookup endpoint for this list; the rejection message names the accepted values.
identity_document.type—NATIONAL_ID,PASSPORT,DRIVER_LICENCE,RESIDENCE_PERMIT. Note the British spelling ofDRIVER_LICENCE. The types actually accepted for a given issuing country are narrower — read them fromGET /api/v2/customers/individual/countries/{country}/identity_documents.
occupation— slugs only, fromGET /api/v2/customers/individual/occupations. An unrecognised slug is always rejected.
Fields From The (V1) Contract Are Rejected, Not IgnoredThe graded contract does not collect
source_of_wealth,source_of_wealth_other,proof_of_address,bank_statement, orhas_us_bank_account, and it does not accept the (V1)individualwrapper or anapplicantkey. Sending any of them returns 422 rather than silently dropping the data — for KYC input, a rejection is safer than a discarded field. Usekyc_levelfor the level number (level,tier, andverification_tierare rejected).
5. Polling Status & Lifecycle
You can check the onboarding progress by polling the GET endpoint or subscribing to webhooks.
5.1 Onboarding Statuses
The response JSON shape contains a top-level status:
{
"data": {
"status": "processing",
"kyc_level": null,
"next_level": null,
"transfers_blocked": true,
"rfi_link": null,
"pending_requirements": []
}
}kyc_level is the approved level, not the level applied for — it stays null until a reviewer approves one. next_level is the level the upgrade endpoint would accept right now, and is null while a review is in flight or an advanced-verification application is already on file.
| Status | Meaning | Developer Action |
|---|---|---|
processing | Data is being validated. | Poll or wait for webhook. |
action_required | Something must be fixed and resent. Covers both a request for more information mid-review and a correctable rejection. Check pending_requirements for details. | Prompt the user to correct the input and send PATCH. |
submitted | Sent for review; awaiting the verdict. | Wait. |
verified | Selected level is successfully approved. | Customer is active at this level. |
declined | Final. A verdict was issued on the person. No resubmission of any kind is accepted. | Stop. Do not resubmit — every further attempt returns 409 (declined_is_final, code 2312). |
failed | Systemic or document processing error. | Resend with PATCH on the same path. |
action_requiredvs.declinedA review ends in one of two ways, and the distinction decides what your integration should do next:
- A correctable rejection converges to
action_required. The customer may fix the data and resend with PATCH.- A declinature converges to
declined. This is final: the customer record is marked DECLINED, and any subsequent onboarding or upgrade attempt returns 409 Conflict with code2312(declined_is_final).There is no
rejectedstatus on this API. If you integrated against an earlier version of this page that listed one, branch onaction_requiredanddeclinedinstead.
5.2 Webhooks
We strongly recommend subscribing to the following webhook events to track real-time onboarding state transitions:
customer_onboarding.submittedcustomer_onboarding.action_requiredcustomer_onboarding.verifiedcustomer_onboarding.declinedcustomer_onboarding.failed
The event prefix is customer_onboarding. with an underscore — it is deliberately distinct from the customer.* events. There is no customer_onboarding.rejected event; a correctable rejection is announced as customer_onboarding.action_required, and a final one as customer_onboarding.declined.
For setup, see Webhook Subscription.
6. Fixing Validation Errors (PATCH)
When a submission has been sent back — status action_required, or failed after a processing error — correct the data and resubmit using a PATCH request.
curl -X PATCH "https://harbor-sandbox.owlpay.com/api/v2/customers/{{customer_uuid}}/individual/onboarding" \
-H "X-API-KEY: ***" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "X-Idempotency-Key: {{Idempotency-Key}}" \
-d '{
"kyc_level": 2,
"nationality": "US",
"residence": {
"country": "US",
"state": "WA",
"street": "200 Pine St (Corrected)",
"sub_street": "Apt 5",
"city": "Seattle",
"postal_code": "98101"
},
"occupation": "legislators_and_senior_officials",
"purpose_of_use": ["payments"],
"ssn": "123456789",
"identity_document": {
"type": "PASSPORT",
"country": "US",
"front": "<base64_bytes>"
}
}'
Critical Rule for PATCHThe PATCH payload replaces the stored onboarding data entirely. Therefore, you must resend all fields (including un-changed fields and base64 files), not just the fields being corrected.
Note: You cannot change the target
kyc_levelusing PATCH; it is locked to the level of the initial submission. A different level is a different application, not a correction — use the upgrade endpoint instead.
Once An Onboarding Exists, PATCH Is The Only Way To ResendA second POST for the same customer is refused. Which conflict you receive depends on where the existing submission stands:
Existing status POST returns processing409 — code 2319(submission_processing)action_required409 — code 2318(action_required_in_flight)submitted409 — code 2302(already_active)verifiedorfailed, where a verification record already exists409 — code 2311(must_update_existing)any status, once the customer is declined 409 — code 2312(declined_is_final)Sending PATCH while the submission is still
processingorsubmittedis likewise refused, with 409 code2305(update_not_allowed_while_processing) — wait for the review to resolve first.
Updated about 18 hours ago