Send your first request to the GOFA Vital Scan API.
This guide will help you create your first hosted Vital Scan session.
Before you begin, you need:
Review Authentication before making your first request.
If you do not have a workspace, follow Getting access. Existing customers should use their existing workspace, not register a duplicate.
https://your-app.example. Enter only the HTTPS origin,
without a path, query, or fragment. Otherwise omit returnUrl entirely.Make this request from your backend:
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"
}'The first 201 Created response contains a vitalScanResultId, launchUrl,
and the same URL as the compatibility alias vitalScanUrl. The following
abbreviated response shows only the fields used
in this walkthrough; see Vital Scan API for the complete
response DTO:
{
"data": {
"vitalScanResultId": "<generated-session-id>",
"userId": "customer-member-123",
"status": "created",
"launchUrl": "https://www.gofa.app/vital-scan/<generated-session-id>#scanToken=<temporary-session-token>",
"vitalScanUrl": "https://www.gofa.app/vital-scan/<generated-session-id>#scanToken=<temporary-session-token>"
}
}Open launchUrl for the intended user in a top-level browser window or
WebView. Do not log or reuse the URL for another user. Inline iframe embedding
is not supported.
Store vitalScanResultId and the idempotency key against your own assessment
request on the backend, and deliver the hosted URL only to that authenticated
user. Prevent duplicate create jobs. If the response is lost, retry with the
same key and body. A replay returns the original session with 200 OK and
Idempotency-Replayed: true without another charge; changing the body under
the same key returns 409. A replay never extends the original two-hour
hosted window. See retry behavior.
Before launching in a mobile app, follow the browser and camera checklist.
After the user finishes, read the same session from your backend:
curl --request GET \
'https://www.gofa.app/api/v1/vital-scan/sessions/<generated-session-id>' \
--header 'Authorization: Bearer <GOFA_API_KEY>'The response remains 200 OK while the session is created or in_progress
and has metrics: null. Continue until the status is completed, failed,
or expired. Do not poll the same session more than once every five seconds;
if the API returns 429, wait for its Retry-After header.
Five seconds is a per-session minimum, not a recommended workspace polling rate. A Free workspace configured for 10 requests/minute cannot sustain five-second polling (12 requests/minute). Read the workspace's actual limit; other sessions and API operations share that budget.
The following server-side JavaScript example handles one session, waits at
least 20 seconds between attempts, honors limit headers, backs off on transient
GET failures, and stops after 10 minutes or five consecutive transient failures.
It never creates a replacement session. The deadline only stops this worker;
it does not expire or delete the GOFA session. You can resume polling later
using the same ID; hosted input still closes at the original expiresAt.
async function pollVitalScan(sessionId, apiKey) {
const deadline = Date.now() + 10 * 60_000;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let intervalMs = 20_000;
let waitMs = intervalMs;
let failures = 0;
while (Date.now() + waitMs < deadline) {
await sleep(waitMs);
let response;
try {
response = await fetch(
`https://www.gofa.app/api/v1/vital-scan/sessions/${encodeURIComponent(sessionId)}`,
{
headers: { Authorization: `Bearer ${apiKey}` },
signal: AbortSignal.timeout(Math.min(15_000, deadline - Date.now())),
},
);
} catch {
if (++failures >= 5) throw new Error('GET retries exhausted; retain the session ID');
waitMs = Math.max(intervalMs, Math.min(60_000, 10_000 * 2 ** failures));
continue;
}
const limit = Number(response.headers.get('ratelimit-limit'));
if (limit > 0) intervalMs = Math.max(20_000, Math.ceil(60_000 / limit) + 1_000);
const retryAfter = response.headers.get('Retry-After');
const retryMs = retryAfter === null ? 0 : /^\d+$/.test(retryAfter)
? Number(retryAfter) * 1_000
: Math.max(0, Date.parse(retryAfter) - Date.now()) || 0;
const resetMs = response.headers.get('ratelimit-remaining') === '0'
? Math.max(0, Number(response.headers.get('ratelimit-reset')) * 1_000 - Date.now()) + 1_000
: 0;
waitMs = Math.max(intervalMs, retryMs, resetMs);
if (response.status === 429 || response.status >= 500) {
if (++failures >= 5) throw new Error('GET retries exhausted; retain the session ID');
waitMs = Math.max(waitMs, Math.min(60_000, 10_000 * 2 ** failures));
continue;
}
if (!response.ok) {
throw new Error(`GET failed: ${response.status}; requestId=${response.headers.get('x-request-id') ?? 'unavailable'}`);
}
const { data } = await response.json();
failures = 0;
if (['completed', 'failed', 'expired'].includes(data.status)) return data;
if (!['created', 'in_progress'].includes(data.status)) {
throw new Error('Unexpected session status; retain the session ID');
}
}
throw new Error('Polling deadline reached; retain the session ID to resume later');
}Use this function only in a trusted backend with its API key from server-side configuration. In production, coordinate requests through a workspace-wide queue or limiter, allow only one poller per session, and add jitter to retries. Multiple independent copies of this example do not enforce an aggregate workspace budget. A return navigation can prompt your backend to fetch the result, but its query parameters are not proof of completion: verify the status through GET and check that the session belongs to your application's signed-in user.
For the full request fields, response schema, return navigation, session removal, and billing behavior, continue to Vital Scan API.