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.
launchUrl (also returned as 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.
For each assessment, generate an Idempotency-Key on your backend. Keep the
same key and request body for retries, including retries made with a replacement
API key from the same workspace.
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 'Idempotency-Key: 7dfdc983-1c55-49a3-99b2-238e78ef934a' \
--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",
"firstOpenedAt": null,
"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,
"launchUrl": "https://www.gofa.app/vital-scan/<generated-session-id>#scanToken=<hosted-session-token>",
"vitalScanUrl": "https://www.gofa.app/vital-scan/<generated-session-id>#scanToken=<hosted-session-token>"
}
}The fragment in the launch URL 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.
The first successful POST for an idempotency key returns 201 Created and
consumes the configured Vital Scan price in credits. Replaying the same key
and request in the same workspace returns the original session with 200 OK
and Idempotency-Replayed: true; it does not charge again. A changed request
with the same key returns 409 IDEMPOTENCY_CONFLICT. The key must contain
16–128 ASCII letters, numbers, ., _, :, or -; a UUID is suitable.
Missing or invalid keys return 400 IDEMPOTENCY_KEY_INVALID. Scope is the
workspace and product, so rotating the API key does not change the replay.
A session can accept hosted input 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
more credits. If a user leaves before completion, the session remains
non-terminal until it is completed or expires.
Replaying a creation request never extends expiresAt. Once the session is
terminal or the window has ended, the replay keeps the original ID and returns
launchUrl: null and vitalScanUrl: null.
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 credits used to create a session are not refunded if the session fails, expires, or the user leaves the hosted flow.
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 credits, but it still requires valid 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.
Any active API key owned by the same workspace can read its session; the key that created it is not required. Workspace access can still be disabled.
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",
"firstOpenedAt": "2026-08-28T09:00:30.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 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, firstOpenedAt, startedAt, completedAt, failedAt, expiresAt | ISO 8601 timestamps. Fields for lifecycle events that have not happened yet are null. expiresAt is the hosted input deadline, not a result-retention deadline. |
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",
"firstOpenedAt": "2026-08-28T09:00:30.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.
This public API has no session deletion operation. DELETE on the single
session path returns 405 Method Not Allowed. Keep your own application
records according to your policies; leaving or expiring does not refund the
creation charge.
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 | IDEMPOTENCY_KEY_INVALID | The required Idempotency-Key is missing or 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 or legacy insufficient_credits | The account lacks the full Vital Scan price for a new session. Legacy responses may include cycle details. |
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. |
409 | IDEMPOTENCY_CONFLICT | The same key was used for a different request. |
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 handler 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 the current Vital Scan price in credits; an idempotent 200 replay does not charge again.For an end-to-end walkthrough, see Quick Start. For the authentication boundary and retry guidance, see Authentication and Errors & limits.