Concepts

Authentication

Authenticate server-to-server calls with API keys or OAuth2 client credentials. Use the publishable key only in browsers. This page covers key formats, scopes, rotation, environment binding, IP allow-lists, rate limits and security practices.

Overview

All /v1 endpoints require a bearer credential in the Authorization header:

Authorization: Bearer vm_test_EeoZZxa3_R0j4wviRzYMghPw3FiZ4_Fzd4DF-UaCEifKpd2X8mQw

There are three kinds of credential:

CredentialLooks likeUse it forWhere it lives
Secret API keyvm_test_... or vm_live_...Server-to-server API callsYour backend only
OAuth2 access tokenvm_at_...Same API, short-lived (1 hour), obtained with client credentialsYour backend only
Publishable keyvm_pk_test_... or vm_pk_live_...Identifying your workspace to the Web SDK in a browserFront-end code, public

API keys

A key has the form vm_<environment>_<8-char id>_<secret>. The first two parts (vm_test_EeoZZxa3) are the key prefix, used to look the key up and shown in the portal. The rest is the secret. Only a SHA-256 hash of the secret is stored, so a lost key cannot be recovered, only replaced.

  • Create keys in the portal under Developers, API keys. Creating a key requires re-entering your password (step-up). The full key is shown once.
  • Each key has a name, a set of scopes, an optional expiry and an optional IP allow-list.
  • Key last-used time is recorded (at most once a minute) so you can spot dead keys.
  • Revoke a key at any time. Revocation takes effect immediately.

Environment binding

The environment is part of the key. A deployment accepts only keys of its own environment:

DeploymentAcceptsRejects
Sandboxvm_test_...vm_live_... with 401
Productionvm_live_...vm_test_... with 401

Environment mismatch is deliberately indistinguishable from a wrong key: both return 401 VERIFYME_UNAUTHENTICATED. GET /v1/ping returns the environment the server is running (test or live), your tenantId and the key's scopes, which makes it a good health check.

Scopes

Grant the minimum a service needs. A missing scope returns 403 VERIFYME_INSUFFICIENT_SCOPE, with the scope named in the message.

ScopeAllowsDefault for new keys
onboarding.createPOST /v1/onboarding-requests, POST .../{id}/reissue-linkyes
onboarding.readGET /v1/onboarding-requests, GET .../{id}, GET /v1/flowsyes
onboarding.resultGET .../{id}/result, GET .../{id}/documents/{documentId} (personal data)yes
onboarding.cancelPOST .../{id}/cancelyes
webhook.manage/v1/webhook-endpoints (create, list, update, delete, test) and GET /v1/webhook-eventsno
billing.readGET /v1/usageno

A typical split: the service that starts onboarding gets onboarding.create and onboarding.read; the service that reads results gets onboarding.result only; a back-office job that cancels gets onboarding.cancel.

Rotation

Rotate keys at least every 90 days and whenever a person with access leaves. Rotation in the portal issues a new key for the same client and keeps the old one valid for an overlap window (24 hours by default) so you can deploy without downtime. The response includes oldKeyExpiresOn.

  1. Rotate the key in the portal and copy the new key.
  2. Deploy the new key to your secret store and roll your services.
  3. Confirm calls succeed with the new key (lastUsedOn updates on the new key).
  4. The old key stops working at oldKeyExpiresOn, or revoke it immediately.

IP allow-list

A key can be restricted to a list of source IPs. Requests from any other address get 403 VERIFYME_FORBIDDEN ("Request IP is not in the allow-list for this credential.") and a security event is recorded. Enter plain addresses (IPv4 or IPv6); a single host may also be written with /32. Ranges are not matched: list each egress address.

OAuth2 client credentials

If your platform prefers short-lived tokens, exchange a key for an access token. client_id is the key prefix and client_secret is the secret part after the prefix.

# Key: vm_test_EeoZZxa3_R0j4wviRzYMghPw3FiZ4_Fzd4DF-UaCEifKpd2X8mQw
curl -X POST "https://kyc.example.com/v1/oauth/token" \
  -d "grant_type=client_credentials" \
  -d "client_id=vm_test_EeoZZxa3" \
  --data-urlencode "client_secret=R0j4wviRzYMghPw3FiZ4_Fzd4DF-UaCEifKpd2X8mQw"
