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:
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.
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.
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:
curl "https://api.aptlab.dev/v1/verify/ifsc?value=HDFC0000123" \
-H "Authorization: Bearer $APTLAB_KEY"
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:
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.
{
"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.
| Field | Type | Meaning |
|---|---|---|
type | string | Which validator ran. |
input | string | The value as received. Redacted for sensitive types. |
normalized | string | Canonical form — store this, not the raw input. |
valid | boolean | Did every applied check pass. |
confidence | number | 0–1. How strongly the passing checks constrain the value. |
checks | object | Named sub-check → true, false, or null (not evaluated). |
reasons | string[] | Machine-readable failure codes. Empty when valid. |
meta | object | Anything 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 #
Body parameters
| Name | Type | Required | Notes |
|---|---|---|---|
value | string | yes | Max 512 characters. |
options | object | no | Per-type options — see the type reference. |
Example
{
"value": "[email protected]",
"options": { "checkMx": true }
}
result.valid, not on the status code.
Verify with detection #
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.
{
"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 #
Returns the ranked candidates without verifying anything.
{
"ok": true,
"value": "29AAECS1234K1Z9",
"candidates": [
{ "type": "gstin", "label": "GSTIN", "category": "Business" }
]
}
Batch verify #
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.
{
"items": [
{ "id": "pan_field", "type": "pan", "value": "ABCPD1234E" },
{ "id": "gst_field", "type": "gstin", "value": "29AAECS1234K1Z9" },
{ "id": "email_field", "value": "[email protected]" }
]
}
{
"ok": true,
"summary": {
"submitted": 3,
"valid": 3,
"invalid": 0,
"errored": 0,
"validRate": 1
},
"results": [
{ "id": "pan_field", "ok": true, "detected": false,
"result": { /* standard envelope */ } }
]
}
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.
/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.
{
"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.
| Code | Meaning |
|---|---|
invalid_length | Wrong number of characters for the type. |
invalid_format | Structural pattern did not match. |
checksum_failed | Check digit does not match the computed value. |
luhn_checksum_failed | Card or IMEI failed the Luhn check. |
unknown_state_code | Embedded state or region code is not assigned. |
unknown_entity_code | PAN holder-type character is not a known code. |
disposable_domain | Email domain is a known throwaway mailbox provider. |
domain_has_no_mx_records | MX lookup ran and found nothing — the domain cannot receive mail. |
invalid_local_part | Mailbox name violates syntax rules. |
invalid_national_number_length | Phone length is wrong for the detected country. |
length_not_valid_for_brand | Card length does not match its detected brand. |
embedded_credentials | URL carries a username or password. |
not_a_real_calendar_date | Date overflowed, e.g. 30 February. |
below_minimum_age_{n} | Derived age is under the requested floor. |
implausible_issue_year | Embedded year is before 1950 or in the future. |
Errors #
Errors share one shape:
{
"ok": false,
"requestId": "req_9c41a7f2b8d3e05a",
"error": {
"code": "unsupported_type",
"message": "Unsupported verification type \"passport_uk\".",
"details": { "supported": ["email", "phone"] }
}
}
| Status | Code | Cause |
|---|---|---|
| 400 | missing_value | No value in the body or query. |
| 400 | value_too_long | Value exceeds 512 characters. |
| 400 | batch_limit_exceeded | More items than your plan allows. |
| 401 | missing_api_key | No credential presented. |
| 401 | invalid_api_key | Key not recognised. |
| 403 | api_key_revoked | Key was revoked. |
| 403 | type_not_permitted | Type is outside this key's scope. |
| 404 | unsupported_type | No validator for that type name. |
| 422 | type_not_detected | Detection found no candidate; pass type. |
| 429 | rate_limit_exceeded | Per-minute ceiling hit. See Retry-After. |
| 429 | monthly_quota_exceeded | Plan quota exhausted for the month. |
| 500 | internal_error | Quote requestId when reporting. |
Rate limits #
Every metered response carries the current window state:
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 297
X-RateLimit-Reset: 1785312000
Retry-After: 41 # only on 429
| Plan | Per minute | Per month | Batch max |
|---|---|---|---|
| Anonymous | 20 | — | 10 |
| Sandbox | 30 | 1,000 | 25 |
| Growth | 300 | 100,000 | 250 |
| Scale | 2,000 | 5,000,000 | 1,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
| Claim | Aptlab | Needs |
|---|---|---|
| Value is well-formed | yes | — |
| Check digit is correct | yes | — |
| Domain can receive mail | yes | checkMx: true |
| Identifier was actually issued | no | Issuer registry lookup |
| Belongs to a named person | no | Consent-bound eKYC |
| Bank account is live and owned | no | Penny-drop |
| Phone is reachable right now | no | HLR lookup or OTP |