Skip to main content

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

FieldTypeRequiredDescription
challenge_idstringYesUUID from challenge creation

Response

Pending

{
"status": "pending"
}

Approved

{
"status": "approved"
}

Denied

{
"status": "denied"
}

Expired

{
"status": "expired"
}

Response Fields

FieldTypeDescription
statusstringOne of: pending, approved, denied, expired

Status Definitions

StatusDescription
pendingWaiting for user response on mobile device
approvedUser approved via biometric authentication
deniedUser explicitly denied the request
expiredChallenge TTL (120s) exceeded without response

Errors

Error CodeHTTP StatusDescription
INVALID_REQUEST400Missing challenge id
INTERNAL_ERROR400Internal error
FORBIDDEN403Challenge belongs to different client
NOT_FOUND404Challenge 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

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);
});
}