> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dakota.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Customer Onboarding

Before processing payments, customers must complete KYB (Know Your Business) onboarding. This guide explains how to integrate Dakota Platform's onboarding flow.

<Info>
  **Looking for detailed onboarding guides?** Check out our support articles for step-by-step walkthroughs:

  * [Onboarding your business account customers](https://support-platform.dakota.xyz/articles/2551929093-onboarding-your-business-account-cust)
  * [Onboarding your individual account customers](https://support-platform.dakota.xyz/articles/4406934078-onboarding-your-individual-account-customers)
  * [Platform Client Onboarding - Information & Documentation](https://support-platform.dakota.xyz/articles/2518037375-platform-client-onboarding)
</Info>

# Overview

The onboarding process involves:

1. Creating a customer record (KYB automatically initiated)
2. Redirecting customer to complete verification
3. Monitoring KYB status via API or webhooks

# Step 1: Create a Customer

First, create a customer record with basic business information.

## Customer Creation Fields

| Field           | Type   | Required | Description                                                      | Example       |
| --------------- | ------ | -------- | ---------------------------------------------------------------- | ------------- |
| `name`          | string | ✅        | Legal name of the business or individual                         | `"Acme Corp"` |
| `customer_type` | string | ✅        | Type of customer account. Must be `"business"` or `"individual"` | `"business"`  |
| `external_id`   | string | ❌        | Your internal identifier for this customer                       | `"acme_001"`  |

## Request Example

```bash cURL theme={null}
curl -X POST https://api.platform.dakota.xyz/customers \
  -H "X-API-Key: your-api-key" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Corp",
    "customer_type": "business"
  }'
```

```javascript JavaScript theme={null}
const response = await fetch('https://api.platform.dakota.xyz/customers', {
  method: 'POST',
  headers: {
    'X-API-Key': 'your-api-key',
    'X-Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'Acme Corp',
    customer_type: 'business'
  })
});

const customer = await response.json();
console.log('Created customer:', customer.data.id);
```

```python Python theme={null}
import requests
import uuid

response = requests.post(
    'https://api.platform.dakota.xyz/customers',
    headers={
        'X-API-Key': 'your-api-key',
        'X-Idempotency-Key': str(uuid.uuid4()),
        'Content-Type': 'application/json'
    },
    json={
        'name': 'Acme Corp',
        'customer_type': 'business'
    }
)

customer = response.json()
print(f'Created customer: {customer["data"]["id"]}')
```

```go Go theme={null}
package main

import (
    "bytes"
    "net/http"
    "github.com/google/uuid"
)

func main() {
    client := &http.Client{}
    body := bytes.NewBufferString(`{
        "name": "Acme Corp",
        "customer_type": "business"
    }`)
    
    req, _ := http.NewRequest("POST", "https://api.platform.dakota.xyz/customers", body)
    
    req.Header.Add("X-API-Key", "your-api-key")
    req.Header.Add("X-Idempotency-Key", uuid.New().String())
    req.Header.Add("Content-Type", "application/json")
    
    resp, _ := client.Do(req)
    defer resp.Body.Close()
}
```

```rust Rust theme={null}
use reqwest::header::{HeaderMap, HeaderValue};
use uuid::Uuid;
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = reqwest::Client::new();
    
    let mut headers = HeaderMap::new();
    headers.insert("X-API-Key", HeaderValue::from_static("your-api-key"));
    headers.insert("X-Idempotency-Key", HeaderValue::from_str(&Uuid::new_v4().to_string())?);
    headers.insert("Content-Type", HeaderValue::from_static("application/json"));
    
    let body = json!({
        "name": "Acme Corp",
        "customer_type": "business"
    });
    
    let response = client
        .post("https://api.platform.dakota.xyz/customers")
        .headers(headers)
        .json(&body)
        .send()
        .await?;
    
    Ok(())
}
```

```java Java theme={null}
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.util.UUID;

public class DakotaCustomerExample {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        
        String body = """
            {
              "name": "Acme Corp",
              "customer_type": "business"
            }
            """;
        
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.platform.dakota.xyz/customers"))
            .header("X-API-Key", "your-api-key")
            .header("X-Idempotency-Key", UUID.randomUUID().toString())
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();
            
        HttpResponse<String> response = client.send(request, 
            HttpResponse.BodyHandlers.ofString());
    }
}
```

Response:

```json theme={null}
{
  "id": "2Nq8G7xK9mR4Ls6DyJ1Uf3Tp",
  "kyb_links": [],
  "application_id": "2WGC9cKv9P4K8eGzqY6qJ3Xz7Qm",
  "application_url": "https://apply.dakota.com/applications/2WGC9cKv9P4K8eGzqY6qJ3Xz7Qm?token=kJ8xN3zQ9mL2pR5vY7wC1aF4dG6hK0sT8uW3nB5eM9",
  "application_expires_at": 1734567890000000000
}
```

# Step 2: Application Process (Automatic)

An onboarding application is automatically created when you create a customer. The response includes an `application_url` that you can use to redirect your customer to Dakota's hosted onboarding form.

## Automatic Application Creation

When you create a customer, the system automatically:

* Creates an onboarding application
* Generates a secure `application_url` with an embedded token
* Sets the initial application status to `"pending"`

## Response Fields

The customer creation response includes:

| Field                    | Description                                                     | Example                                                 |
| ------------------------ | --------------------------------------------------------------- | ------------------------------------------------------- |
| `id`                     | Unique identifier for the customer                              | `"2Nq8G7xK9mR4Ls6DyJ1Uf3Tp"`                            |
| `kyb_links`              | Array of KYB provider links (if any)                            | `[]`                                                    |
| `application_id`         | Unique identifier for the onboarding application                | `"2WGC9cKv9P4K8eGzqY6qJ3Xz7Qm"`                         |
| `application_url`        | Dakota-hosted onboarding URL with embedded auth token           | `"https://apply.dakota.com/applications/...?token=..."` |
| `application_expires_at` | Unix timestamp (nanoseconds) when the application token expires | `1734567890000000000`                                   |

## Using the Application URL

From the customer creation response, extract the onboarding URL:

```javascript theme={null}
const customer = await response.json();
const onboardingUrl = customer.application_url;

// Redirect customer to complete onboarding
window.location.href = onboardingUrl;
```

# Step 3: Handle the Onboarding Flow

Direct your customer to the `application_url` to complete verification using Dakota's hosted onboarding form. The customer will:

1. Provide business information and documentation
2. Add beneficial owners and control persons
3. Upload required documents (ID, proof of address, etc.)
4. Accept legal agreements (Terms of Service, Privacy Policy, etc.)
5. Submit the application for review
6. Receive approval or requests for additional information

## Proof of Address for Individual Customers

For **individual** customer onboarding, Proof of Address (PoA) is **not strictly required to complete onboarding**. An individual can submit identity documents alone and reach `application_status: submitted` without uploading PoA.

However, PoA is required for a customer to transact more than **\$3,000 in any rolling 7-day window**. Customers who cross that threshold without an approved PoA on file are frozen — inbound transactions over the limit are held in compliance review until they provide one. Encourage customers to upload PoA at onboarding when possible; the field is presented with a note explaining the threshold rather than as a low-priority optional step.

### PoA State

The application-level `poa_status` field on `GET /applications/{id}` exposes the current review state for individual customers:

| `poa_status`               | Meaning                                                                           |
| -------------------------- | --------------------------------------------------------------------------------- |
| `missing`                  | No PoA on file. Inbound transactions over the rolling-7-day threshold are paused. |
| `submitted_pending_review` | PoA uploaded; awaiting compliance review.                                         |
| `approved`                 | PoA approved. Customer is exempt from the threshold gate.                         |
| `rejected`                 | Compliance rejected the PoA. The customer must upload a new document.             |
| `null`                     | Field not applicable (business application).                                      |

### Three Ways to Submit PoA After Onboarding

If a customer didn't upload PoA at onboarding but later needs to:

1. **Client API call.** Use the standard document-upload endpoint:
   ```
   POST /applications/{applicationId}/associated-individuals/{individualId}/documents
   ```
   Document type can be `proof_of_address`, `bank_statement`, or `utility_bill` — all three are accepted as PoA-equivalent.

2. **Customer self-serve via the original onboarding link.** The link returned in `application_url` remains valid for 90 days after creation. When a previously-approved individual reopens it, the form locks every already-approved field and exposes only the PoA upload, so the customer can submit a PoA without re-attesting or re-entering personal data.

3. **Compliance manual upload.** A reviewer with the `compliance_documents:manage` permission can upload PoA on the customer's behalf from the compliance dashboard.

Any of these paths transitions the application from `completed` to `compliance_review` (or to `submitted_pending_review` at the PoA level if the app hasn't yet been decided) and emits a `customer.kyb_status.updated` webhook with `reason_code: pending_proof_of_address`. Once compliance reviews, the customer receives a follow-up event with `reason_code: proof_of_address_approved` or `proof_of_address_rejected`.

See [State lifecycles → Proof of Address State](/api-reference/state-lifecycles#proof-of-address-state-individuals) for the full set of transitions, and [Webhooks → Customer Events](/documentation/webhooks#customer-events) for the corresponding event payloads.

## Legal Agreements & Attestations

<Warning>
  **Compliance requirement:** Before an application can be submitted, your customer must accept Dakota's legal agreements. If you are building a custom onboarding flow using the API (rather than the hosted form), you are responsible for presenting these documents to your end users and collecting their consent before submitting attestations.
</Warning>

During onboarding, the following legal agreements must be presented and accepted:

| Attestation Type           | Document                                                                   | Description                                                       |
| -------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `e_sign`                   | [E-Sign & Electronic Communications Notice](https://www.dakota.xyz/e-sign) | Consent to use electronic signatures — **must be accepted first** |
| `terms_of_service`         | [Terms of Service](https://www.dakota.xyz/terms)                           | Dakota's terms of service                                         |
| `privacy_policy`           | [Privacy Policy](https://www.dakota.xyz/privacy)                           | Dakota's privacy policy                                           |
| `funds_transfer_agreement` | [Funds Transfer Agreement](https://www.dakota.xyz/funds-transfer)          | Agreement governing fund transfers                                |
| `lead_bank_privacy_policy` | [Banking partner privacy policy](https://www.lead.bank/privacy-and-terms)  | Banking partner's privacy policy                                  |
| `information_accuracy`     | —                                                                          | Attestation that all provided information is accurate             |

### Submitting Attestations via API

If you are using the API to build a custom onboarding experience, you must submit attestations using the [Submit Attestation](/api-reference/onboarding/submit-an-attestation-for-an-application) endpoint. The `e_sign` attestation **must be submitted first** — all other attestations will be rejected until e-sign consent is recorded.

```bash cURL theme={null}
# Step 1: Submit e_sign first (capture timestamp)
E_SIGN_TIMESTAMP=$(date +%s)

curl -s -X POST "https://api.platform.dakota.xyz/applications/${APPLICATION_ID}/attestations" \
  -H "X-Application-Token: ${APPLICATION_TOKEN}" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d "{
    \"attestation_type\": \"e_sign\",
    \"timestamp\": ${E_SIGN_TIMESTAMP},
    \"applicant_id\": \"${APPLICANT_ID}\"
  }"

# Step 2: Wait to ensure a different timestamp, then submit remaining attestations
sleep 1
OTHER_TIMESTAMP=$(date +%s)

for TYPE in information_accuracy terms_of_service privacy_policy funds_transfer_agreement lead_bank_privacy_policy; do
  curl -s -X POST "https://api.platform.dakota.xyz/applications/${APPLICATION_ID}/attestations" \
    -H "X-Application-Token: ${APPLICATION_TOKEN}" \
    -H "X-Idempotency-Key: $(uuidgen)" \
    -H "Content-Type: application/json" \
    -d "{
      \"attestation_type\": \"${TYPE}\",
      \"timestamp\": ${OTHER_TIMESTAMP},
      \"applicant_id\": \"${APPLICANT_ID}\"
    }"
done
```

```javascript JavaScript theme={null}
// Step 1: Submit e_sign first
const eSignTimestamp = Math.floor(Date.now() / 1000);

await fetch(`https://api.platform.dakota.xyz/applications/${applicationId}/attestations`, {
  method: 'POST',
  headers: {
    'X-Application-Token': applicationToken,
    'X-Idempotency-Key': crypto.randomUUID(),
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    attestation_type: 'e_sign',
    timestamp: eSignTimestamp,
    applicant_id: applicantId
  })
});

// Step 2: Submit remaining attestations with a later timestamp
const otherTimestamp = eSignTimestamp + 1;
const otherTypes = [
  'information_accuracy',
  'terms_of_service',
  'privacy_policy',
  'funds_transfer_agreement',
  'lead_bank_privacy_policy'
];

for (const type of otherTypes) {
  await fetch(`https://api.platform.dakota.xyz/applications/${applicationId}/attestations`, {
    method: 'POST',
    headers: {
      'X-Application-Token': applicationToken,
      'X-Idempotency-Key': crypto.randomUUID(),
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      attestation_type: type,
      timestamp: otherTimestamp,
      applicant_id: applicantId
    })
  });
}
```

```python Python theme={null}
import requests
import uuid
import time

# Step 1: Submit e_sign first
e_sign_timestamp = int(time.time())

requests.post(
    f'https://api.platform.dakota.xyz/applications/{application_id}/attestations',
    headers={
        'X-Application-Token': application_token,
        'X-Idempotency-Key': str(uuid.uuid4()),
        'Content-Type': 'application/json',
    },
    json={
        'attestation_type': 'e_sign',
        'timestamp': e_sign_timestamp,
        'applicant_id': applicant_id,
    },
)

# Step 2: Submit remaining attestations with a later timestamp
other_timestamp = e_sign_timestamp + 1
other_types = [
    'information_accuracy',
    'terms_of_service',
    'privacy_policy',
    'funds_transfer_agreement',
    'lead_bank_privacy_policy',
]

for attestation_type in other_types:
    requests.post(
        f'https://api.platform.dakota.xyz/applications/{application_id}/attestations',
        headers={
            'X-Application-Token': application_token,
            'X-Idempotency-Key': str(uuid.uuid4()),
            'Content-Type': 'application/json',
        },
        json={
            'attestation_type': attestation_type,
            'timestamp': other_timestamp,
            'applicant_id': applicant_id,
        },
    )
```

<Note>
  * The `timestamp` is the Unix epoch time (in seconds) when the user accepted the agreement.
  * The `applicant_id` is the individual making the attestation. For business applications, this must be a control person. For individual applications, this must be the individual themselves.
  * Each attestation's timestamp must be **strictly after** the `e_sign` attestation timestamp — equal timestamps will be rejected.
  * All 6 attestation types are **required** before an application can be submitted.
</Note>

### Checking Missing Attestations

You can check which attestations are still needed by retrieving the application with the `validation` include parameter:

```bash theme={null}
curl "https://api.platform.dakota.xyz/applications/{application_id}?include=attestations,validation" \
  -H "X-Application-Token: {application_token}"
```

The response `validation.attestations` field shows completed and missing attestations:

```json theme={null}
{
  "attestations": {
    "completed": [
      {
        "type": "e_sign",
        "attested_at": "2024-01-17T15:30:00Z",
        "attested_by": "John Doe"
      }
    ],
    "missing": [
      "terms_of_service",
      "privacy_policy"
    ]
  }
}
```

<Info>
  **Using the hosted onboarding form?** If you redirect customers to the `application_url`, Dakota's hosted form handles presenting all legal agreements and collecting attestations automatically. You only need to manage attestations manually if you are building a custom onboarding UI.
</Info>

# Step 4: Monitor Application Status

Check onboarding status programmatically:

```bash cURL theme={null}
curl -X GET https://api.platform.dakota.xyz/customers/{customer_id} \
  -H "X-API-Key: your-api-key"
```

```javascript JavaScript theme={null}
const response = await fetch(`https://api.platform.dakota.xyz/customers/${customerId}`, {
  headers: {
    'X-API-Key': 'your-api-key'
  }
});

const customer = await response.json();
console.log('KYB Status:', customer.kyb_status);
```

```python Python theme={null}
import requests

response = requests.get(
    f'https://api.platform.dakota.xyz/customers/{customer_id}',
    headers={'X-API-Key': 'your-api-key'}
)

customer = response.json()
print(f'KYB Status: {customer["kyb_status"]}')
```

```go Go theme={null}
package main

import (
    "net/http"
)

func main() {
    client := &http.Client{}
    req, _ := http.NewRequest("GET", "https://api.platform.dakota.xyz/customers/" + customerId, nil)
    
    req.Header.Add("X-API-Key", "your-api-key")
    
    resp, _ := client.Do(req)
    defer resp.Body.Close()
}
```

```rust Rust theme={null}
use reqwest::header::{HeaderMap, HeaderValue};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = reqwest::Client::new();
    
    let mut headers = HeaderMap::new();
    headers.insert("X-API-Key", HeaderValue::from_static("your-api-key"));
    
    let response = client
        .get(&format!("https://api.platform.dakota.xyz/customers/{}", customer_id))
        .headers(headers)
        .send()
        .await?;
    
    Ok(())
}
```

```java Java theme={null}
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;

public class DakotaKYBStatusExample {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();
        
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.platform.dakota.xyz/customers/" + customerId))
            .header("X-API-Key", "your-api-key")
            .GET()
            .build();
            
        HttpResponse<String> response = client.send(request, 
            HttpResponse.BodyHandlers.ofString());
    }
}
```

Response:

```json theme={null}
{
  "id": "2Nq8G7xK9mR4Ls6DyJ1Uf3Tp",
  "name": "Acme Corp",
  "customer_type": "business",
  "kyb_status": "active",
  "kyb_links": [],
  "application_id": "2WGC9cKv9P4K8eGzqY6qJ3Xz7Qm",
  "created_at": 1705320600,
  "updated_at": 1705324200
}
```

# KYB Status Values

The `kyb_status` field indicates the customer's verification status:

| Status           | Description                        |
| ---------------- | ---------------------------------- |
| `pending`        | KYB verification in progress       |
| `active`         | Customer approved and can transact |
| `partner_review` | Under manual review                |
| `rejected`       | Verification failed                |
| `frozen`         | Account suspended                  |

You can also check the application status directly via the [Get Application](/api-reference/onboarding/get-an-onboarding-application) endpoint for more detailed status information.

# Best Practices

## User Experience

* Clearly communicate the onboarding requirements
* Provide progress indicators
* Set expectations for processing time
* Offer support contact information

## Error Handling

* Handle rejection gracefully
* Provide clear next steps
* Allow customers to restart if needed
* Log all onboarding events

## Compliance

* Present all required legal agreements (ToS, Privacy Policy, etc.) to end users before collecting attestations
* Record the exact timestamp when each agreement was accepted
* Store audit trails
* Monitor for suspicious activity
* Keep records for compliance reporting
* Regular review of rejected applications

# Required Documents

Typical documents required for business verification:

* **Certificate of Incorporation**
* **Bank Statements**
* **Director/Officer Identification**
* **Beneficial Ownership Information**

# Troubleshooting

## Common Issues

**Onboarding Stuck in "Pending"**

* Check if customer has accessed the onboarding URL

**Repeated Rejections**

* Ensure document quality meets requirements
* Consider manual review process

**Webhook Not Received**

* Verify webhook endpoint is responding with 200
* Check webhook signature validation
* Review webhook logs in dashboard

Any issues not covered here can be addressed by contacting Dakota Platform support.

# Next Steps

After successful customer onboarding:

1. **[Set up Recipients & Destinations](destinations-recipients)** - Configure payment targets
2. **[Create Transactions](/documentation/common-flows)** - Process payments for approved customers
3. **[Webhook Integration](webhooks)** - Get real-time KYB status updates (see the [full event list](/documentation/webhooks#event-types-reference) for all available events)
4. **[Testing](testing)** - Test your onboarding flow

# API Reference

For detailed endpoint documentation, see:

* [Customers API Reference](/api-reference/customers/create-a-customer-record) - Customer creation and management
* [Webhooks API Reference](/api-reference/webhooks/create-a-webhook-target) - Real-time onboarding notifications
