Managed Customers
Клиент - это конечный пользователь, которого вы ведёте на платформе: под ним живут крипто-кошельки, платежи, верификация и история операций. Вы создаёте клиентов через API, привязываете к ним свой external_id и работаете дальше по uuid.
phone в формате E.164 - обязательное поле: номер служит идентификатором человека в контуре и уникален в пределах вашей учётной записи и окружения. Регулярное выражение проверки: ^\+[1-9][0-9]{6,19}$ - плюс, затем от 7 до 20 цифр, первая не ноль. Пробелы, скобки и дефисы не допускаются: +79001234567.
Модель клиента
Поля
uuid
string
optional
external_id
string|null
optional
name
string
required
email
string|null
optional
phone
string
required
+79001234567. Уникален в пределах вашей учётной записи и окружения.env
enum
optional
live или sandbox. Наследуется от ключа, которым клиент создан, и не меняется.status
enum
optional
active или blocked. У заблокированного клиента запрещены вывод и расчёты; удалению мешает только блокировка со стороны платформы, ваша собственная - нет.blocked_reason
string|null
optional
blocked_at
timestamp|null
optional
metadata
object|null
optional
block_source у заблокированного клиента (см. Блокировка); при обновлении metadata он сохраняется автоматически, передавать его не нужно (а подменить - нельзя).kyc_status
enum
optional
not_started, pending, processing, verified, rejected, expired.kyc_level
integer
optional
0..3. См. KYC.consent_signed
boolean
optional
consent_signed_at
timestamp|null
optional
consent_ip
string|null
optional
created_at
timestamp
optional
| Name | Type | Required | Description |
|---|---|---|---|
uuid
|
string
|
optional | Публичный идентификатор клиента. Используется во всех путях эндпоинтов. |
external_id
|
string|null
|
optional | Ваш собственный идентификатор пользователя. Участвует в идемпотентности создания. |
name
|
string
|
required | Имя клиента. |
email
|
string|null
|
optional | Электронная почта (опционально). |
phone
|
string
|
required |
Телефон в E.164: +79001234567. Уникален в пределах вашей учётной записи и окружения.
|
env
|
enum
|
optional |
live или sandbox. Наследуется от ключа, которым клиент создан, и не меняется.
|
status
|
enum
|
optional |
active или blocked. У заблокированного клиента запрещены вывод и расчёты; удалению мешает только блокировка со стороны платформы, ваша собственная - нет.
|
blocked_reason
|
string|null
|
optional | Причина блокировки, если она передавалась. |
blocked_at
|
timestamp|null
|
optional | Момент блокировки. |
metadata
|
object|null
|
optional |
Ваши произвольные поля. Платформа добавляет сюда служебный ключ block_source у заблокированного клиента (см. Блокировка); при обновлении metadata он сохраняется автоматически, передавать его не нужно (а подменить - нельзя).
|
kyc_status
|
enum
|
optional |
not_started, pending, processing, verified, rejected, expired.
|
kyc_level
|
integer
|
optional |
Уровень верификации 0..3. См. KYC.
|
consent_signed
|
boolean
|
optional | Вы подтвердили, что клиент дал согласие на обработку данных. Обязательно перед KYC-операциями. |
consent_signed_at
|
timestamp|null
|
optional | Когда согласие было зафиксировано. |
consent_ip
|
string|null
|
optional | IP клиента в момент согласия (передаёте вы). |
created_at
|
timestamp
|
optional |
Объект может содержать дополнительные служебные поля - читайте только те, что перечислены выше, и не полагайтесь на полный состав ответа.
Создать клиента
/api/v1/customers
Создать клиента (идемпотентно)
Request body
name
string
required
phone
string
required
^\+[1-9][0-9]{6,19}$.email
string
optional
external_id
string
optional
metadata
object
optional
| Name | Type | Required | Description |
|---|---|---|---|
name
|
string
|
required | Имя клиента, до 255 символов. |
phone
|
string
|
required |
Телефон в E.164: ^\+[1-9][0-9]{6,19}$.
|
email
|
string
|
optional | Электронная почта, до 255 символов. |
external_id
|
string
|
optional | Ваш идентификатор пользователя, до 100 символов. |
metadata
|
object
|
optional | Произвольный JSON. |
external_id- если передан и клиент с таким значением у вас уже есть, возвращается он;phone- если по номеру уже заведён клиент, возвращается он.
201, найденный существующий - 200 с его текущими данными (поля из запроса при этом не перезаписываются - для правки используйте PATCH). Различайте создание и совпадение по HTTP-коду.
Responses
{
"data": {
"uuid": "5f8d7a3c-1234-4567-89ab-cdef01234567",
"external_id": "user-42",
"name": "Иван Петров",
"email": "ivan@partner.com",
"phone": "+79001234567",
"env": "live",
"status": "active",
"blocked_reason": null,
"blocked_at": null,
"kyc_status": "not_started",
"kyc_level": 0,
"consent_signed": false,
"consent_ip": null,
"metadata": {"tier": "gold"},
"created_at": "2026-08-17T11:23:00.000000Z",
"updated_at": "2026-08-17T11:23:00.000000Z"
}
}
{
"data": {
"uuid": "5f8d7a3c-1234-4567-89ab-cdef01234567",
"external_id": "user-42",
"name": "Иван Петров",
"email": "ivan@partner.com",
"phone": "+79001234567",
"env": "live",
"status": "active",
"blocked_reason": null,
"blocked_at": null,
"kyc_status": "verified",
"kyc_level": 2,
"consent_signed": true,
"consent_ip": "203.0.113.10",
"metadata": {"tier": "gold"},
"created_at": "2026-07-02T09:14:00.000000Z",
"updated_at": "2026-08-17T11:23:00.000000Z"
}
}
{
"error": {
"code": "invalid_request",
"message": "Некорректные данные запроса.",
"request_id": "req_a1B2c3D4e5F6g7H8i9J0kLmN",
"fields": {
"phone": ["The phone field format is invalid."],
"name": ["The name field is required."]
}
}
}
Набор полей в ответе одинаков у 201 и 200 - различается только HTTP-код и сами значения. Разбирайте оба ответа одним кодом.
Создание, изменение, удаление клиента и фиксация согласия делят общую корзину лимита частоты: 120 запросов в минуту на всю четвёрку. У чтения (список, карточка, баланс, история) выделенной корзины нет.
Список клиентов
/api/v1/customers
Постраничный список ваших клиентов
Query parameters
filter[phone]
string
optional
422. Самый быстрый способ найти клиента по телефону.filter[status]
string
optional
active или blocked.filter[kyc_status]
string
optional
not_started, pending, processing, verified, rejected, expired.filter[env]
string
optional
live или sandbox. По умолчанию - окружение ключа.filter[search]
string
optional
external_id. До 100 символов.per_page
integer
optional
20page
integer
optional
1| Name | Type | Required | Description |
|---|---|---|---|
filter[phone]
|
string
|
optional |
Точный поиск по номеру (не подстрока). Значение обязано быть валидным E.164, иначе 422. Самый быстрый способ найти клиента по телефону.
|
filter[status]
|
string
|
optional |
active или blocked.
|
filter[kyc_status]
|
string
|
optional |
not_started, pending, processing, verified, rejected, expired.
|
filter[env]
|
string
|
optional |
live или sandbox. По умолчанию - окружение ключа.
|
filter[search]
|
string
|
optional |
Подстрока по имени, почте, телефону и external_id. До 100 символов.
|
per_page
|
integer
|
optional |
Размер страницы, 1..100.
Default:
20 |
page
|
integer
|
optional |
Номер страницы.
Default:
1 |
Responses
{
"data": [ { "uuid": "5f8d7a3c-…", "name": "Иван Петров", "phone": "+79001234567", "status": "active" } ],
"links": {
"first": "https://fin-os.io/api/v1/customers?page=1",
"last": "https://fin-os.io/api/v1/customers?page=1",
"prev": null,
"next": null
},
"meta": {
"current_page": 1,
"from": 1,
"to": 1,
"per_page": 20,
"total": 1,
"last_page": 1
}
}
Получить клиента
/api/v1/customers/{uuid}
Карточка клиента по UUID
env='live', для ключа sk_test_ не существует - придёт 404 (и наоборот). Это намеренно: ключ не должен даже узнать о существовании клиента в чужом окружении.
Обновить клиента
/api/v1/customers/{uuid}
Изменить имя, почту, телефон, external_id или metadata
Request body
name
string
optional
email
string|null
optional
null, чтобы очистить.phone
string|null
optional
null для очистки. Здесь телефон необязателен - обязателен он только при создании, поэтому GET → PATCH тем же телом безопасен.external_id
string|null
optional
null. Значение уникально среди ваших клиентов окружения: конфликт отвечает 422 invalid_request с подсказкой в fields.external_id.metadata
object|null
optional
block_source платформа сохраняет сама.| Name | Type | Required | Description |
|---|---|---|---|
name
|
string
|
optional | До 255 символов. |
email
|
string|null
|
optional |
Передайте null, чтобы очистить.
|
phone
|
string|null
|
optional |
Новый номер в E.164 либо null для очистки. Здесь телефон необязателен - обязателен он только при создании, поэтому GET → PATCH тем же телом безопасен.
|
external_id
|
string|null
|
optional |
До 100 символов либо null. Значение уникально среди ваших клиентов окружения: конфликт отвечает 422 invalid_request с подсказкой в fields.external_id.
|
metadata
|
object|null
|
optional |
Заменяет объект целиком; служебный ключ block_source платформа сохраняет сама.
|
Поля kyc_*, env, status и consent_* через PATCH не меняются - для них есть отдельные эндпоинты.
Responses
{
"error": {
"code": "duplicate_phone",
"message": "Этот номер телефона уже занят другим клиентом.",
"request_id": "req_7Kp2Rt9Wx4Yz1Qb6Nm3Vc8L"
}
}
Ответ 409 duplicate_phone приходит, когда назначаемый номер уже принадлежит другому вашему клиенту того же окружения. Назначение клиенту его же текущего номера ошибкой не является.
Блокировка
/api/v1/customers/{uuid}/block
Заблокировать клиента
charge и payout. Уходит вебхук customer.blocked. Удалению ваша собственная блокировка не мешает: заблокированного вами клиента DELETE удалит, если у него нет остатков, адресов и истории.Request body
reason
string
optional
blocked_reason и в полезную нагрузку вебхука.| Name | Type | Required | Description |
|---|---|---|---|
reason
|
string
|
optional |
Причина блокировки, до 255 символов. Попадает в поле blocked_reason и в полезную нагрузку вебхука.
|
/api/v1/customers/{uuid}/unblock
Снять блокировку
customer.unblocked.metadata.block_source: merchant - наложили вы, officer - комплаенс-офицер платформы, sanctions - автоматическая блокировка по санкционному совпадению. Снять через API можно только merchant; на остальные приходит 409 conflict. Блокировка всегда усиливается: санкционное совпадение по уже заблокированному вами клиенту перекрывает источник, и снять её вашим вызовом будет нельзя.
Оба эндпоинта возвращают 200 с обновлённой карточкой клиента. Повторная блокировка уже заблокированного клиента и разблокировка активного ошибкой не считаются. Блокировка и разблокировка делят общую корзину мутирующих вызовов кошельков (30 запросов в минуту на всю группу, вместе с выдачей адреса и симуляцией депозита), а не по 30 на каждый эндпоинт.
Удалить клиента
/api/v1/customers/{uuid}
Мягкое удаление клиента
204 без тела. Удаление мягкое: запись сохраняется для целей комплаенса, полное стирание не предусмотрено.Удаление возможно не всегда - платформа отказывает в трёх случаях:
customer_blocked
409
optional
block_source: merchant) удалению не мешает - её вы вправе снять сами.customer_has_funds
409
optional
customer_has_wallets
409
optional
| Name | Type | Required | Description |
|---|---|---|---|
customer_blocked
|
409
|
optional |
Клиент заблокирован платформой - комплаенс-офицером или автоматически по санкционному совпадению. Иначе удаление и повторное создание по тому же номеру давало бы чистого клиента вместо заблокированного. Ваша собственная блокировка (block_source: merchant) удалению не мешает - её вы вправе снять сами.
|
customer_has_funds
|
409
|
optional | На счетах клиента есть остатки, включая замороженные. Сначала выведите или спишите деньги. |
customer_has_wallets
|
409
|
optional | Клиенту выданы крипто-адреса либо по нему есть история движений. Такой клиент не удаляется даже с нулевым остатком: платёж «в полёте» придёт на уже выданный адрес, а номер телефона к тому моменту мог быть отдан другому человеку. |
Зафиксировать согласие
/api/v1/customers/{uuid}/consent
Подтвердить, что клиент дал согласие на обработку данных
Request body
ip
string
required
| Name | Type | Required | Description |
|---|---|---|---|
ip
|
string
|
required | IP клиента в момент согласия. Валидный IPv4 или IPv6. |
Балансы клиента
/api/v1/customers/{uuid}/balance
Торговые остатки и крипто-балансы одним ответом
Responses
{
"data": [
{ "currency": "RUB", "available": "15000.0000000000", "locked": "0.0000000000" }
],
"meta": {
"crypto": [
{
"asset": "USDT",
"symbol": "USDT",
"network": "TRON",
"currency": "USDT_TRON",
"available": "150.0000000000",
"locked": "0.0000000000"
}
],
"status": "active"
}
}
data лежат строки торговых остатков клиента, в meta.crypto - крипто-балансы по каждой доступной вам монете (то же, что отдаёт GET /api/v1/customers/{uuid}/wallet). Поле locked в обоих случаях - сумма, временно недоступная клиенту: резерв под операцию либо заморозка комплаенса. Подробнее - Customer Wallets.
История операций
/api/v1/customers/{uuid}/transactions
Единая лента: крипто-движения и торговые операции
Query parameters
per_page
integer
optional
25starting_after
string
optional
id последней полученной записи (значение meta.next_cursor). Неизвестный или устаревший курсор не считается ошибкой - вернётся первая страница.| Name | Type | Required | Description |
|---|---|---|---|
per_page
|
integer
|
optional |
Размер страницы, 1..100.
Default:
25 |
starting_after
|
string
|
optional |
Курсор: id последней полученной записи (значение meta.next_cursor). Неизвестный или устаревший курсор не считается ошибкой - вернётся первая страница.
|
Responses
{
"data": [
{
"kind": "crypto",
"id": "7c1f2b90-4a3e-4a1f-9c62-8b0f4a7d2e11",
"network": "TRON",
"asset": "USDT",
"direction": "in",
"amount": "150.000000000000000000",
"status": "confirmed",
"aml_status": "clear",
"frozen": false,
"fee": { "platform": "0.000000000000000000", "merchant": "0.000000000000000000" },
"created_at": "2026-08-17T12:04:11+00:00"
},
{
"kind": "trading",
"id": "c93a1f70-2b48-4f61-a0d5-88e2c4b71f03",
"type": "payment",
"status": 2,
"amount": "10000.00",
"currency": "RUB",
"amount_to": "117.4500",
"currency_to": "USDT",
"created_at": "2026-08-16T09:12:40+00:00"
}
],
"meta": {
"per_page": 25,
"returned": 2,
"has_more": false,
"next_cursor": null
}
}
Разбор ленты
kind
enum
optional
crypto - движение по крипто-кошельку клиента (полный состав полей описан в Customer Wallets); trading - операция обмена фиата и криптовалюты.id
string
optional
starting_after и работает для обоих источников - различать их не нужно.meta.returned
integer
optional
meta.has_more
bool
optional
meta.next_cursor
string|null
optional
null, когда лента закончилась.| Name | Type | Required | Description |
|---|---|---|---|
kind
|
enum
|
optional |
crypto - движение по крипто-кошельку клиента (полный состав полей описан в Customer Wallets); trading - операция обмена фиата и криптовалюты.
|
id
|
string
|
optional |
Идентификатор записи. Он же передаётся в starting_after и работает для обоих источников - различать их не нужно.
|
meta.returned
|
integer
|
optional | Сколько записей в этом ответе. |
meta.has_more
|
bool
|
optional | Есть ли ещё записи. |
meta.next_cursor
|
string|null
|
optional |
Курсор для следующего запроса. null, когда лента закончилась.
|
kind: "trading") поле status - число, код статуса сделки: 0 - в обработке, 1 - ожидание банка, 2 - успешно, 3 - холд, 4 - отменена, 5 - отклонена антифродом, 6 - ошибка банка, 7 - запрошен возврат, 8 - возвращена, 9 - ошибка инициализации, 10 - истекло время, а type - строка операции (payment - покупка, payout - продажа; значений buy/sell здесь нет). У крипто-строк (kind: "crypto") status - строка из трёх значений pending / confirmed / failed, как описано в разделе Customer Wallets.
События по клиенту
Изменения статуса клиента и движения по его счетам приходят вебхуками: customer.blocked, customer.unblocked, customer.deposit.*, customer.withdrawal.*, customer.charged, customer.payout и события верификации customer.kyc.*. Полный список и формат - Webhooks.
Примеры кода
KEY="sk_live_..."
B="https://fin-os.io/api/v1"
# 1. Создать клиента (телефон обязателен)
curl -X POST $B/customers \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Иван Петров",
"phone": "+79001234567",
"email": "ivan@example.com",
"external_id": "user-42",
"metadata": {"tier": "gold"}
}'
# 201 - создан, 200 - клиент с таким external_id или телефоном уже был
# 2. Найти клиента по телефону (точное совпадение)
curl -G $B/customers -H "Authorization: Bearer $KEY" \
--data-urlencode "filter[phone]=+79001234567"
# 3. Согласие на обработку данных (нужно перед KYC)
curl -X POST $B/customers/$UUID/consent \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"ip": "203.0.113.42"}'
# 4. Заблокировать и разблокировать
curl -X POST $B/customers/$UUID/block \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{"reason": "Подозрительная активность"}'
curl -X POST $B/customers/$UUID/unblock -H "Authorization: Bearer $KEY"
# 5. История операций постранично по курсору
curl -G $B/customers/$UUID/transactions -H "Authorization: Bearer $KEY" \
--data-urlencode "per_page=50" \
--data-urlencode "starting_after=7c1f2b90-4a3e-4a1f-9c62-8b0f4a7d2e11"
const B = 'https://fin-os.io/api/v1';
const KEY = process.env.FINOS_SECRET_KEY;
const H = { 'Authorization': `Bearer ${KEY}`, 'Content-Type': 'application/json' };
// Создание идемпотентно: 201 - новый, 200 - уже был
async function createCustomer(data) {
const res = await fetch(`${B}/customers`, { method: 'POST', headers: H, body: JSON.stringify(data) });
const body = await res.json();
if (!res.ok) throw Object.assign(new Error(body.error.message), body.error);
return { customer: body.data, created: res.status === 201 };
}
// Поиск по телефону - точное совпадение
async function findByPhone(phone) {
const q = new URLSearchParams({ 'filter[phone]': phone });
const { data } = await (await fetch(`${B}/customers?${q}`, { headers: H })).json();
return data[0] ?? null;
}
// Полная история клиента по курсору
async function history(uuid) {
const rows = [];
let cursor = null;
do {
const q = new URLSearchParams({ per_page: '100' });
if (cursor) q.set('starting_after', cursor);
const body = await (await fetch(`${B}/customers/${uuid}/transactions?${q}`, { headers: H })).json();
rows.push(...body.data);
cursor = body.meta.has_more ? body.meta.next_cursor : null;
} while (cursor);
return rows;
}
const { customer, created } = await createCustomer({
name: 'Иван Петров', phone: '+79001234567', external_id: 'user-42',
});
console.log(created ? 'создан' : 'уже существовал', customer.uuid);
use Illuminate\Support\Facades\Http;
$api = Http::withToken(config('services.finos.secret_key'))
->acceptJson()
->baseUrl('https://fin-os.io/api/v1');
// 1. Создание. 201 - новый клиент, 200 - уже был (по external_id или телефону)
$response = $api->post('/customers', [
'name' => 'Иван Петров',
'phone' => '+79001234567',
'email' => 'ivan@example.com',
'external_id' => 'user-42',
]);
$customer = $response->json('data');
$created = $response->status() === 201;
// 2. Согласие на обработку данных
$api->post("/customers/{$customer['uuid']}/consent", ['ip' => request()->ip()]);
// 3. Поиск по телефону
$found = $api->get('/customers', ['filter' => ['phone' => '+79001234567']])->json('data');
// 4. Удаление: 204 либо 409 с причиной отказа
$delete = $api->delete("/customers/{$customer['uuid']}");
if ($delete->status() === 409) {
match ($delete->json('error.code')) {
'customer_has_funds' => 'сначала выведите остатки',
'customer_has_wallets' => 'у клиента есть адреса или история движений',
'customer_blocked' => 'блокировка снимается комплаенс-офицером',
};
}
import os, requests
B = 'https://fin-os.io/api/v1'
KEY = os.environ['FINOS_SECRET_KEY']
H = {'Authorization': f'Bearer {KEY}'}
# Создание идемпотентно по external_id, затем по телефону
r = requests.post(f'{B}/customers', headers=H, json={
'name': 'Иван Петров',
'phone': '+79001234567',
'external_id': 'user-42',
})
r.raise_for_status()
customer, created = r.json()['data'], r.status_code == 201
# Точный поиск по телефону
found = requests.get(f'{B}/customers', headers=H,
params={'filter[phone]': '+79001234567'}).json()['data']
# История по курсору
def history(uuid, per_page=100):
cursor = None
while True:
params = {'per_page': per_page}
if cursor:
params['starting_after'] = cursor
body = requests.get(f'{B}/customers/{uuid}/transactions',
headers=H, params=params).json()
yield from body['data']
if not body['meta']['has_more']:
return
cursor = body['meta']['next_cursor']