Why This Project
A follow-on to the Partner Ops project, built to answer a more specific question: how
would I actually verify a third-party KYC check — the exact kind of NSDL/Aadhaar
verification a lending onboarding flow depends on — beyond "I sent a request and got
a response back." Self-directed practice, not a real NSDL integration or client work.
Step by Step — From Collection to Result
Step 1 — Build the request
A new Postman collection, one request: POST {{baseUrl}}/v1/kyc/verify, with an Authorization: Bearer header and a JSON body.
{
"customerId": "TEST001",
"pan": "ABCDE1234F",
"dob": "1995-06-15"
}
Step 2 — Define the 9 examples first (the expected contract)
Before testing anything, 9 examples got built — one per scenario, each a
request/response pair stating what should happen. This is the answer key: without it,
there's nothing to check a real response against.
1. Valid
Token: VALID_TEST_TOKEN
Body: { "customerId": "TEST001", "pan": "ABCDE1234F", "dob": "1995-06-15" }
Expected — 200 OK
{ "status": "SUCCESS", "customerId": "TEST001", "kycStatus": "VERIFIED", "verificationDate": "2026-09-10", "requestId": "REQ-10001" }
2. Missing PAN
Token: VALID_TEST_TOKEN
Body: { "customerId": "TEST001", "dob": "1995-06-15" }
Expected — 400 Bad Request
{ "status": "FAILED", "errorCode": "MISSING_FIELD", "message": "PAN is required" }
3. Invalid PAN format
Token: VALID_TEST_TOKEN
Body: { "customerId": "TEST001", "pan": "12345", "dob": "1995-06-15" }
Expected — 400 Bad Request
{ "status": "FAILED", "errorCode": "INVALID_FORMAT", "message": "PAN format is invalid (expected AAAAA9999A)" }
4. Invalid token
Token: WRONG_TOKEN
Body: { "customerId": "TEST001", "pan": "ABCDE1234F", "dob": "1995-06-15" }
Expected — 401 Unauthorized
{ "status": "FAILED", "errorCode": "INVALID_TOKEN", "message": "Invalid or expired token" }
5. Missing auth
Token: (none — Auth Type set to "No Auth")
Body: { "customerId": "TEST001", "pan": "ABCDE1234F", "dob": "1995-06-15" }
Expected — 401 Unauthorized
{ "status": "FAILED", "errorCode": "MISSING_AUTH", "message": "Authorization header is required" }
6. Customer not found
Token: VALID_TEST_TOKEN
Body: { "customerId": "NOTFOUND001", "pan": "ABCDE1234F", "dob": "1995-06-15" }
Expected — 404 Not Found
{ "status": "FAILED", "errorCode": "CUSTOMER_NOT_FOUND", "message": "No customer found with this ID" }
7. Invalid DOB format
Token: VALID_TEST_TOKEN
Body: { "customerId": "TEST001", "pan": "ABCDE1234F", "dob": "15-06-1995" }
Expected — 400 Bad Request
{ "status": "FAILED", "errorCode": "INVALID_DATE_FORMAT", "message": "dob must be in YYYY-MM-DD format" }
8. Duplicate request
Token: VALID_TEST_TOKEN
Body: { "customerId": "DUPLICATE001", "pan": "ABCDE1234F", "dob": "1995-06-15" }
Expected — 409 Conflict
{ "status": "FAILED", "errorCode": "DUPLICATE_REQUEST", "message": "A KYC request for this customer is already in progress" }
9. Server error
Token: VALID_TEST_TOKEN
Body: { "customerId": "ERROR001", "pan": "ABCDE1234F", "dob": "1995-06-15" }
Expected — 500 Internal Server Error
{ "status": "FAILED", "errorCode": "INTERNAL_ERROR", "message": "Something went wrong on our end" }
One real mix-up along the way, worth keeping: an example has two separate halves —
request (what gets sent) and response (what comes back) — and the first attempt typed
the expected response into the request half by mistake, leaving the actual response
empty. Easy error, and it's exactly why having the expected answer written down
first matters: it made the mistake obvious immediately, instead of only
surfacing later when a real test didn't behave as expected.
Postman turned out to offer two separate mock types — this example-matching one, and a
newer code-based "Mock (BETA)" that runs real logic. The code-based one is what Step 5's
actual test run used, but these 9 examples are what "correct" was defined against.
Step 3 — Stand up a mock server with real logic, not a canned answer
Rather than a static example response, the mock server runs actual validation code:
checks the auth header, checks each required field is present, validates PAN against
the real NSDL format pattern (AAAAA9999A) and the date against
YYYY-MM-DD, then branches into business-rule outcomes (not found,
duplicate in progress, server error, a deliberately slow response for a timeout test)
keyed off specific test customer IDs. It genuinely evaluates whatever gets sent to
it, the same as a real backend would.
Step 4 — Two more real mistakes in the code-based mock, caught by checking the actual response, not just the status
First: a JSON body got pasted into the URL field by accident — Postman returned
"Invalid character in header content", which is a real, specific error a
mixed-up field produces. Second, and more instructive: a request came back
401 Unauthorized when it should have been 200 — not because the mock
was broken, but because the Authorization tab's Token field was empty (a token typed
into the wrong field earlier). The fix in both cases was reading what the response
actually said instead of assuming the request was fine.
POST {{baseUrl}}/v1/kyc/verify
Authorization: Bearer VALID_TEST_TOKEN
Body: { "customerId": "TEST001", "pan": "ABCDE1234F", "dob": "1995-06-15" }
Output — 200 OK
{ "status": "SUCCESS", "customerId": "TEST001", "kycStatus": "VERIFIED", "verificationDate": "2026-09-10", "requestId": "REQ-10001" }
Step 5 — Run it again with different data, to prove it's not hardcoded
If the mock just matched Step 2's exact inputs, that would prove nothing — it could be
a lookup table. So this run deliberately sends different values —
different customer ID, a different (but still valid) PAN, a different DOB, a different
wrong token — same 9 scenario types, none of the literal data reused. The 3
business-rule cases (not found / duplicate / server error) keep the same trigger ID,
because those are literal constants the mock's code checks for by design — the same
convention real sandbox environments use (e.g. a payment gateway's test card numbers
that deliberately trigger a decline). Everything else genuinely varies.
Where this gets typed: the same KYC Verify request's
Authorization and Body tabs in Postman — not a new request — sent against the
code-based mock server running locally at http://localhost:4501.
1. Valid request
Token: VALID_TEST_TOKEN
Body: { "customerId": "TEST002", "pan": "XYZAB5678C", "dob": "1998-11-23" }
Actual — 200 OK ✅ (matches expected)
{ "status": "SUCCESS", "customerId": "TEST002", "kycStatus": "VERIFIED", "verificationDate": "2026-09-10", "requestId": "REQ-10001" }
2. Missing PAN
Token: VALID_TEST_TOKEN
Body: { "customerId": "TEST002", "dob": "1998-11-23" }
Actual — 400 Bad Request ✅ (matches expected)
{ "status": "FAILED", "errorCode": "MISSING_FIELD", "message": "PAN is required" }
3. Invalid PAN format
Token: VALID_TEST_TOKEN
Body: { "customerId": "TEST002", "pan": "999999", "dob": "1998-11-23" }
Actual — 400 Bad Request ✅ (matches expected)
{ "status": "FAILED", "errorCode": "INVALID_FORMAT", "message": "PAN format is invalid (expected AAAAA9999A)" }
4. Invalid token
Token: FAKE_TOKEN_999
Body: { "customerId": "TEST002", "pan": "XYZAB5678C", "dob": "1998-11-23" }
Actual — 401 Unauthorized ✅ (matches expected)
{ "status": "FAILED", "errorCode": "INVALID_TOKEN", "message": "Invalid or expired token" }
5. Missing auth
Token: (none — Auth Type set to "No Auth")
Body: { "customerId": "TEST002", "pan": "XYZAB5678C", "dob": "1998-11-23" }
Actual — 401 Unauthorized ✅ (matches expected)
{ "status": "FAILED", "errorCode": "MISSING_AUTH", "message": "Authorization header is required" }
6. Customer not found
Token: VALID_TEST_TOKEN
Body: { "customerId": "NOTFOUND001", "pan": "XYZAB5678C", "dob": "1998-11-23" }
Actual — 404 Not Found ✅ (matches expected)
{ "status": "FAILED", "errorCode": "CUSTOMER_NOT_FOUND", "message": "No customer found with this ID" }
7. Invalid DOB format
Token: VALID_TEST_TOKEN
Body: { "customerId": "TEST002", "pan": "XYZAB5678C", "dob": "23-11-1998" }
Actual — 400 Bad Request ✅ (matches expected)
{ "status": "FAILED", "errorCode": "INVALID_DATE_FORMAT", "message": "dob must be in YYYY-MM-DD format" }
8. Duplicate request
Token: VALID_TEST_TOKEN
Body: { "customerId": "DUPLICATE001", "pan": "XYZAB5678C", "dob": "1998-11-23" }
Actual — 409 Conflict ✅ (matches expected)
{ "status": "FAILED", "errorCode": "DUPLICATE_REQUEST", "message": "A KYC request for this customer is already in progress" }
9. Server error
Token: VALID_TEST_TOKEN
Body: { "customerId": "ERROR001", "pan": "XYZAB5678C", "dob": "1998-11-23" }
Actual — 500 Internal Server Error ✅ (matches expected)
{ "status": "FAILED", "errorCode": "INTERNAL_ERROR", "message": "Something went wrong on our end" }
Result — 9 for 9, with entirely different data than Step 2 used. Same category of input still lands on the same correct branch every time — real validation logic, not a lookup table keyed to one specific example.
A Self-Caught Mistake Worth Keeping In
Alongside this, the same session included a plain HTTP-methods drill against a public
test API (GET/POST/PUT/PATCH/DELETE, no mock involved) — and one request meant to be a
PATCH was accidentally sent as a leftover GET. It still came back 200
OK. The status code alone said nothing was wrong; only checking the response
body — the "update" had silently not happened — caught it. That's the actual habit
this whole project was practicing: a green status code is not proof the intended
thing occurred, the response content is.