Skip to main content

OpenAPI · REST reference

REST request envelopes, authentication, workflow lifecycle, error handling, configuration and file upload.

Getting Started

Base URL

Environment Gateway URL
Staging https://api-gateway-platform.width.com/gateway
Production https://api-gateway.width.com/gateway

Request Format

POST {base_url}?api={apiName}&apiVersion={apiVersion}&apiKey={apiKey}&sign={sign}
Content-Type: application/json
x-domain: {domainId}

{JSON body}
Parameter Location Required Description
api Query Yes API name
apiVersion Query Yes Varies per interface — workflow.apply.submit recommends 2.0.0, others use 1.0.0 (see Workflow Lifecycle API)
apiKey Query Yes Your API key
sign Query Yes HMAC-SHA256 signature of request body
x-domain Header Yes Your business domain ID

Submit a Workflow

Endpoint: workflow.apply.submit POST

Routes to the currently published (PUBLISHED) workflow by eventType + source and starts an execution.

apiVersion

apiVersion Behaviour
1.0.0 Auto-routes by eventType + source
2.0.0 Everything in 1.0.0 plus an optional workflowId to name the published workflow explicitly; an empty workflowId behaves exactly like 1.0.0. Recommended.

Request

{
  "eventType": "KYC_EVENT",
  "source": "API",
  "data": {
    "customerId": "demo-customer-001",
    "fullName": "ZHANG SAN",
    "dateOfBirth": "1990-01-01",
    "nationality": "CN"
  },
  "workflowId": ""
}
Field Type Required Description
eventType string Yes Event type: KYC_EVENT / KYB_EVENT / TRANSACTION_EVENT / WEB3_TRANSACTION_EVENT / MANUAL_CASE_EVENT, subject to the events enabled for your tenant. The Console labels the first two KYC and KYB — those are display names; the API expects KYC_EVENT and KYB_EVENT. MANUAL_CASE_EVENT submits a subject for name screening; it appears as Manual in the Console.
source string Yes Origin. Server-side integrations always send API.
data object Yes Business payload. customerId is required and must stay stable for the same customer (workflow.apply.query relies on it for recovery); the remaining fields follow the field definitions of the workflow’s entry event.
workflowId string No 2.0.0 only. Names a published workflow; empty means auto-route by event.

Response

{
  "apiCode": 200,
  "apiMsg": "OK",
  "data": {
    "code": 200,
    "message": null,
    "executionId": "exec-7208f03b931e4897",
    "status": "EXECUTING",
    "requestId": "...",
    "sync": false
  }
}
Field Type Description
executionId string Execution instance ID. Persist it — you poll workflow.execution.status with it.
status string Initial status (EXECUTING / SUBMITTED).
sync boolean Whether the execution ran synchronously; in sync mode the output carries the decision directly.
code int 200 = OK; 403 = quota exhausted (billing blocked).
Quota — submit is the only billed endpoint. A data.code of 403 means the eventType’s quota is exhausted; do not retry blindly — ask an admin to top up or adjust the quota.
The submit table documents KYC_EVENT, KYB_EVENT, TRANSACTION_EVENT, WEB3_TRANSACTION_EVENT and MANUAL_CASE_EVENT, subject to tenant enablement. It is not presented as exhaustive: the monitoring section also uses PERIODIC_FLOW_EVENT, and its inclusion in the submit enum list remains pending engineering confirmation.
customerId contract: the general submit reference requires data.customerId, stable for the same customer so workflow.apply.query can recover the execution. The Web3 field table marks it optional. Until engineering confirms the Web3 requirement, include a stable customerId in Web3 requests rather than relying on omission.

Where to find the Workflow ID: in WIDTH One Platform, open Workflow. Copy the Workflow ID shown beside the workflow name in the listing and pass it as workflowId when using apiVersion 2.0.0.

Response Format

Success:

{
  "apiCode": 200,
  "apiMsg": "OK",
  "data": {}
}

