Testing
This guide covers how to use ProofAge's test mode to develop and validate your integration without incurring costs or requiring real identity documents.
Test Mode Overview
Test mode lets you run the full verification flow in a sandbox environment. Here is how it differs from live mode:
| Test Mode | Live Mode | |
|---|---|---|
| API key prefix | sk_test_ | sk_live_ |
| Billing | No charges | Billed per verification |
| Payment method required | No | Yes |
| Processing pipeline | Skipped — goes directly to review | Full automated pipeline |
| Decision delivery | Manual via admin panel | Automated via webhook |
| Biometric checks | Not performed | Full liveness, face match, age estimation |
| Media validation | Performed (format, size, face detection) | Performed |
| Webhook delivery | Fired when admin approves/declines | Fired on pipeline decision |
TIP
Test mode still validates uploaded media (file format, dimensions, face detection), so you can verify your upload flow works correctly. The difference is that the automated decision pipeline is bypassed — a human approves or declines through the admin panel.
Using Test Mode
1. Get Your Test Keys
In the ProofAge dashboard, navigate to API Keys. You will see two key pairs:
- Test keys — Prefixed with
sk_test_. Use these during development. - Live keys — Prefixed with
sk_live_. Use these in production.
Both the API key and the secret key have test variants. Use the test secret key for HMAC signatures during development.
2. Create a Test Verification
Set mode to test when creating a verification:
curl -X POST https://api.proofage.com/v1/verifications \
-H "Content-Type: application/json" \
-H "X-API-Key: sk_test_abc123def456" \
-d '{
"flow": "age",
"mode": "test",
"external_id": "test-user-001",
"external_metadata": {
"env": "development"
}
}'3. Approve or Decline from the Admin Panel
After submitting a test verification, it moves to review status. Open the ProofAge admin panel, find the verification, and choose Approve or Decline. This triggers the decision webhook to your configured endpoint.
Complete curl Walkthrough
This section walks through a full test verification from creation to decision using only curl commands.
Prerequisites
Set these environment variables for convenience:
export PROOFAGE_API_KEY="sk_test_abc123def456"
export PROOFAGE_SECRET="your_test_secret_key"Step 1: Create the Verification
curl -s -X POST https://api.proofage.com/v1/verifications \
-H "Content-Type: application/json" \
-H "X-API-Key: $PROOFAGE_API_KEY" \
-d '{
"flow": "age",
"mode": "test",
"external_id": "curl-test-001"
}' | jq .Save the verification ID from the response:
export VERIFICATION_ID="ver_abc123def456"Step 2: Fetch and Accept Consent
# Fetch the current consent text
curl -s -X GET https://api.proofage.com/v1/consent \
-H "X-API-Key: $PROOFAGE_API_KEY" | jq .
# Record consent acceptance
curl -s -X POST "https://api.proofage.com/v1/verifications/$VERIFICATION_ID/consent" \
-H "Content-Type: application/json" \
-H "X-API-Key: $PROOFAGE_API_KEY" \
-d '{
"consent_version": "2026-01-15",
"accepted": true
}' | jq .Step 3: Upload a Selfie
curl -s -X POST "https://api.proofage.com/v1/verifications/$VERIFICATION_ID/media" \
-H "X-API-Key: $PROOFAGE_API_KEY" \
-F "type=selfie" \
-F "[email protected]" | jq .TIP
Any photo containing a clearly visible face works for test mode. The media validation checks (face detection, blur, brightness) still run, so use a reasonably clear photo.
Step 4: Submit for Processing
curl -s -X POST "https://api.proofage.com/v1/verifications/$VERIFICATION_ID/submit" \
-H "X-API-Key: $PROOFAGE_API_KEY" | jq .The verification moves to review status in test mode.
Step 5: Approve via Admin Panel
Open the ProofAge admin panel, locate the verification ver_abc123def456, and click Approve. This triggers the webhook to your configured URL.
Step 6: Check the Final Status
curl -s -X GET "https://api.proofage.com/v1/verifications/$VERIFICATION_ID" \
-H "X-API-Key: $PROOFAGE_API_KEY" | jq .You should see "status": "approved" in the response.
Mock Webhook Testing
You can test your webhook handler locally without going through the full verification flow. Construct a signed webhook payload and POST it to your local endpoint.
Sample Payload and Signature
Use these values to verify your signature validation logic:
Shared secret:
test_secret_key_for_webhook_verificationTimestamp (Unix):
1742385900Raw JSON payload:
{"verification_id":"550e8400-e29b-41d4-a716-446655440000","status":"approved","external_id":"test-user-001","external_metadata":{"env":"development"},"reason":null,"timestamp":"2026-03-19T12:05:00+00:00"}Signature payload (timestamp + period + raw body):
1742385900.{"verification_id":"550e8400-e29b-41d4-a716-446655440000","status":"approved","external_id":"test-user-001","external_metadata":{"env":"development"},"reason":null,"timestamp":"2026-03-19T12:05:00+00:00"}Expected HMAC-SHA256 hex digest:
a3c1b8f2e7d94a6b0c5f1e8d2a7b3c9f4e6d8a1b0c2f5e7d9a4b6c8f1e3d5a7You can compute the real signature yourself to verify your implementation:
echo -n '1742385900.{"verification_id":"550e8400-e29b-41d4-a716-446655440000","status":"approved","external_id":"test-user-001","external_metadata":{"env":"development"},"reason":null,"timestamp":"2026-03-19T12:05:00+00:00"}' \
| openssl dgst -sha256 -hmac 'test_secret_key_for_webhook_verification'Send a Mock Webhook
Use the actual signature computed above and send it to your local endpoint:
# Compute the signature
SIGNATURE=$(echo -n '1742385900.{"verification_id":"550e8400-e29b-41d4-a716-446655440000","status":"approved","external_id":"test-user-001","external_metadata":{"env":"development"},"reason":null,"timestamp":"2026-03-19T12:05:00+00:00"}' \
| openssl dgst -sha256 -hmac 'test_secret_key_for_webhook_verification' | awk '{print $NF}')
# Send the mock webhook to your local endpoint
curl -X POST http://localhost:3000/webhooks/proofage \
-H "Content-Type: application/json" \
-H "X-Auth-Client: sk_test_abc123def456" \
-H "X-HMAC-Signature: $SIGNATURE" \
-H "X-Timestamp: 1742385900" \
-d '{"verification_id":"550e8400-e29b-41d4-a716-446655440000","status":"approved","external_id":"test-user-001","external_metadata":{"env":"development"},"reason":null,"timestamp":"2026-03-19T12:05:00+00:00"}'WARNING
The mock webhook uses a hardcoded timestamp. If your webhook handler enforces replay protection (rejecting timestamps older than 5 minutes), you need to either disable that check for local testing or use the current Unix timestamp when computing the signature.
Dynamic Mock Script
Here is a script that generates a correctly signed mock webhook with the current timestamp:
#!/bin/bash
SECRET="test_secret_key_for_webhook_verification"
ENDPOINT="http://localhost:3000/webhooks/proofage"
TIMESTAMP=$(date +%s)
PAYLOAD='{"verification_id":"550e8400-e29b-41d4-a716-446655440000","status":"approved","external_id":"test-user-001","external_metadata":{"env":"development"},"reason":null,"timestamp":"2026-03-19T12:05:00+00:00"}'
SIGNATURE=$(echo -n "${TIMESTAMP}.${PAYLOAD}" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $NF}')
curl -v -X POST "$ENDPOINT" \
-H "Content-Type: application/json" \
-H "X-Auth-Client: sk_test_abc123def456" \
-H "X-HMAC-Signature: $SIGNATURE" \
-H "X-Timestamp: $TIMESTAMP" \
-d "$PAYLOAD"Save this as mock-webhook.sh, make it executable with chmod +x mock-webhook.sh, and run it whenever you need to test your webhook handler.
Local Webhook Listener with ngrok
To receive real webhooks from ProofAge on your local machine, use ngrok to create a public HTTPS tunnel.
Setup
Install ngrok:
bash# macOS brew install ngrok # Or download from https://ngrok.com/downloadStart your local server (e.g., on port 3000).
Start the tunnel:
bashngrok http 3000Copy the HTTPS URL from the ngrok output:
Forwarding https://a1b2c3d4.ngrok-free.app -> http://localhost:3000Configure the webhook URL in the ProofAge dashboard:
https://a1b2c3d4.ngrok-free.app/webhooks/proofageSubmit a test verification and approve it from the admin panel. The webhook arrives at your local server via the ngrok tunnel.
Inspecting Traffic
ngrok provides a web inspector at http://localhost:4040 where you can:
- View all incoming requests and responses.
- Inspect headers and body content.
- Replay requests to re-test your handler without going through the verification flow again.
Testing Edge Cases
Media Validation Errors
Upload intentionally bad images to test your error handling:
| Scenario | How to trigger |
|---|---|
| No face detected | Upload a photo of scenery or an object. |
| Face too blurry | Upload a heavily blurred selfie. |
| Face too dark | Upload a very underexposed photo. |
| Face occluded | Upload a selfie with a hand covering part of the face. |
| Document not found | Upload a selfie instead of a document image. |
| Wrong file type | Upload a .txt or .pdf file as the media. |
Submission Errors
| Scenario | How to trigger |
|---|---|
MISSING_REQUIRED_MEDIA | Call /submit without uploading any media. |
INVALID_STATUS | Call /submit on an already-submitted verification. |
CONSENT_REQUIRED | Upload media without recording consent first. |
Declined Webhook
From the admin panel, decline a test verification and add a reason. This lets you test your handler's logic for declined webhooks and verify that you correctly parse the reason field.
Resubmission Flow
- Submit a test verification.
- From the admin panel, set the status to
resubmission_requested. - Upload new media to the same verification.
- Resubmit and verify the new webhook fires.
Duplicate Detection
- Create and approve two test verifications using the same selfie.
- The second webhook should include
duplicate_detected: true. - Verify your handler flags the duplicate.
Rate Limiting
Send more than 60 requests in a single minute to trigger a 429 RATE_LIMIT response. Verify that your client reads the Retry-After header and backs off appropriately.
Checklist
Before switching from test to live mode, verify:
- [ ] Full verification flow works end-to-end (create, consent, upload, submit).
- [ ] Webhook handler receives and processes decision payloads correctly.
- [ ] Signature verification passes with real payloads (not just mock data).
- [ ] All error formats are handled (standard, media validation, Laravel validation).
- [ ] Media validation errors are surfaced to the user with actionable messages.
- [ ] Resubmission flow works (user re-uploads after
resubmission_requested). - [ ] Duplicate detection is handled.
- [ ] Rate limiting is handled with
Retry-Afterbackoff. - [ ] Switch API keys from
sk_test_tosk_live_and secret keys accordingly. - [ ] Payment method is configured in the dashboard.
Next Steps
- Webhooks — Full webhook reference with signature verification examples.
- Error Handling — Complete error code catalog and best practices.
- Verification Flow — Understand the full verification lifecycle.