This page is for developers building an integration that calls VBAPI for longer than a single script run. It explains the login flow, the challenges login can return, how to send and reuse the ID token, and how to keep credentials safe.
VBAPI uses two steps. You log in once with your username and password and receive tokens. You then send the ID token as a Bearer token on every other call until it expires.
POST /user-authentication has no body. It reads your username and password from a Basic Authorization header and needs these headers:
| Header | Value |
|---|---|
x-api-key | Your API key. |
Authorization | Basic <base64 of username:password>. |
vbasoftware-client-id | Your client ID. |
vbasoftware-client-code | Your client code. |
vbasoftware-database | The database to log in to, for example YOUR_DATABASE. |
vbasoftware-machine-id | Optional. An identifier for the calling machine. |
User-Agent | Recommended: your integration name, for example YourCompany-Integration/1.0. |
The shared client used by the samples logs in like this:
public async Task AuthenticateAsync()
{
using var request = new HttpRequestMessage(HttpMethod.Post, $"{_baseUrl}/user-authentication");
var basic = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_username}:{_password}"));
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", basic);
request.Headers.Add("x-api-key", _apiKey);
request.Headers.Add("vbasoftware-client-id", _clientId);
request.Headers.Add("vbasoftware-client-code", _clientCode);
request.Headers.Add("vbasoftware-database", _database);
request.Headers.UserAgent.ParseAdd(_userAgent);
using var response = await _http.SendAsync(request);
var body = await ReadJsonAsync(response);
if (response.StatusCode != HttpStatusCode.OK)
throw new VbapiException(response.StatusCode, body?["error"], body?["debug"]?["activityID"]?.GetValue<string>());
var login = body!["data"]!;
var challenge = login["challengeName"]?.GetValue<string>();
if (!string.IsNullOrEmpty(challenge))
throw new VbapiChallengeException(challenge, login["session"]?.GetValue<string>());
var result = login["authenticationResult"]!;
_idToken = result["idToken"]!.GetValue<string>();
var expiresIn = result["expiresIn"]?.GetValue<int>() ?? 3600; // seconds
_expiresAt = DateTimeOffset.UtcNow.AddSeconds(expiresIn - 60); // refresh one minute early
}
A successful login returns the tokens in data.authenticationResult. Example response (illustrative; the token values are fake and shortened, and expiresIn varies, so read it from each response):
{
"data": {
"challengeName": null,
"session": null,
"authenticationResult": {
"accessToken": "eyJraWQiOiJET0NTIn0.fake-access-token",
"idToken": "eyJraWQiOiJET0NTIn0.fake-id-token",
"refreshToken": "fake-refresh-token",
"expiresIn": 3600
}
},
"error": null,
"debug": { "activityID": "00000000-0000-0000-0000-000000000000" }
}Send the idToken, not the accessToken, as Authorization: Bearer <idToken> on every other call, together with x-api-key, vbasoftware-database, and a descriptive User-Agent.
Instead of tokens, login can return a challengeName and a session, with authenticationResult empty. Your code must check for this before it reads the token. The session value ties the next call to this login attempt, so pass it back unchanged.
| Challenge | What it means | Next call |
|---|---|---|
NEW_PASSWORD_REQUIRED | The user still has a temporary password. | POST /change-temporary-password with userID, newPassword, and session. |
| A multi-factor challenge | The user must enter a code from their authenticator. | POST /mfa-submit-code with username, mfaCode, and session. It returns 202 with the same response shape as a login. |
Both calls send vbasoftware-client-id, vbasoftware-client-code, and vbasoftware-database, like login. Their responses have the same shape as a login response, so they can carry tokens or another challenge; check challengeName again.
Example body for POST /change-temporary-password (illustrative; values are fake):
{
"userID": "DOCS-APIUSER",
"newPassword": "a-new-strong-password",
"session": "session-value-from-the-login-response"
}An unattended integration cannot answer a challenge by itself. The shared client stops with a clear message when login returns one. Complete the challenge once, by hand or with a small one-off script, and then run the integration again.
expiresIn is the token's lifetime in seconds. Do not hard-code a lifetime; read expiresIn from each login response.
- Log in once and reuse the ID token for every call until shortly before it expires. Do not log in for each request.
- Share one client (and so one token) across your process rather than logging in from every worker.
- Renew the token a little early. The shared client renews one minute before
expiresInruns out. - If a call returns
401anyway, log in again once and retry the call. The shared client does this; see Errors and troubleshooting for the code.
The shared client keeps the token and its expiry time in memory and checks them before every call, as shown in the login code above.
Login also returns a refreshToken. POST /user-refresh-tokens exchanges it for new tokens. It takes the expired ID token in the expired-id-token header and the refresh token in the refresh-token header, plus vbasoftware-client-id, vbasoftware-client-code, and vbasoftware-database, and returns the same shape as a login. The samples log in again instead, which needs nothing beyond the credentials you already have.
If a user forgets their password, POST /forgot-password with the userID sends the user a confirmation code. POST /confirm-forgot-password then sets the new password, with userID, password, and confirmationCode. Both send vbasoftware-client-id, vbasoftware-client-code, and vbasoftware-database. For an API service user, it is usually simpler to ask whoever issued your credentials.
| What you see | Likely cause | Fix |
|---|---|---|
401 from POST /user-authentication | Wrong username or password, or the wrong client ID, client code, or database for that user. | Check each value with whoever issued your credentials. |
challengeName: NEW_PASSWORD_REQUIRED instead of tokens | The user still has a temporary password. | Set a permanent password with POST /change-temporary-password, then log in again. |
401 on a data call | The ID token is missing, expired, or not sent as Bearer. | Log in again, and send Authorization: Bearer <idToken>. |
403 with {"message":"Forbidden"} on every call | The x-api-key header is missing or wrong. | Send your API key in x-api-key on every request, login included. |
429 | More than 25 requests per second (burst 50) on your API key. | Slow down and retry with backoff. The samples do this for you. |
| Login or data calls fail for a database you expect to use | The vbasoftware-database value does not match a database your user can reach. | Use the database name you were given, exactly as issued. |
- Load the API key, client ID, client code, username, and password from a secrets manager or environment variables, never from source code.
- Keep tokens in memory. Do not write them to disk or share them between systems.
- Never log passwords, tokens, the API key, or full
Authorizationheaders. Log thedebug.activityIDinstead. - Rotate the password and API key according to your security policy, and straight away if they may have been exposed.
Send VBA support:
- the
debug.activityIDfrom the response - the date and time of the call, with time zone
- the method and path, for example
POST /claim-batches - the full
errorobject from the response - the request body, with passwords, tokens, and personal data removed
Never send your password, API key, or tokens. See Support and escalation.
- Quick start: log in and make a first call.
- Credentials and environments: what VBA issues and how to request it.
- Requests and responses: headers, methods, and status codes.