Error:

{
  "apiCode": 999001,
  "apiMsg": "SIGN_ERROR",
  "data": null
}
Two layers to read. The HTTP status is transport-level — 4xx/5xx means the gateway did not accept the request (no data). On HTTP 200, read apiCode for the gateway result (200 = OK). Some business endpoints then wrap their result in a second { code, data, msg } layer under data — so the business payload is at data.data (see the Register Customer example).

Read the response in order: HTTP status, then apiCode, then data.code and data.data where the endpoint uses a business envelope. apiCode 200 alone does not prove business success; use the endpoint-specific business result.

Error Codes

apiCode apiMsg Description Action
200 OK Success —
999000 GATEWAY_ERROR General gateway error Contact support
999001 SIGN_ERROR Signature verification failed See Authentication
999301 API_INVALID_REQUEST Malformed or invalid request Check request body
999302 API_NOT_FOUND API name not found Verify api parameter
999303 API_NON_AUTHORIZED Not authorized for this API Check apiKey permissions
999304 RATE_LIMIT_TRY_AGAIN Rate limit exceeded Wait and retry
999401 SERVICE_TIME_OUT Backend service timeout Retry with backoff
999402 SERVICE_INVOKE_ERROR Backend invocation failed Retry; contact support if persistent
999403 SERVICE_NOT_FOUND Backend service unavailable Contact support
999404 SERVICE_RESPONSE_FORMAT_ERROR Unexpected response from backend Contact support
999501 REQUEST_API_EXCEED_LIMIT API call quota exceeded Contact support to increase limits

Authentication

Signature Computation

sign = HmacSHA256(apiSecret, requestBody)

Both key and body use UTF-8 encoding. The result is a 64-character lowercase hex string. The signature must be computed on the exact bytes sent as the HTTP body.

Code Examples

Java:

Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(apiSecret.getBytes("UTF-8"), "HmacSHA256"));
byte[] hash = mac.doFinal(requestBody.getBytes("UTF-8"));
String sign = HexFormat.of().formatHex(hash); // Java 17+

Python:

import hmac, hashlib

sign = hmac.new(
    api_secret.encode('utf-8'),
    request_body.encode('utf-8'),
    hashlib.sha256
).hexdigest()

Node.js:

const crypto = require('crypto');
const sign = crypto.createHmac('sha256', apiSecret)
    .update(requestBody, 'utf8').digest('hex');
Never expose apiSecret in browser or mobile client code. Sign requests on a server you control. SDK clients use a session-scoped sdkToken.

Idempotency

All node data submission APIs are idempotent based on callbackKey. Submitting the same callbackKey multiple times is safe — only the first successful submission takes effect. This allows safe retries on network timeouts.

Interaction Model

Workflow Lifecycle API

The Workflow Lifecycle API is the pure server-side integration path: your backend submits an event, polls for the execution status and the Case decision, and — when a request is lost or retried — queries whether the customer already has an execution in flight. Three endpoints form one closed loop.

This differs from the node-driven SDK path documented under KYC / KYB, where the client submits data node by node. Here the platform runs the entire published workflow end to end and you only submit and poll.

Flow Overview

  1. Submit
  2. Poll Status
  3. Case Decision

workflow.apply.query sits before this loop as a recovery path: call it when a submit may have already happened (timeout or upstream retry) to avoid submitting twice.

Billing

API Purpose Billed
workflow.apply.submit Start a workflow execution Yes — charged per eventType
workflow.execution.status Read execution status + Case decision by executionId No
workflow.apply.query Check whether a customer already has an execution No
All three endpoints use the same gateway base URL, the same query-string request envelope (api / apiVersion / apiKey / sign) plus the x-domain header, and the same HMAC-SHA256 request-body signing as every other OpenAPI call. See OpenAPI Reference → Getting Started for the base URL (Staging api-gateway-platform.width.com/gateway / Production api-gateway.width.com/gateway) and request format, and Authentication for the full signing rules.