const [, env, id, secret] = process.env.VERIFYME_API_KEY.match(/^vm_(test|live)_([A-Za-z0-9]{8})_(.+)$/);

const res = await fetch(`${process.env.VERIFYME_HOST}/v1/oauth/token`, {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "client_credentials",
    client_id: `vm_${env}_${id}`,
    client_secret: secret,
  }),
});
const { access_token, expires_in } = await res.json();   // cache it; refresh before expires_in
import os, re, requests

env, key_id, secret = re.match(r"^vm_(test|live)_([A-Za-z0-9]{8})_(.+)$", os.environ["VERIFYME_API_KEY"]).groups()
r = requests.post(
    f"{os.environ['VERIFYME_HOST']}/v1/oauth/token",
    data={"grant_type": "client_credentials", "client_id": f"vm_{env}_{key_id}", "client_secret": secret},
    timeout=15,
)
r.raise_for_status()
token = r.json()["access_token"]
{
  "access_token": "vm_at_QtNRWxcZbpd1tBNLdfG9-GfcIqE6ZW8eHWKeXlxwqgM",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "onboarding.create onboarding.read onboarding.result onboarding.cancel"
}

Use it exactly like a key: Authorization: Bearer vm_at_.... The token carries the same scopes, environment and IP allow-list as the key it was issued for, and is rate limited as that key. There is no refresh token: request a new one shortly before expires_in elapses. The token endpoint accepts application/x-www-form-urlencoded only. An unsupported grant_type returns 400 VERIFYME_VALIDATION_ERROR (UNSUPPORTED_GRANT); bad credentials return 401. Revoking the key stops new tokens; tokens already issued live until they expire.

Publishable key versus secret key

Secret keyPublishable key
Prefixvm_test_ / vm_live_vm_pk_test_ / vm_pk_live_
Secret?Yes. Never ship in browsers or appsNo. Safe in page source
Can create requests or read resultsYes (by scope)No
PurposeAuthenticate server API callsTell the Web SDK / hosted UI which workspace the page belongs to
RotationSelf-service, with overlapTied to the workspace; visible in the portal on the API keys page

The publishable key is checked when an applicant session is opened: if you pass it, it must belong to the same workspace as the link, otherwise the link is treated as invalid. It does not grant any access on its own. Browser origin control is a separate setting, Allowed origins, in the portal.

Rate limits and headers

Every authenticated /v1 response carries rate limit headers:

HeaderMeaning
X-RateLimit-LimitRequests allowed per minute for this key (default 600; deployments can change it)
X-RateLimit-RemainingRequests left in the current one-minute window
X-RateLimit-ResetSeconds until the window resets (not an epoch timestamp)
Retry-AfterOn 429 only: seconds to wait before retrying
HTTP/1.1 200 OK
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 598
X-RateLimit-Reset: 42
X-Request-Id: trc_01M3TRRXR1FPQV13143S

Exceeding the limit returns 429 VERIFYME_RATE_LIMITED. Back off using Retry-After with jitter. In addition: repeated failed authentication from one IP is limited and raises a security alert, and the applicant-facing endpoints have their own limits (OTP sends per request and per number, link opening per IP). More in Errors and limits.

Security best practices

  • Keep secret keys in a secrets manager or environment variables, never in source control, mobile apps, front-end bundles, logs or support tickets.
  • Use a separate key per service and per environment, with only the scopes it needs.
  • Set an IP allow-list on production keys when you have static egress IPs.
  • Rotate on a schedule and on staff changes; use the overlap window to avoid downtime.
  • Create onboarding requests only from your backend. The onboardingUrl is a bearer link: send it only to the intended applicant over a trusted channel, and use reissue-link if it may have leaked.
  • Do not log Authorization headers, onboardingUrl values or result bodies. VerifyMe redacts tokens from its own logs and API call history.
  • Verify webhook signatures on every delivery and reject stale timestamps. See Webhooks.
  • Treat results as personal data under the DPDP Act: restrict who can call onboarding.result, store the minimum, and delete according to your retention policy.
  • Monitor for 401 and 403 spikes. A burst of 401 from an unknown IP can mean a leaked or guessed key.
Docs version 1.0.0API version v1Last updated 1 Oct 2026