Create a hosted Vital Scan assessment and retrieve its result.
The Vital Scan API lets your backend create a hosted assessment for a user. GOFA hosts the assessment experience and returns the result through the session API.
vitalScanUrl to your web application or mobile WebView,
or open it in a top-level browser window.vitalScanResultId until it reaches a terminal status.The returned URL is the integration boundary for the hosted experience. Inline
<iframe> embedding is not supported.
The Production API base URL is:
https://www.gofa.appVital Scan is a server-to-server API. Keep your API key in your backend and do not place it in browser code, frontend bundles, mobile apps, WebViews, URLs, or public repositories. See Authentication for the required bearer header.
Do not send x-request-id. GOFA creates a request ID for every request and
returns it in the x-request-id response header. Error responses also include
the same value as error.requestId, so you can give it to GOFA Support without
exposing credentials or user data.
POST /api/v1/vital-scan/sessionsThe request body is strict: send only the fields documented below.
| Field | Required | Description |
|---|---|---|
userId | Yes | Your system's opaque user identifier. Leading and trailing whitespace is trimmed; after trimming, it must be 1–128 characters and may contain only ASCII letters, numbers, ., _, :, @, +, or -. Embedded whitespace and / are not allowed. |
locale | No | One of the currently supported values: en, zh, zh-Hant, zh-Hans, or zh-TW. |
returnUrl | No | An HTTPS URL up to 2,048 characters. Its origin must already be approved for your workspace. |
returnUrl is optional. If you use it, your workspace administrator must first
add the exact HTTPS origin under Vital Scan's Allowed return origins in the
customer portal (API Services → Vital Scan → Quick start). See
workspace preparation. A path or query
string may be included in the request URL, but credentials must not be included. If
the supplied URL contains a fragment, GOFA clears it when navigating back;
fragments are not preserved in the return navigation. The origin must match the
approved origin.
curl --request POST 'https://www.gofa.app/api/v1/vital-scan/sessions' \
--header 'Authorization: Bearer <GOFA_API_KEY>' \
--header 'Content-Type: application/json' \
--data '{
"userId": "customer-member-123",
"locale": "en",
"returnUrl": "https://your-app.example/vital-scan/complete"
}'Replace the example return URL only with an HTTPS URL whose origin is approved
in your workspace. The response to a successful request is 201 Created and includes a
GOFA-generated request ID header:
HTTP/1.1 201 Created
x-request-id: <GOFA-generated-request-id>{
"data": {
"vitalScanResultId": "<generated-session-id>",
"userId": "customer-member-123",
"status": "created",
"createdAt": "2026-08-28T09:00:00.000Z",
"updatedAt": "2026-08-28T09:00:00.000Z",
"startedAt": null,
"completedAt": null,
"failedAt": null,
"expiresAt": "2026-08-28T11:00:00.000Z",
"returnUrl": "https://your-app.example/vital-scan/complete",
"error": null,
"metrics": null,
"vitalScanUrl": "https://www.gofa.app/vital-scan/<generated-session-id>#scanToken=<hosted-session-token>"
}
}The fragment in vitalScanUrl contains a temporary Hosted session credential.
Give this URL only to the intended user for this session. Do not log or
persist it, include it in analytics events, or forward it to another user. It
is not an API key.
Every successful POST creates a new independent session and consumes one
available point. Sending the same body again creates another session and
consumes another point. The endpoint does not support a customer-supplied
idempotency key.
A session is valid for two hours from creation; expiresAt is the end of that
window. An unfinished session can be resumed in the same hosted experience and
the user can retry the scan without creating another session or consuming
another point. If a user leaves before completion, the session remains
non-terminal until it is completed or expires.
The hosted experience can move through these states:
| Status | Meaning |
|---|---|
created | The session exists and has not started the hosted flow. |
in_progress | The hosted flow has started and is not complete. |
completed | The assessment completed; read the available metrics, which may be partial or absent. |
failed | The assessment ended with an error. |
expired | The two-hour session window ended before completion. |
When the assessment completes, the GOFA hosted page shows the result. The point used to create a session is not refunded if the session fails, expires, or the user leaves the hosted flow. Deleting a session is also not a refund.
If you provide an approved returnUrl, an ordinary hosted session offers a
return action after completion. If the user exits the hosted flow before
completion, GOFA navigates to the URL immediately. GOFA navigates to that URL
with these query parameters:
vital_scan_session_id: the session ID.status: completed or exited.The return URL does not receive a bearer token or health data, and the hosted
token fragment is removed before navigation. Without a returnUrl, the user
stays on the GOFA hosted completion or exit page.
Treat the return query parameters as navigation hints, not trusted result data.
Your backend must verify the session with GET and associate it with the correct
signed-in user. An exited return does not mean the session is failed.
vitalScanUrl, including its token fragment, in a
top-level browser or camera-capable WebView. Do not strip the fragment, embed
the page in an iframe, or append your Commercial API key.This is an integration acceptance checklist, not a certified browser/version support matrix. The public API contract does not guarantee every embedded browser or OS version. Confirm your target device matrix with GOFA before rollout.
GET /api/v1/vital-scan/sessions/{vitalScanResultId}Reading a session does not consume a point, but it still requires bearer
authentication and is subject to the configured requests-per-minute limit. The
same session may be polled no more than once every five seconds. A poll that is
too soon returns 429; wait for the Retry-After response header before
polling again.
curl --request GET \
'https://www.gofa.app/api/v1/vital-scan/sessions/<vitalScanResultId>' \
--header 'Authorization: Bearer <GOFA_API_KEY>'For example, an in-progress session returns 200 OK with metrics: null:
{
"data": {
"vitalScanResultId": "<generated-session-id>",
"userId": "customer-member-123",
"status": "in_progress",
"createdAt": "2026-08-28T09:00:00.000Z",
"updatedAt": "2026-08-28T09:01:05.000Z",
"startedAt": "2026-08-28T09:00:45.000Z",
"completedAt": null,
"failedAt": null,
"expiresAt": "2026-08-28T11:00:00.000Z",
"returnUrl": null,
"error": null,
"metrics": null
}
}The response DTO contains:
| Field | Description |
|---|---|
vitalScanResultId | The session identifier used in later GET and DELETE requests. |
userId | The opaque identifier supplied when the session was created; the response type also permits null when unavailable. |
status | One of created, in_progress, completed, failed, or expired. |
createdAt, updatedAt, startedAt, completedAt, failedAt, expiresAt | ISO 8601 timestamps. Fields for lifecycle events that have not happened yet are null. |
returnUrl | The approved return URL, or null when no return URL was supplied. |
error | null unless the session failed; a failed session contains an object with a message. |
metrics | null unless the session is completed and a stored SDK result exists. Otherwise an object containing the available fields below. |
Every measurement field, including nested fields, is optional. Unavailable
fields are omitted, not populated with zero. A completed session may have
metrics: null if no SDK result is stored, or an empty/partial metrics object.
Do not assume completion guarantees any particular measurement. Check for a
numeric value explicitly rather than using a truthiness check: zero and missing
are different. Field names are case-sensitive.
| Field | JSON type when present | Unit / interpretation |
|---|---|---|
hr | number | Heart rate, beats/minute. |
hrv | number | Heart rate variability, milliseconds. Do not assume this is specifically RMSSD or SDNN. |
sbp, dbp | number | Systolic / diastolic blood pressure, mmHg. |
spo2 | number | Oxygen saturation, percent (for example, 98, not 0.98). |
rr | number | Respiratory rate, breaths/minute; unrelated to API requests/minute. |
si | number | SDK stress index; no physical unit. |
hrv_indices | object | Optional numeric HF, LF, LF_HF_RATIO, MEAN_RR, PHF, PLF, RMSSD, RRIV, SDNN, SD1, SD2, SDNNI. Units and normalization are not specified by this public contract. |
check_feature_buffer_for_hrv | number | SDK HRV buffer indicator, not a boolean; no public enum is defined. |
ANSIndex | object | Optional numeric PNS and SNS autonomic indices. |
PRQ, shock_index | number | SDK pulse-respiration quotient and shock index respectively. |
wellness_score, activity, equilibrium, health, metabolism, relaxation, sleep | number | SDK scores/indices; do not assume they all share a scale or represent percentages. |
HbA1c | object | Optional value (number), risk (number), range (number array or string). |
hemoglobin | number | SDK estimate; unit is not specified by this public contract. |
cholesterol | object | Optional value (number), risk (number), range (string). |
cardiovascular_age, cardiovascular_bmi | number | SDK cardiovascular estimates. |
cvd_risk | object | Optional points (number) and percentage (number; SDK 10-year risk percentage). |
afib | string | SDK status text; no fixed set of string values is promised. |
reference_sbp, reference_dbp, calibrated_sbp, calibrated_dbp | number | Reference and calibrated blood pressure, mmHg, when available. |
wrong_input_group_risk, miscalibration_risk | number | SDK risk indicators; no public threshold or enum is defined. |
For advanced SDK fields without an explicit unit or scale above, confirm the unit, interpretation, and availability with GOFA before displaying them to customers. This reference specifies the transport shape, not diagnostic thresholds or clinical interpretation. Internal SDK quality fields and raw capture data are not part of the public metrics response.
This synthetic example includes the full session envelope and a partial set of
measurements. Values illustrate JSON structure, not expected ranges or guaranteed
outputs. GET does not return vitalScanUrl or its temporary credential.
{
"data": {
"vitalScanResultId": "<generated-session-id>",
"userId": "customer-member-123",
"status": "completed",
"createdAt": "2026-08-28T09:00:00.000Z",
"updatedAt": "2026-08-28T09:02:00.000Z",
"startedAt": "2026-08-28T09:00:45.000Z",
"completedAt": "2026-08-28T09:02:00.000Z",
"failedAt": null,
"expiresAt": "2026-08-28T11:00:00.000Z",
"returnUrl": "https://your-app.example/vital-scan/complete",
"error": null,
"metrics": {
"hr": 72,
"hrv": 42.5,
"sbp": 118,
"dbp": 76,
"spo2": 98,
"rr": 16,
"hrv_indices": { "RMSSD": 38.2, "SDNN": 42.5 },
"ANSIndex": { "PNS": 0.4, "SNS": -0.2 },
"PRQ": 4.5
}
}
}The API is addressed by vitalScanResultId; it does not provide a list,
search, or webhook endpoint. The public contract does not promise a generic
retention period, so keep the identifier and any application records you need
in your own systems.
DELETE /api/v1/vital-scan/sessions/{vitalScanResultId}Deletion is free and returns 204 No Content when the session belongs to your
workspace. It is a soft delete: subsequent access to that session returns
404, and the original point is not refunded.
curl --request DELETE \
'https://www.gofa.app/api/v1/vital-scan/sessions/<vitalScanResultId>' \
--header 'Authorization: Bearer <GOFA_API_KEY>'All responses include a GOFA-generated x-request-id. For errors from the
common API access boundary, the JSON envelope is:
{
"error": {
"code": "<public-error-code>",
"message": "<human-readable-message>",
"requestId": "<GOFA-generated-request-id>"
}
}The current public codes are:
| Status | error.code | Applies to |
|---|---|---|
400 | invalid_request | The POST body, locale, or returnUrl is invalid. |
400 | return_origin_not_approved | The returnUrl is HTTPS but its origin is not approved. |
401 | invalid_api_key | The bearer key is missing, invalid, expired, or revoked. |
402 | insufficient_credits | The account has no available points for a new session. The response also includes remaining, cycleId, and resetsAt. |
403 | product_not_available | Vital Scan is not available for the customer workspace. |
403 | product_not_entitled | The authenticated key is not entitled to Vital Scan. |
403 | entitlement_inactive, entitlement_expired, free_membership_required | The customer access required by the API is not active. |
429 | rate_limit_exceeded | The configured requests-per-minute limit was exceeded. |
503 | entitlement_unavailable, rate_limit_unavailable, free_membership_unavailable, free_allowance_unavailable, vital_scan_creation_unavailable, commit_outcome_unknown, usage_event_unavailable | A required access, protection, session-creation, or successful-usage-recording dependency is temporarily unavailable. |
500 | vital_scan_creation_failed | An unexpected error prevented session creation. |
Errors returned by the GET and DELETE handlers use the same JSON envelope shape
but intentionally return only error.message and error.requestId; those
handler responses do not promise an error.code for 404, the per-session
polling 429, or route 500 responses. The common access boundary can still
return the codes listed above before a handler runs.
The per-session 429 message explains the five-second polling interval and
includes Retry-After. This is distinct from the common requests-per-minute
rate_limit_exceeded response.
POST with 201) consumes one point.For an end-to-end walkthrough, see Quick Start. For the authentication boundary and retry guidance, see Authentication and Errors & limits.