Status & Case Decision

Endpoint: workflow.execution.status POST

Read-only — it does not trigger execution and is not billed. executionId is checked against your tenant (and x-domain); it cannot be queried across tenants. Use apiVersion 1.0.0.

Request

{ "executionId": "exec-7208f03b931e4897" }
Field Type Required Description
executionId string Yes The execution instance ID returned by workflow.apply.submit.

Response

{
  "apiCode": 200,
  "apiMsg": "OK",
  "data": {
    "executionId": "exec-7208f03b931e4897",
    "status": "COMPLETED",
    "caseNo": "C001443",
    "caseStatus": "REJECTED",
    "riskScore": 45.0,
    "resolvedAt": 1784714818000
  }
}
Field Type Description
status string Execution status: RUNNING / WAITING / COMPLETED / FAILED / cancellation value: pending confirmation.
caseNo string Associated Case number (C%06d); empty until a Case is generated.
caseStatus string Case handling status: UNASSIGNED (queued, not yet assigned) / UNDER_REVIEW (being handled by a reviewer) / RETURNED (sent back for rework) / OTHER_GROUP (this group is done; the flow continues in another group/node) / APPROVED / REJECTED / CLOSED (finished, flow ended); empty when there is no Case. Polling exit conditions key off this field.
riskScore number Risk score (0–100); empty if none.
resolvedAt long Resolution time (epoch millis); empty until resolved.
currentNode etc. object SDK flow-control internals (currentNode / output / pendingCallbackTask); ignore them for server-side integration.
An empty data string means the executionId does not exist, or belongs to a different tenant/domain than the current apiKey.

Status Semantics

status caseStatus Meaning
RUNNING / WAITING empty Executing (including waiting on data collection); no Case yet.
RUNNING / WAITING UNASSIGNED / UNDER_REVIEW / RETURNED Stopped at a review node awaiting manual handling — keep polling.
RUNNING / WAITING OTHER_GROUP The current Case’s group is done; the flow continues in another group/node — keep polling.
COMPLETED / cancellation value: pending confirmation APPROVED / REJECTED / CLOSED Decision reached, flow finished.
FAILED — Execution failed; contact support with the executionId.

Response by Stage

Stage 1 — automatic nodes running, no Case yet (the brief window before the decision node):

{ "apiCode": 200, "apiMsg": "OK",
  "data": { "executionId": "exec-xxxx", "status": "RUNNING" } }

No caseStatus field → no Case generated yet, keep polling.

Stage 2 — Case generated, awaiting manual handling:

{ "apiCode": 200, "apiMsg": "OK",
  "data": { "executionId": "exec-xxxx", "status": "RUNNING",
            "caseNo": "C001286", "caseStatus": "UNASSIGNED", "riskScore": 49.17 } }

While being handled manually, caseStatus reflects progress: UNASSIGNED (queued) → UNDER_REVIEW (after a reviewer picks it up); RETURNED when sent back for rework.

Stage 3 — decision reached (manual review or automatic decision):

{ "apiCode": 200, "apiMsg": "OK",
  "data": { "executionId": "exec-xxxx", "status": "COMPLETED",
            "caseNo": "C001443", "caseStatus": "REJECTED", "riskScore": 45.0,
            "resolvedAt": 1784714818000 } }
In a multi-review-group flow, once the current Case’s group is done but the overall flow is still running in another group/node, caseStatus is OTHER_GROUP — keep polling until caseStatus reaches a terminal state (APPROVED / REJECTED / CLOSED).

Polling Exit Conditions

Every execution generates a Case once it reaches the decision node (manual review → UNASSIGNED / UNDER_REVIEW; automatic pass/reject/close → a terminal Case directly), so:

