Poll Challenge Status
Poll the status of an authentication challenge. Call this endpoint repeatedly until the status is no longer pending.
Endpoint
POST /api/v1/auth/status
Request Body
{
"challenge_id": "550e8400-e29b-41d4-a716-446655440000"
}
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
challenge_id | string | Yes | UUID from challenge creation |
Response
Pending
{
"status": "pending"
}
Approved
{
"status": "approved"
}
Denied
{
"status": "denied"
}
Expired
{
"status": "expired"
}
Response Fields
| Field | Type | Description |
|---|---|---|
status | string | One of: pending, approved, denied, expired |
Status Definitions
| Status | Description |
|---|---|
pending | Waiting for user response on mobile device |
approved | User approved via biometric authentication |
denied | User explicitly denied the request |
expired | Challenge TTL (120s) exceeded without response |
Errors
| Error Code | HTTP Status | Description |
|---|---|---|
INVALID_REQUEST | 400 | Missing challenge id |
INTERNAL_ERROR | 400 | Internal error |
FORBIDDEN | 403 | Challenge belongs to different client |
NOT_FOUND | 404 | Challenge not found or already expired |
Examples
cURL
#!/bin/bash
CLIENT_ID="your-client-id"
CLIENT_SECRET="your-client-secret"
API_BASE="https://ztxbas01.corezt.com"
CHALLENGE_ID="550e8400-e29b-41d4-a716-446655440000"
TIMESTAMP=$(date +%s)
NONCE=$(openssl rand -hex 32)
BODY="{\"challenge_id\":\"${CHALLENGE_ID}\"}"
SIGN_DATA="POST|/api/v1/auth/status|${TIMESTAMP}|${NONCE}|${BODY}"
SIGNATURE=$(echo -n "$SIGN_DATA" | openssl dgst -sha256 -hmac "$CLIENT_SECRET" | awk '{print $2}')
curl -X POST "${API_BASE}/v1/api/auth/status" \
-H "Content-Type: application/json" \
-H "X-Client-ID: ${CLIENT_ID}" \
-H "X-Timestamp: ${TIMESTAMP}" \
-H "X-Nonce: ${NONCE}" \
-H "X-Signature: ${SIGNATURE}" \
-d "$BODY"
Python
import hmac
import hashlib
import time
import secrets
import requests
import json
class ZTXClient:
def __init__(self, client_id: str, client_secret: str, base_url: str = "https://ztxbas01.corezt.com"):
self.client_id = client_id
self.client_secret = client_secret
self.base_url = base_url
def _sign_request(self, method: str, endpoint: str, body: str) -> dict:
timestamp = str(int(time.time()))
nonce = secrets.token_hex(32)
sign_data = f"{method}|{endpoint}|{timestamp}|{nonce}|{body}"
signature = hmac.new(
self.client_secret.encode(),
sign_data.encode(),
hashlib.sha256
).hexdigest()
return {
"X-Client-ID": self.client_id,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": signature,
"Content-Type": "application/json"
}
def poll_status(self, challenge_id: str) -> dict:
"""
Poll challenge status.
Returns:
dict with 'status'
"""
endpoint = "/api/v1/auth/status"
body = json.dumps({"challenge_id": challenge_id})
headers = self._sign_request("POST", endpoint, body)
response = requests.post(f"{self.base_url}{endpoint}", headers=headers, data=body)
response.raise_for_status()
return response.json()
def wait_for_approval(self, challenge_id: str, timeout: int = 120, poll_interval: float = 2.0) -> dict:
"""
Poll until challenge is resolved or timeout.
Args:
challenge_id: Challenge to poll
timeout: Max seconds to wait
poll_interval: Seconds between polls
Returns:
Final status dict
Raises:
TimeoutError: If timeout exceeded while pending
"""
start_time = time.time()
while time.time() - start_time < timeout:
result = self.poll_status(challenge_id)
if result["status"] != "pending":
return result
time.sleep(poll_interval)
raise TimeoutError("Challenge polling timed out")
# Usage
client = ZTXClient("your-client-id", "your-client-secret")
# Create challenge first
challenge = client.create_challenge("user@example.com", "https://app.example.com")
print(f"Waiting for user approval...")
print(f"Challenge ID: {challenge['challenge_id']}")
try:
result = client.wait_for_approval(challenge["challenge_id"])
if result["status"] == "approved":
print(f"✓ Approved!")
elif result["status"] == "denied":
print("✗ User denied the request")
elif result["status"] == "expired":
print("✗ Challenge expired")
except TimeoutError:
print("✗ Timed out waiting for user")
Node.js
const crypto = require('crypto');
const https = require('https');
class ZTXClient {
constructor(clientId, clientSecret, baseUrl = 'https://ztxbas01.corezt.com') {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.baseUrl = new URL(baseUrl);
}
_signRequest(method, endpoint, body) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(32).toString('hex');
const signData = `${method}|${endpoint}|${timestamp}|${nonce}|${body}`;
const signature = crypto
.createHmac('sha256', this.clientSecret)
.update(signData)
.digest('hex');
return {
'X-Client-ID': this.clientId,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
'Content-Type': 'application/json'
};
}
async pollStatus(challengeId) {
const endpoint = '/api/v1/auth/status';
const body = JSON.stringify({ challenge_id: challengeId });
const headers = this._signRequest('POST', endpoint, body);
return new Promise((resolve, reject) => {
const options = {
hostname: this.baseUrl.hostname,
port: this.baseUrl.port || 443,
path: endpoint,
method: 'POST',
headers
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
if (res.statusCode !== 200) {
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
return;
}
resolve(JSON.parse(data));
});
});
req.on('error', reject);
req.write(body);
req.end();
});
}
async waitForApproval(challengeId, timeout = 120000, pollInterval = 2000) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const result = await this.pollStatus(challengeId);
if (result.status !== 'pending') {
return result;
}
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
throw new Error('Challenge polling timed out');
}
}
// Usage
const client = new ZTXClient('your-client-id', 'your-client-secret');
async function authenticate(userEmail, origin) {
// Create challenge
const challenge = await client.createChallenge(userEmail, origin);
console.log(`Waiting for user approval...`);
console.log(`Challenge ID: ${challenge.challenge_id}`);
try {
const result = await client.waitForApproval(challenge.challenge_id);
switch (result.status) {
case 'approved':
console.log(`✓ Approved!`);
return null;
case 'denied':
console.log('✗ User denied the request');
return null;
case 'expired':
console.log('✗ Challenge expired');
return null;
}
} catch (error) {
console.log('✗ Timed out waiting for user');
return null;
}
}
authenticate('user@example.com', 'https://app.example.com');
C# (.NET)
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
public class ZTXClient
{
private readonly string _clientId;
private readonly string _clientSecret;
private readonly HttpClient _httpClient;
public ZTXClient(string clientId, string clientSecret, string baseUrl = "https://ztxbas01.corezt.com")
{
_clientId = clientId;
_clientSecret = clientSecret;
_httpClient = new HttpClient { BaseAddress = new Uri(baseUrl) };
}
private Dictionary<string, string> SignRequest(string method, string endpoint, string body)
{
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var nonce = Convert.ToHexString(RandomNumberGenerator.GetBytes(32)).ToLower();
var signData = $"{method}|{endpoint}|{timestamp}|{nonce}|{body}";
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_clientSecret));
var signature = Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(signData))).ToLower();
return new Dictionary<string, string>
{
["X-Client-ID"] = _clientId,
["X-Timestamp"] = timestamp,
["X-Nonce"] = nonce,
["X-Signature"] = signature
};
}
public async Task<StatusResponse> PollStatusAsync(string challengeId)
{
var endpoint = "/api/v1/auth/status";
var body = JsonSerializer.Serialize(new { challenge_id = challengeId });
var headers = SignRequest("POST", endpoint, body);
var request = new HttpRequestMessage(HttpMethod.Post, endpoint)
{
Content = new StringContent(body, Encoding.UTF8, "application/json")
};
foreach (var header in headers)
{
request.Headers.Add(header.Key, header.Value);
}
var response = await _httpClient.SendAsync(request);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<StatusResponse>(json, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
});
}
public async Task<StatusResponse> WaitForApprovalAsync(
string challengeId,
TimeSpan? timeout = null,
TimeSpan? pollInterval = null,
CancellationToken cancellationToken = default)
{
timeout ??= TimeSpan.FromSeconds(120);
pollInterval ??= TimeSpan.FromSeconds(2);
var startTime = DateTime.UtcNow;
while (DateTime.UtcNow - startTime < timeout)
{
cancellationToken.ThrowIfCancellationRequested();
var result = await PollStatusAsync(challengeId);
if (result.Status != "pending")
{
return result;
}
await Task.Delay(pollInterval.Value, cancellationToken);
}
throw new TimeoutException("Challenge polling timed out");
}
}
public class StatusResponse
{
public string Status { get; set; }
}
// Usage
var client = new ZTXClient("your-client-id", "your-client-secret");
// Create challenge first
var challenge = await client.CreateChallengeAsync("user@example.com", "https://app.example.com");
Console.WriteLine($"Waiting for user approval...");
Console.WriteLine($"Challenge ID: {challenge.ChallengeId}");
try
{
var result = await client.WaitForApprovalAsync(challenge.ChallengeId);
switch (result.Status)
{
case "approved":
Console.WriteLine($"✓ Approved!");
break;
case "denied":
Console.WriteLine("✗ User denied the request");
break;
case "expired":
Console.WriteLine("✗ Challenge expired");
break;
}
}
catch (TimeoutException)
{
Console.WriteLine("✗ Timed out waiting for user");
}
Polling Best Practices
Recommended Polling Strategy
Initial delay: 1 second
Poll interval: 2 seconds
Max duration: 120 seconds (matches challenge TTL)
Exponential Backoff (Optional)
For high-traffic applications, consider exponential backoff:
intervals = [1, 2, 2, 3, 3, 5, 5, 5, 10, 10, 10, 10, ...]
WebSocket Alternative
For real-time updates without polling, consider implementing WebSocket connections. Contact CoreZT support for WebSocket API access.
Frontend Integration
JavaScript Polling Example
async function authenticateUser(email, origin) {
const challenge = await ztxClient.createChallenge(email, origin);
// Show waiting UI
showWaitingModal(challenge.origin.display_name);
const pollInterval = 2000;
const maxAttempts = 60; // 2 minutes
let attempts = 0;
return new Promise((resolve, reject) => {
const poll = async () => {
attempts++;
try {
const result = await ztxClient.pollStatus(challenge.challenge_id);
if (result.status === 'pending') {
if (attempts < maxAttempts) {
setTimeout(poll, pollInterval);
} else {
hideWaitingModal();
reject(new Error('Authentication timed out'));
}
return;
}
hideWaitingModal();
if (result.status === 'approved') {
resolve();
} else {
reject(new Error(`Authentication ${result.status}`));
}
} catch (error) {
hideWaitingModal();
reject(error);
}
};
setTimeout(poll, pollInterval);
});
}