API reference

Introduction #

Aptlab is a REST API for verifying that a piece of data is well-formed. It covers 29 types across identity documents, business registrations, financial instruments, contact details and web data. Every type is validated with its real structural rules and, where one exists, its actual check digit.

The API has a small surface. Base URL for all requests:

base url
https://api.aptlab.dev/v1

Requests and responses are JSON. Every response carries a top-level ok boolean and an X-Request-Id header you can quote in support threads.

Scope. A valid: true result means the value is structurally and mathematically sound. It does not assert that the identifier was issued, is active, or belongs to a given person — see data handling for what each type can and cannot prove.

Quickstart #

Verify a GSTIN. Substitute your key for $APTLAB_KEY.

shell
curl -X POST https://api.aptlab.dev/v1/verify/gstin \
  -H "Authorization: Bearer $APTLAB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"value": "29AAECS1234K1Z9"}'

A GET works too, which is handy for a quick check from a shell:

shell
curl "https://api.aptlab.dev/v1/verify/ifsc?value=HDFC0000123" \
  -H "Authorization: Bearer $APTLAB_KEY"
Never put a real key in a GET URL or client-side code. Use POST with the header from your backend, and issue a scoped key for anything that runs in a browser.

Authentication #

Pass your key as a bearer token:

http
Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxxxxxx

An X-API-Key header is accepted as an alternative. Keys are hashed at rest — the plaintext is shown once, at creation, and cannot be recovered afterwards. Rotate rather than recover.

Key scoping #

A key can be restricted to a subset of types with allowedTypes. Use this to keep high-sensitivity types away from keys that ship anywhere near a client. Requests outside a key's scope return 403 type_not_permitted.

json · request
{
  "name": "web-signup-form",
  "plan": "growth",
  "allowedTypes": ["email", "phone", "pincode"]
}

Result envelope #

Every verification returns the same shape regardless of type, so client code branches once and works for all 29.

FieldTypeMeaning
typestringWhich validator ran.
inputstringThe value as received. Redacted for sensitive types.
normalizedstringCanonical form — store this, not the raw input.
validbooleanDid every applied check pass.
confidencenumber0–1. How strongly the passing checks constrain the value.
checksobjectNamed sub-check → true, false, or null (not evaluated).
reasonsstring[]Machine-readable failure codes. Empty when valid.
metaobjectAnything decoded from the value — state, brand, issuer, age.

On null checks #

A check set to null was not evaluated — either the option was off, or a network lookup timed out. It is never a failure. Treat null as "unknown" and decide for yourself whether to escalate.

On confidence #

Confidence reflects how hard the value would be to fake by accident. An IBAN scores 0.95 — MOD 97-10 catches essentially every transposition. An Indian bank account number scores 0.5, because no checksum exists to run. Gate expensive downstream verification on this number rather than treating every pass as equal.

Verify by type #

POST /v1/verify/{type}
GET /v1/verify/{type}?value=…

Body parameters

NameTypeRequiredNotes
valuestringyesMax 512 characters.
optionsobjectnoPer-type options — see the type reference.

Example

POST /v1/verify/email
{
  "value": "[email protected]",
  "options": { "checkMx": true }
}
A failed verification is still HTTP 200 — the request succeeded, the data did not. Non-2xx status codes mean something is wrong with the request: bad key, unknown type, quota exhausted. Branch on result.valid, not on the status code.

Verify with detection #

POST /v1/verify

Omit the type and Aptlab infers it, verifies against the best match, and tells you what else it considered. Useful for a single "paste anything" field.

200 OK
{
  "ok": true,
  "detected": {
    "type": "cin",
    "confidence": "inferred",
    "alternatives": []
  },
  "result": { /* standard envelope */ }
}

Pass type explicitly when you already know it. Detection is a convenience, not a substitute for a typed schema — an ambiguous 12-digit number could be an Aadhaar, a UAN or a card.

Detect type #

POST /v1/detect

Returns the ranked candidates without verifying anything.

200 OK
{
  "ok": true,
  "value": "29AAECS1234K1Z9",
  "candidates": [
    { "type": "gstin", "label": "GSTIN", "category": "Business" }
  ]
}

Batch verify #

POST /v1/batch

Verify a whole record in one round trip. Items are independent — each may set its own type and options, and one bad item never fails the request. Every item counts as one verification against your quota.

POST /v1/batch
{
  "items": [
    { "id": "pan_field",   "type": "pan",  "value": "ABCPD1234E" },
    { "id": "gst_field",   "type": "gstin", "value": "29AAECS1234K1Z9" },
    { "id": "email_field", "value": "[email protected]" }
  ]
}