caseStatus in {APPROVED, REJECTED, CLOSED}
OR status == FAILED
OR status == CONFIRMED_CANCELLATION_ENUM
Polling cadence — every 3–5 seconds; once in manual handling (caseStatus ∈ {UNASSIGNED, UNDER_REVIEW, RETURNED}) you can back off to 30–60 seconds.

Recovery Query (Dedup & Resume)

Endpoint: workflow.apply.query POST

Looks up whether a customer already has an execution by eventType + source + customerId. Read-only — it starts no new execution and is not billed. Use apiVersion 1.0.0.

Request

{
  "eventType": "KYC_EVENT",
  "source": "API",
  "customerId": "demo-customer-001"
}
Field Type Required Description
eventType string Yes Same as at submit time.
source string Yes Same as at submit time.
customerId string Yes Same as data.customerId at submit time (must stay stable for the same customer).

Response

{
  "apiCode": 200,
  "apiMsg": "OK",
  "data": {
    "code": 200,
    "executionId": "exec-7208f03b931e4897",
    "status": "RUNNING"
  }
}
Match executionId status Suggested action
Customer already approved most recent COMPLETED execution APPROVED Do not submit again.
An execution is in flight that execution RUNNING Switch to polling workflow.execution.status.
No match (rejected or no record) empty — Call submit normally.
customerId stability — the customerId must exactly match the data.customerId used at submit time and stay stable for the same customer over time; otherwise the recovery query will never find a match.

Error Codes

Layer Code Meaning Action
Gateway apiCode 200 Success (check data.code for the business result) —
Gateway apiCode 999302 API name error Check api / apiVersion
Gateway apiCode 999305 apiKey invalid or disabled Verify the API Key on the API & Integration page
Gateway apiCode 999001 Signature error Check the sign computation (sign the raw body, no extra newlines)
Gateway apiCode 999401 Service timeout Retry later; contact support if it persists
data.code 400 Missing parameter Check the request body
data.code 403 Quota exhausted (submit only) Ask an admin to top up / adjust the quota
empty data — Execution does not exist or belongs to another tenant/domain (status only) Verify executionId against your apiKey / x-domain; always send the required x-domain header

Code Example

The curl commands are individual call recipes. Inspect the recovery response before running submit: APPROVED means do not submit again; RUNNING means resume status polling. After a submit timeout, query before retrying because submit is billed. The Python sample requires a cancellation enum confirmed by engineering before it can run.

curl (full loop)

API_KEY="ak_xxx"
API_SECRET="sk_xxx"
DOMAIN_ID="{domainId}"
BASE_URL="https://api-gateway-platform.width.com/gateway"

# HMAC-SHA256 sign helper / 签名助手
sign() { printf "%s" "$1" | openssl dgst -sha256 -mac HMAC -macopt key:"$API_SECRET" | sed 's/^.* //'; }

# 0. (optional) recovery query — dedup before submit / 恢复查询防重
BODY='{"eventType":"KYC_EVENT","source":"API","customerId":"demo-customer-001"}'
curl -s -X POST "${BASE_URL}?api=workflow.apply.query&apiVersion=1.0.0&apiKey=${API_KEY}&sign=$(sign "$BODY")" \
  -H "Content-Type: application/json" -H "x-domain: ${DOMAIN_ID}" -d "$BODY"

# 1. submit / 发起工作流
BODY='{"eventType":"KYC_EVENT","source":"API","data":{"customerId":"demo-customer-001","fullName":"ZHANG SAN"},"workflowId":""}'
curl -s -X POST "${BASE_URL}?api=workflow.apply.submit&apiVersion=2.0.0&apiKey=${API_KEY}&sign=$(sign "$BODY")" \
  -H "Content-Type: application/json" -H "x-domain: ${DOMAIN_ID}" -d "$BODY"
# -> record data.executionId / 记下 data.executionId

