API Reference
Midnight Auth REST API v1
Authentication
All API requests require an API key sent via the X-API-Key header. API keys are generated in the Console under Apps.
// Every request must include: X-API-Key: ma_your_api_key_here Content-Type: application/json
| Param | Type | Required | Description |
|---|---|---|---|
| X-API-Key | string | Yes | Partner app API key (starts with "ma_") |
| Content-Type | string | Yes | Must be "application/json" for POST/PUT/PATCH |
Security Notes
- •API keys are scoped per partner app — never share across apps
- •Keys can be rotated in the Console without downtime
- •All requests must use HTTPS in production
- •Rate limit: 100 requests/minute per API key
- •No PII is stored or returned — only derived attributes and booleans
Health Check
Check if the API is reachable and healthy. Does not require authentication.
Response 200
{
"status": "ok",
"service": "midnight-auth-api",
"timestamp": "2026-02-26T10:00:00.000Z",
"uptime": 86400
}Login Endpoints
The login flow lets partner apps authenticate users via their Veridian wallet. Your app initiates a session, displays a QR code, and polls until the wallet approves.
Start a new login session. Returns a QR code and deep link for the user's wallet.
Request Body
{
"apiKey": "ma_your_api_key_here",
"requirements": ["email", "phone"],
"redirectUrl": "https://myapp.com/dashboard" // optional
}| Param | Type | Required | Description |
|---|---|---|---|
| apiKey | string | Yes | Partner app API key |
| requirements | string[] | Yes | Credential types: "email", "phone", "social", "humanity", "age" |
| redirectUrl | string | No | URL to redirect user after login |
Response 201
{
"sessionId": "ses_a1b2c3d4...",
"challengeNonce": "n_x7y8z9...",
"deepLink": "midnight-auth://login?session=ses_a1b2c3d4...",
"qrCodeUrl": "data:image/png;base64,iVBOR...",
"expiresAt": "2026-02-26T10:05:00.000Z",
"appName": "My App",
"requirements": ["email", "phone"]
}Check the current status of a login session. Use for polling.
Response 200
{
"sessionId": "ses_a1b2c3d4...",
"status": "pending", // "pending" | "presented" | "verified" | "expired" | "denied"
"requirements": ["email", "phone"],
"expiresAt": "2026-02-26T10:05:00.000Z"
}| Param | Type | Required | Description |
|---|---|---|---|
| pending | status | — | Waiting for user to scan QR code |
| presented | status | — | User scanned, presenting credentials |
| verified | status | — | All credentials verified — call /login/result |
| expired | status | — | Session expired (5 min TTL) |
| denied | status | — | User denied the login request |
Fetch the verified login result. Only returns data when status is "verified".
Response 200
{
"verified": true,
"userId": "usr_7a8b3c...",
"verifiedAt": "2026-02-26T10:01:30.000Z",
"emailVerified": true,
"emailDomain": "gmail.com",
"domainType": "consumer",
"phoneVerified": true,
"phoneCountry": "US",
"carrierType": "mobile",
"hasSocial": true,
"socialPlatform": "twitter",
"accountAgeMonths": 36,
"followerRange": "1k-10k",
"isHuman": true,
"humanityScore": 0.98,
"over18": true,
"over21": true,
"over25": false
}Onboarding Endpoints
The onboarding flow issues verifiable ACDC credentials to users' Veridian wallets. Each verification step issues a separate credential.
Connect a Veridian wallet to start the onboarding session.
Request Body
{
"userAid": "D_keri_aid_44_chars..."
}Response 201
{
"sessionId": "ses_x1y2z3...",
"connectedAt": "2026-02-26T10:00:00.000Z",
"expiresAt": "2026-02-26T10:30:00.000Z"
}Send a 6-digit verification code to the user's email. The email is hashed and never stored.
Request Body
{
"sessionId": "ses_x1y2z3...",
"email": "user@example.com"
}Response 200
{
"message": "Verification code sent",
"domain": "consumer",
"expiresInMinutes": 10
}Verify the email code. On success, issues an EmailVerificationCredential ACDC.
Request Body
{
"sessionId": "ses_x1y2z3...",
"code": "123456"
}Response 200
{
"credentialType": "EmailVerificationCredential",
"schemaSaid": "EBfd...",
"issuedAt": "2026-02-26T10:02:00.000Z",
"commitmentHash": "0x7a8b...",
"trustLevel": 1,
"message": "Email verified, credential issued"
}Send a verification code to the user's phone via WhatsApp/SMS. Number is never stored.
Request Body
{
"sessionId": "ses_x1y2z3...",
"phoneNumber": "+1234567890"
}Response 200
{
"message": "Verification code sent",
"expiresInMinutes": 10
}Verify the phone code. On success, issues a PhoneVerificationCredential ACDC.
Request Body
{
"sessionId": "ses_x1y2z3...",
"code": "654321"
}Response 200
{
"credentialType": "PhoneVerificationCredential",
"schemaSaid": "EBfe...",
"issuedAt": "2026-02-26T10:03:00.000Z",
"commitmentHash": "0x9c0d...",
"trustLevel": 2,
"message": "Phone verified, credential issued"
}Start OAuth flow for a social provider. Provider must be "twitter", "discord", or "github".
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
| sessionId | string | Yes | Active onboarding session ID |
Response 200
{
"authorizationUrl": "https://twitter.com/i/oauth2/authorize?...",
"state": "st_abc123..."
}Handle OAuth callback. On success, issues a SocialAccountCredential ACDC.
Query Parameters
| Param | Type | Required | Description |
|---|---|---|---|
| provider | string | Yes | OAuth provider name |
| code | string | Yes | Authorization code from OAuth redirect |
| state | string | Yes | State parameter from the initial redirect |
Response 200
{
"credentialType": "SocialAccountCredential",
"schemaSaid": "EBff...",
"issuedAt": "2026-02-26T10:04:00.000Z",
"commitmentHash": "0xab12...",
"trustLevel": 3,
"message": "Social account verified, credential issued"
}Verify humanity with a captcha token. On success, issues a HumanityCredential ACDC.
Request Body
{
"sessionId": "ses_x1y2z3...",
"captchaToken": "hcaptcha_response_token_here"
}Response 200
{
"credentialType": "HumanityCredential",
"schemaSaid": "EBfg...",
"issuedAt": "2026-02-26T10:05:00.000Z",
"commitmentHash": "0xcd34...",
"trustLevel": 4,
"message": "Humanity verified, credential issued"
}Get the current status of an onboarding session.
Response 200
{
"sessionId": "ses_x1y2z3...",
"userAid": "D_keri_aid_44_chars...",
"completedSteps": ["email", "phone"],
"trustLevel": 2,
"expiresAt": "2026-02-26T10:30:00.000Z"
}Error Responses
All errors return a consistent JSON structure. Error messages never contain PII.
{
"statusCode": 403,
"message": "Invalid or inactive API key",
"error": "Forbidden"
}| Param | Type | Required | Description |
|---|---|---|---|
| 400 | Bad Request | — | Missing or invalid request parameters |
| 401 | Unauthorized | — | Missing API key header |
| 403 | Forbidden | — | Invalid or inactive API key |
| 404 | Not Found | — | Session or resource not found |
| 409 | Conflict | — | Duplicate operation (e.g. code already sent) |
| 429 | Too Many Requests | — | Rate limit exceeded (100 req/min) |
| 500 | Internal Server Error | — | Server error — retry with backoff |
CORS & Production Requirements
- •HTTPS is required for all production API calls
- •CORS is configured per partner app in the Console
- •API keys must be kept server-side — never expose in client code
- •All PII (email, phone) is hashed on receipt and never stored in plain text
- •Session tokens expire after 5 minutes (login) or 30 minutes (onboarding)
- •Rate limiting: 100 requests/minute per API key, 429 returned when exceeded