Per-plan batch ceilings: 25 (Sandbox), 250 (Growth), 1,000 (Scale). Exceeding one returns 400 batch_limit_exceeded with the limit in error.details.

Metadata #

Unauthenticated and unmetered. Useful for building a dynamic form.

GET /v1/health
GET /v1/types
GET /v1/types/{type}
GET /v1/plans
GET /v1/whoami

/v1/whoami echoes back which key you presented, its plan, its scope and its month-to-date usage — the fastest way to confirm a deploy picked up the right credential.

Key management #

These endpoints require your admin key, not a regular API key.

POST /v1/keys
GET /v1/keys
GET /v1/keys/{id}
DELETE /v1/keys/{id}
201 Created
{
  "ok": true,
  "key": {
    "id": "key_8f2c1ab90d3e4c77",
    "name": "web-signup-form",
    "plan": "growth",
    "keyPrefix": "pk_live_a9Kd...",
    "allowedTypes": ["email", "phone"],
    "active": true
  },
  "secret": "pk_live_a9Kd…",
  "warning": "Store this secret now. It cannot be retrieved again."
}

Revoking is immediate and irreversible. Requests with a revoked key return 403 api_key_revoked.

Type reference #

All supported types, their options, and what each one can actually prove. Click a row to open it in the playground.

Reason codes #

reasons is a stable, machine-readable list. Match on these strings rather than on human-readable text.

CodeMeaning
invalid_lengthWrong number of characters for the type.
invalid_formatStructural pattern did not match.
checksum_failedCheck digit does not match the computed value.
luhn_checksum_failedCard or IMEI failed the Luhn check.
unknown_state_codeEmbedded state or region code is not assigned.
unknown_entity_codePAN holder-type character is not a known code.
disposable_domainEmail domain is a known throwaway mailbox provider.
domain_has_no_mx_recordsMX lookup ran and found nothing — the domain cannot receive mail.
invalid_local_partMailbox name violates syntax rules.
invalid_national_number_lengthPhone length is wrong for the detected country.
length_not_valid_for_brandCard length does not match its detected brand.
embedded_credentialsURL carries a username or password.
not_a_real_calendar_dateDate overflowed, e.g. 30 February.
below_minimum_age_{n}Derived age is under the requested floor.
implausible_issue_yearEmbedded year is before 1950 or in the future.

Errors #

Errors share one shape:

4xx
{
  "ok": false,
  "requestId": "req_9c41a7f2b8d3e05a",
  "error": {
    "code": "unsupported_type",
    "message": "Unsupported verification type \"passport_uk\".",
    "details": { "supported": ["email", "phone"] }
  }
}
StatusCodeCause
400missing_valueNo value in the body or query.
400value_too_longValue exceeds 512 characters.
400batch_limit_exceededMore items than your plan allows.
401missing_api_keyNo credential presented.
401invalid_api_keyKey not recognised.
403api_key_revokedKey was revoked.
403type_not_permittedType is outside this key's scope.
404unsupported_typeNo validator for that type name.
422type_not_detectedDetection found no candidate; pass type.
429rate_limit_exceededPer-minute ceiling hit. See Retry-After.
429monthly_quota_exceededPlan quota exhausted for the month.
500internal_errorQuote requestId when reporting.

Rate limits #

Every metered response carries the current window state:

http · response headers
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 297
X-RateLimit-Reset: 1785312000
Retry-After: 41          # only on 429
PlanPer minutePer monthBatch max
Anonymous2010
Sandbox301,00025
Growth300100,000250
Scale2,0005,000,0001,000

Retry 429 with exponential backoff, honouring Retry-After. Monthly quotas reset at 00:00 UTC on the 1st.

Data handling #

Verification is stateless. Values are held in memory for the life of the request and are not written to durable storage.

Redacted types

For aadhaar, card, bank_account and password, the response input is replaced with [redacted] and meta carries a masked form instead. These values never appear in request logs.

What each type can prove

ClaimAptlabNeeds
Value is well-formedyes
Check digit is correctyes
Domain can receive mailyescheckMx: true
Identifier was actually issuednoIssuer registry lookup
Belongs to a named personnoConsent-bound eKYC
Bank account is live and ownednoPenny-drop
Phone is reachable right nownoHLR lookup or OTP
Regulatory note. In India, collecting or storing Aadhaar numbers is restricted under the Aadhaar Act and the DPDP Act. Validating a checksum client-side to catch typos is materially different from storing the number. Prefer Virtual IDs or an authorised eKYC provider when you need to retain anything.