# 2. poll status / 轮询执行状态
BODY='{"executionId":"exec-7208f03b931e4897"}'
curl -s -X POST "${BASE_URL}?api=workflow.execution.status&apiVersion=1.0.0&apiKey=${API_KEY}&sign=$(sign "$BODY")" \
  -H "Content-Type: application/json" -H "x-domain: ${DOMAIN_ID}" -d "$BODY"

Python (polling loop)

import hashlib, hmac, json, os, time, requests

GATEWAY = "https://api-gateway-platform.width.com/gateway"
API_KEY = os.environ["WIDTH_API_KEY"]
API_SECRET = os.environ["WIDTH_API_SECRET"]
DOMAIN_ID = os.environ["WIDTH_DOMAIN_ID"]
# Supply only the cancellation enum verified by WIDTH engineering.
# Do not guess between the conflicting spellings in the reference.
CANCELLED_STATUS = os.environ["WIDTH_CONFIRMED_CANCEL_STATUS"]

def call(api, version, payload):
    body = json.dumps(payload, separators=(",", ":"))
    signature = hmac.new(API_SECRET.encode(), body.encode(), hashlib.sha256).hexdigest()
    response = requests.post(GATEWAY,
        params={"api": api, "apiVersion": version, "apiKey": API_KEY, "sign": signature},
        headers={"Content-Type": "application/json", "x-domain": DOMAIN_ID},
        data=body, timeout=30)
    response.raise_for_status()
    result = response.json()
    if result.get("apiCode") != 200:
        raise RuntimeError(result)
    data = result.get("data")
    if isinstance(data, dict) and data.get("code", 200) != 200:
        raise RuntimeError(data)
    return data

identity = {"eventType": "KYC_EVENT", "source": "API", "customerId": "demo-customer-001"}
recovered = call("workflow.apply.query", "1.0.0", identity)
if recovered and recovered.get("status") == "APPROVED":
    print(recovered)
else:
    if recovered and recovered.get("status") == "RUNNING":
        execution_id = recovered["executionId"]
    else:
        submitted = call("workflow.apply.submit", "2.0.0", {
            "eventType": identity["eventType"], "source": identity["source"],
            "data": {"customerId": identity["customerId"], "fullName": "ZHANG SAN"},
            "workflowId": ""})
        execution_id = submitted["executionId"]
        # Persist execution_id before polling; recover after an interrupted submit.
    while True:
        state = call("workflow.execution.status", "1.0.0", {"executionId": execution_id})
        if not isinstance(state, dict):
            raise RuntimeError("Verify executionId, apiKey and x-domain; no execution returned")
        if state.get("caseStatus") in ("APPROVED", "REJECTED", "CLOSED") or state.get("status") in ("FAILED", CANCELLED_STATUS):
            print(state)
            break
        time.sleep(30 if state.get("caseStatus") in ("UNASSIGNED", "UNDER_REVIEW", "RETURNED") else 5)

FAQ

Q: submit timed out and I’m not sure it started — what now?
Do not retry submit directly (you’d be billed twice). Call workflow.apply.query first: on a RUNNING / APPROVED hit, recover the executionId and resume polling; only re-submit if there is no match.
Q: status can’t find the execution I just submitted (empty data)?
Confirm you are using the same apiKey and x-domain as at submit time — the endpoint checks ownership by tenant and domain, and a cross-domain query returns empty data.
Q: is it a problem if an execution sits in RUNNING / WAITING for a long time?
Not necessarily. While stopped at a review node awaiting manual handling, the execution status stays non-terminal and caseStatus is UNASSIGNED / UNDER_REVIEW / RETURNED. Use caseStatus to track handling progress.
Cancellation contract pending confirmation: the lifecycle reference uses CANCELED while the KYC reference uses CANCELLED. Stop on a confirmed cancellation, but confirm the exact API enum with WIDTH engineering before deploying cancellation-dependent code. Neither spelling is established here as definitive.

Configuration

Retrieve platform configuration data (country lists, document types, form templates, etc.) before starting a workflow.

Endpoint: prepare.avira.config POST

