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 'Content-Type: application/json' \
--data '{
"userId": "customer-member-123",
"locale": "en"
}'The 201 Created response contains a vitalScanResultId and a
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",
"vitalScanUrl": "https://www.gofa.app/vital-scan/<generated-session-id>#scanToken=<temporary-session-token>"
}
}Open vitalScanUrl 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 against your own user record on the backend, and deliver
the hosted URL only to that authenticated user. Prevent double-clicks and
duplicate create jobs. Do not automatically retry this POST after a timeout or
5xx; 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 10 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. Resume later using the same ID.
async function pollVitalScan(sessionId, apiKey) {
const deadline = Date.now() + 10 * 60_000;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
let intervalMs = 10_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(10_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, deletion, and billing behavior, continue to Vital Scan API.