curl --request POST \
--url https://api-sandbox.endl.io/api/v0/customer/{userId}/documents \
--header 'API-KEY: <api-key>' \
--header 'Api-Version: <api-version>' \
--header 'Content-Type: multipart/form-data' \
--form document=@example-file \
--form 'idDocType=<string>' \
--form 'country=<string>' \
--form 'idDocSubType=<string>' \
--form 'side=<string>'import requests
url = "https://api-sandbox.endl.io/api/v0/customer/{userId}/documents"
files = { "document": ("example-file", open("example-file", "rb")) }
payload = {
"idDocType": "<string>",
"country": "<string>",
"idDocSubType": "<string>",
"side": "<string>"
}
headers = {
"Api-Version": "<api-version>",
"API-KEY": "<api-key>"
}
response = requests.post(url, data=payload, files=files, headers=headers)
print(response.text)const form = new FormData();
form.append('document', '<string>');
form.append('idDocType', '<string>');
form.append('country', '<string>');
form.append('idDocSubType', '<string>');
form.append('side', '<string>');
const options = {
method: 'POST',
headers: {'Api-Version': '<api-version>', 'API-KEY': '<api-key>'}
};
options.body = form;
fetch('https://api-sandbox.endl.io/api/v0/customer/{userId}/documents', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api-sandbox.endl.io/api/v0/customer/{userId}/documents",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"document\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"idDocType\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"country\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"idDocSubType\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"side\"\r\n\r\n<string>\r\n-----011000010111000001101001--",
CURLOPT_HTTPHEADER => [
"API-KEY: <api-key>",
"Api-Version: <api-version>",
"Content-Type: multipart/form-data"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api-sandbox.endl.io/api/v0/customer/{userId}/documents"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"document\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"idDocType\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"country\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"idDocSubType\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"side\"\r\n\r\n<string>\r\n-----011000010111000001101001--")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Api-Version", "<api-version>")
req.Header.Add("API-KEY", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api-sandbox.endl.io/api/v0/customer/{userId}/documents")
.header("Api-Version", "<api-version>")
.header("API-KEY", "<api-key>")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"document\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"idDocType\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"country\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"idDocSubType\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"side\"\r\n\r\n<string>\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://api-sandbox.endl.io/api/v0/customer/{userId}/documents")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Api-Version"] = '<api-version>'
request["API-KEY"] = '<api-key>'
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"document\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"idDocType\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"country\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"idDocSubType\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"side\"\r\n\r\n<string>\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"accepted": true,
"status": "SUBMITTED",
"missing": [
"SELFIE",
"PROOF_OF_RESIDENCE"
]
}{
"code": 400,
"message": "Onboarding validation failed",
"status": "ERROR",
"data": null,
"errors": [
{
"code": "ERRCUS_1406",
"message": "idDocType must be one of PASSPORT, ID_CARD, DRIVERS, RESIDENCE_PERMIT, SELFIE, UTILITY_BILL",
"field": "idDocType"
}
]
}{
"code": 401,
"message": "Invalid API key",
"status": "ERROR",
"data": null,
"errors": [
{
"code": "ERRCORE_1004",
"message": "Invalid API key",
"field": null
}
]
}{
"code": 404,
"message": "User with ID 'cus_PWEhqXkPKpWXiFd96jGq' not found",
"status": "ERROR",
"data": null,
"errors": [
{
"code": "ERRREF_1001",
"message": "User with ID 'cus_PWEhqXkPKpWXiFd96jGq' not found",
"field": "userId"
}
]
}{
"code": 400,
"message": "<string>",
"status": "ERROR",
"data": null,
"errors": [
{
"code": "<string>",
"message": "<string>",
"field": "<string>"
}
]
}{
"code": 413,
"message": "File exceeds the maximum upload size of 5 MB",
"status": "ERROR",
"data": null,
"errors": [
{
"code": "ERRCORE_1001",
"message": "File too large",
"field": null
}
]
}Upload document
Uploads an individual’s identity document, or a company document for a business.
For an individual, the customer is submitted for review automatically — but only once every required document step is uploaded: IDENTITY (passport, ID card, driving licence or residence permit), SELFIE, and PROOF_OF_RESIDENCE (a UTILITY_BILL). Until then the response stays SUBMITTED and missing lists what is still needed; after the last one the status is PENDING and missing is omitted.
| Upload | Response status | missing |
|---|---|---|
1. PASSPORT | SUBMITTED | ["SELFIE", "PROOF_OF_RESIDENCE"] |
2. SELFIE | SUBMITTED | ["PROOF_OF_RESIDENCE"] |
3. UTILITY_BILL | PENDING | (omitted) |
For a business, Endl submits the business after its first company document, once every owner’s documents are complete. Upload both company groups before the owners’ last document, so the company isn’t submitted with one group missing.
Content is sniffed rather than trusted by extension, so a renamed file is rejected.
curl --request POST \
--url https://api-sandbox.endl.io/api/v0/customer/{userId}/documents \
--header 'API-KEY: <api-key>' \
--header 'Api-Version: <api-version>' \
--header 'Content-Type: multipart/form-data' \
--form document=@example-file \
--form 'idDocType=<string>' \
--form 'country=<string>' \
--form 'idDocSubType=<string>' \
--form 'side=<string>'import requests
url = "https://api-sandbox.endl.io/api/v0/customer/{userId}/documents"
files = { "document": ("example-file", open("example-file", "rb")) }
payload = {
"idDocType": "<string>",
"country": "<string>",
"idDocSubType": "<string>",
"side": "<string>"
}
headers = {
"Api-Version": "<api-version>",
"API-KEY": "<api-key>"
}
response = requests.post(url, data=payload, files=files, headers=headers)
print(response.text)const form = new FormData();
form.append('document', '<string>');
form.append('idDocType', '<string>');
form.append('country', '<string>');
form.append('idDocSubType', '<string>');
form.append('side', '<string>');
const options = {
method: 'POST',
headers: {'Api-Version': '<api-version>', 'API-KEY': '<api-key>'}
};
options.body = form;
fetch('https://api-sandbox.endl.io/api/v0/customer/{userId}/documents', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api-sandbox.endl.io/api/v0/customer/{userId}/documents",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"document\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"idDocType\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"country\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"idDocSubType\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"side\"\r\n\r\n<string>\r\n-----011000010111000001101001--",
CURLOPT_HTTPHEADER => [
"API-KEY: <api-key>",
"Api-Version: <api-version>",
"Content-Type: multipart/form-data"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api-sandbox.endl.io/api/v0/customer/{userId}/documents"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"document\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"idDocType\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"country\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"idDocSubType\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"side\"\r\n\r\n<string>\r\n-----011000010111000001101001--")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Api-Version", "<api-version>")
req.Header.Add("API-KEY", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api-sandbox.endl.io/api/v0/customer/{userId}/documents")
.header("Api-Version", "<api-version>")
.header("API-KEY", "<api-key>")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"document\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"idDocType\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"country\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"idDocSubType\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"side\"\r\n\r\n<string>\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://api-sandbox.endl.io/api/v0/customer/{userId}/documents")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Api-Version"] = '<api-version>'
request["API-KEY"] = '<api-key>'
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"document\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"idDocType\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"country\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"idDocSubType\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"side\"\r\n\r\n<string>\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"accepted": true,
"status": "SUBMITTED",
"missing": [
"SELFIE",
"PROOF_OF_RESIDENCE"
]
}{
"code": 400,
"message": "Onboarding validation failed",
"status": "ERROR",
"data": null,
"errors": [
{
"code": "ERRCUS_1406",
"message": "idDocType must be one of PASSPORT, ID_CARD, DRIVERS, RESIDENCE_PERMIT, SELFIE, UTILITY_BILL",
"field": "idDocType"
}
]
}{
"code": 401,
"message": "Invalid API key",
"status": "ERROR",
"data": null,
"errors": [
{
"code": "ERRCORE_1004",
"message": "Invalid API key",
"field": null
}
]
}{
"code": 404,
"message": "User with ID 'cus_PWEhqXkPKpWXiFd96jGq' not found",
"status": "ERROR",
"data": null,
"errors": [
{
"code": "ERRREF_1001",
"message": "User with ID 'cus_PWEhqXkPKpWXiFd96jGq' not found",
"field": "userId"
}
]
}{
"code": 400,
"message": "<string>",
"status": "ERROR",
"data": null,
"errors": [
{
"code": "<string>",
"message": "<string>",
"field": "<string>"
}
]
}{
"code": 413,
"message": "File exceeds the maximum upload size of 5 MB",
"status": "ERROR",
"data": null,
"errors": [
{
"code": "ERRCORE_1001",
"message": "File too large",
"field": null
}
]
}Authorizations
Your partner API key. The key must carry the customers permission.
Headers
The API version this request targets. Required on every /api/v0/* call. Format YYYY-MM.<release>; 2026-09.1 is the latest. The version that served the request is echoed back on the response. Missing, malformed or unsupported values are rejected with 400.
^\d{4}-\d{2}\.\d+$Path Parameters
The customer reference id returned by create — a cus_-prefixed string, not a UUID.
Body
JPEG, PNG or PDF. Maximum 5 MB; the whole request is capped at 10 MB.
For an individual: PASSPORT, ID_CARD, DRIVERS, RESIDENCE_PERMIT, SELFIE or UTILITY_BILL (a utility bill or bank statement, as proof of address). DRIVERS_LICENSE is still accepted as an alias and is stored and returned as DRIVERS. Omitted entirely → 400 ERRCORE_1001 (idDocType is required); sent but blank → ERRCUS_1405; any other value → ERRCUS_1406. For a business use COMPANY_DOC.
ISO 3166-1 alpha-3. Omitted entirely → 400 ERRCORE_1001 (country is required); sent but blank → ERRCUS_1012; invalid → ERRCUS_1013.
Which company document: INCORPORATION_CERT, INCORPORATION_ARTICLES or SHAREHOLDER_REGISTRY. Not needed for individual photo IDs.
FRONT or BACK, meaningful only for two-sided documents (ID_CARD, DRIVERS). Not validated — any value is passed through.
Response
Accepted. missing lists the document steps still outstanding, and is omitted once they are all in. For a business company upload, missing is not returned.