{
  "keys": "particular,avira_ssic,avira_ssoc,avira_countries,avira_document_master,avira_document_default_rule,required-form,avira_phone_code,avira_entity_types,ownership_structure_layer,transaction_behaviour,product_service_complexity,source_of_wealth,avira_source_of_funds"
}

Configuration Keys

Key Description Used By
particular Personal/corporate information field definitions Particular nodes (KYC & KYB)
required-form Form templates with validation rules Required Forms nodes
avira_countries Country list (ISO codes + names) Nationality, residence, etc.
avira_document_master Supported document types per country OCR & document upload
avira_document_default_rule Default document requirements Required document nodes
avira_ssic Industry classification codes Industry field
avira_ssoc Occupation classification codes Occupation field
avira_phone_code Country phone codes Phone verification
avira_entity_types Corporate entity types KYB particular
ownership_structure_layer Ownership structure depth settings KYB workflows
transaction_behaviour Transaction behaviour categories KYB forms
product_service_complexity Product/service complexity categories KYB forms
source_of_wealth Source of wealth options KYC/KYB forms
avira_source_of_funds Source of funds options KYC/KYB forms
Caching — Configuration data changes infrequently. Cache it locally and refresh daily or on demand.

File Upload

For any API requiring S3 URIs (documents, photos), upload files first.

Step 1 — Get Upload Token

Endpoint: storage.upload.token POST

Request body: {}. Token is valid for 15 minutes.

Response:

{
  "apiCode": 200,
  "data": {
    "uploadToken": "a1b2c3d4e5f6...",
    "expiresIn": 900
  }
}

Step 2 — Upload File

Storage Service URL:

Environment URL
Staging https://api-gateway-platform.width.com/storage
Production https://api-gateway.width.com/storage

Multipart upload:

curl -X POST "${STORAGE_URL}/api/v1/upload/file" \
  -F "uploadToken=a1b2c3d4e5f6..." \
  -F "file=@passport_front.jpg"

Base64 upload:

curl -X POST "${STORAGE_URL}/api/v1/upload" \
  -H "Content-Type: application/json" \
  -d '{
    "uploadToken": "a1b2c3d4e5f6...",
    "image": "data:image/jpeg;base64,/9j/4AAQ...",
    "filename": "passport_front.jpg"
  }'

Response:

{
  "apiCode": 200,
  "data": {
    "url": "s3://workflow-kyc/tenantImages/2026-04-16/abc123.jpg",
    "filename": "passport_front.jpg",
    "content_type": "image/jpeg",
    "size": 245760
  }
}
The S3 URI is an internal reference, not a publicly accessible URL. Do not attempt to download files using this URI directly.
Supported types: PNG, JPG, JPEG, GIF, WEBP, TIFF, PDF — Max size: 20 MB
Token expired (HTTP 401)? Obtain a new token and retry the upload.

Error handling & retry strategy

ScenarioAction
SIGN_ERROR / 999001Fix the signature; do not retry unchanged.
999304Wait and retry after rate limiting.
HTTP 500 / 999401Retry up to three times with exponential backoff (1 s, 2 s, 4 s); recover a potentially accepted submit with workflow.apply.query first.
data.code = 403Ask an administrator to replenish or adjust the event quota; do not retry blindly.
Upload token HTTP 401Obtain a new upload token and retry the upload once.
OTP verificationResend the OTP if the maximum attempt count is exhausted.
Node submission timeoutRetry the same callbackKey for node-data submissions; do not generalise this guarantee to workflow.apply.submit.
Polling cadence: 3–5 seconds during automated processing, or 30–60 seconds during manual handling. Keep polling for OTHER_GROUP until the case reaches APPROVED, REJECTED or CLOSED; stop on FAILED or a confirmed cancellation. The guide also states a generic 30-minute polling cap, which conflicts with long manual reviews. The stopping policy beyond that window is pending engineering confirmation; do not treat a timer as an approval or rejection.