curl --request POST \
--url https://app.sahlfinancial.com/api/v1/flows/{flow_id}/publish \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"allow_manual_steps": false
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({allow_manual_steps: false})
};
fetch('https://app.sahlfinancial.com/api/v1/flows/{flow_id}/publish', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://app.sahlfinancial.com/api/v1/flows/{flow_id}/publish"
payload = { "allow_manual_steps": False }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"id": "d6a3f9b8-2c5e-4170-b8d1-4e0a7c3f5b29",
"tenant_id": "0b6f5a3e-7c1d-4e0a-9d2f-3a1c5e8b7f10",
"name": "Salaried borrower",
"description": null,
"use_case": "lending",
"kind": "kyc",
"country": "MA",
"nodes": [
{
"id": "start",
"type": "start",
"position": {
"x": 0,
"y": 0
},
"data": {
"label": "Start"
}
},
{
"id": "end",
"type": "end",
"position": {
"x": 240,
"y": 0
},
"data": {
"label": "End"
}
}
],
"edges": [
{
"id": "e1",
"source": "start",
"target": "end"
}
],
"version": 1,
"is_published": true,
"status": "published",
"environment": "sandbox",
"created_at": "2026-10-07T09:14:22Z",
"updated_at": "2026-10-07T09:14:22Z",
"published_at": "2026-10-07T09:14:22Z",
"country_support": null
}{
"detail": "Invalid or expired token"
}{
"detail": "Insufficient permissions"
}{
"detail": "Flow not found"
}{
"detail": "the policy was changed at the same time; publish again"
}{
"detail": {
"code": "manual_steps_unacknowledged",
"message": "These steps are not verified automatically: Liveness check. Each case will ask a reviewer to do them by hand. Publish again with allow_manual_steps=true to accept that.",
"steps": [
{
"step_type": "liveness",
"label": "Liveness check"
}
]
}
}{
"code": "rate_limit_exceeded",
"message": "Too many requests. Please slow down."
}Publish a flow
Who can call it: tenant_admin, tenant_reviewer, tenant_api_manager or platform_admin. A tenant_viewer is read-only and gets 403.
422 country_required / country_not_supported / steps_not_for_country when the country has no document rules for the flow; 422 manual_steps_unacknowledged when the flow has steps that a person performs, unless the body has allow_manual_steps: true. Publishing the flow that drives the Partner API’s KYC policy recompiles that policy (admin only; 422 kyc_flow_invalid if it no longer compiles).
Auth: dashboard session (Authorization: Bearer <access token>). Not available with a partner API key.
curl --request POST \
--url https://app.sahlfinancial.com/api/v1/flows/{flow_id}/publish \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"allow_manual_steps": false
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({allow_manual_steps: false})
};
fetch('https://app.sahlfinancial.com/api/v1/flows/{flow_id}/publish', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://app.sahlfinancial.com/api/v1/flows/{flow_id}/publish"
payload = { "allow_manual_steps": False }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"id": "d6a3f9b8-2c5e-4170-b8d1-4e0a7c3f5b29",
"tenant_id": "0b6f5a3e-7c1d-4e0a-9d2f-3a1c5e8b7f10",
"name": "Salaried borrower",
"description": null,
"use_case": "lending",
"kind": "kyc",
"country": "MA",
"nodes": [
{
"id": "start",
"type": "start",
"position": {
"x": 0,
"y": 0
},
"data": {
"label": "Start"
}
},
{
"id": "end",
"type": "end",
"position": {
"x": 240,
"y": 0
},
"data": {
"label": "End"
}
}
],
"edges": [
{
"id": "e1",
"source": "start",
"target": "end"
}
],
"version": 1,
"is_published": true,
"status": "published",
"environment": "sandbox",
"created_at": "2026-10-07T09:14:22Z",
"updated_at": "2026-10-07T09:14:22Z",
"published_at": "2026-10-07T09:14:22Z",
"country_support": null
}{
"detail": "Invalid or expired token"
}{
"detail": "Insufficient permissions"
}{
"detail": "Flow not found"
}{
"detail": "the policy was changed at the same time; publish again"
}{
"detail": {
"code": "manual_steps_unacknowledged",
"message": "These steps are not verified automatically: Liveness check. Each case will ask a reviewer to do them by hand. Publish again with allow_manual_steps=true to accept that.",
"steps": [
{
"step_type": "liveness",
"label": "Liveness check"
}
]
}
}{
"code": "rate_limit_exceeded",
"message": "Too many requests. Please slow down."
}Autorisations
The access token (JWT) of a signed-in console user, from POST /v1/auth/login. It lasts 30 minutes. It is not an API key: a partner API key is refused here. There is no cookie.
Paramètres de chemin
Corps
Réponse
OK
kyc, kyb "ga", "beta" (CI, SN, TN, EG, SA) or None for the flow's country.