Loading…
We use a strictly-necessary cookie to keep you signed in. Accept all also allows optional cookies for analytics, our in-app support chat, and marketing (including ad audience cookies and email open tracking). See our Privacy Policy for details. You can change your choice anytime under Settings → Privacy.
While verify-access runs for a call, AstraSync can ask the agent a live question: are you calling this counterparty right now? It POSTs a challenge to <handler>/astrasync/challenge, and the handler must answer with the counterparty in its list of pending calls. Someone who only knows the agent's ASTRA-id can't pass: the real agent isn't making their call, so its handler doesn't list it.
requiresRuntimeChallenge) for the whole endpoint.requiresRuntimeChallenge on the route). Other routes are unaffected.enableRuntimeChallenge: true). The result is recorded but never changes the decision unless the endpoint or route requires it.apiEndpoint. Calls the agent makes directly (not through the checkpoint) are challenged at the agent's own handler; an agent with no handler of its own fails the challenge on direct calls, because they bypassed governance. If the checkpoint can't be reached when the agent is issued, the agent is issued anyway and AstraSync creates its checkpoint once it's back (it tries every 5 minutes); the agent's page says so meanwhile. Get the checkpoint's bearer token then with Reset bearer token under Enterprise Governance → Workforce Guard.apiEndpoint./astrasync/challenge to your apiEndpoint. Register your base URL only (e.g. https://my-agent.example.com). If you register https://my-agent.com/astrasync, the platform POSTs to https://my-agent.com/astrasync/astrasync/challenge — a 404.Include apiEndpoint at registration if you know it; if you don't (for example you run locally), leave it blank — the owner can add it when approving. Don't guess: a wrong URL fails challenges. apiEndpoint is part of the agent's version, so changing it later is done with Upgrade (a new version with a new ASTRA-id; the old one is retired). A Workforce Guard checkpoint follows the agent through an Upgrade, at the same URL.
Mount the SDK's handler, and make your calls with AgentClient passing the same handler. Each call is listed as pending for exactly as long as it is in flight — by the destination's origin and, when you pass it, its ASTRAE-id.
import express from 'express';
import { ChallengeHandler, AgentClient } from '@astrasyncai/verification-gateway/agent';
const challengeHandler = new ChallengeHandler({ agentId: 'ASTRA-your-id' });
// AstraSync POSTs here during verify-access
const app = express();
app.use(express.json());
app.post('/astrasync/challenge', challengeHandler.expressMiddleware());
app.listen(8080);
// Make calls through AgentClient so each one is pending while it runs
const client = new AgentClient({ agentId: 'ASTRA-your-id', challengeHandler });
await client.fetch('https://shop.example.com/api/cart', {
method: 'POST',
counterpartyId: 'ASTRAE-the-endpoint-id', // pass it when you know it
});AgentClient, register them by hand (below) or your challenges will fail.If you don't use AgentClient, register the counterparty before each call and remove it afterwards. Use the identifier the challenge will name: the endpoint's ASTRAE-id when the endpoint has one, otherwise its URL.
challengeHandler.registerPending('ASTRAE-counterparty-id');
try {
await callTheCounterparty();
} finally {
challengeHandler.removePending('ASTRAE-counterparty-id');
}Concurrent calls to the same counterparty are counted, so one finishing doesn't remove the other.
On another stack, keep your own list of in-flight calls and answer with it. Never copy the counterparty from the challenge into your answer — that defeats the check.
const inFlight = new Set<string>(); // add before each call, delete after
app.post('/astrasync/challenge', (req, res) => {
const { challengeId, expiresAt } = req.body ?? {};
if (!challengeId || !expiresAt) return res.status(400).json({ error: 'malformed' });
if (new Date(expiresAt) < new Date()) return res.status(410).json({ error: 'expired' });
res.json({
challengeId,
acknowledged: true,
pendingCounterparties: [...inFlight],
respondedAt: new Date().toISOString(),
});
});AstraSync POSTs to <handler>/astrasync/challenge with Content-Type: application/json:
{
"challengeId": "a8f3c0b1-…", // fresh UUID, single-use
"type": "pending_verification",
"counterpartyId": "ASTRAE-…" | null, // the endpoint's ASTRAE-id, when the call named one
"counterpartyUrl": "https://…" | null, // otherwise the endpoint's URL
"question": "Confirm this counterparty is in your pending interaction list",
"issuedAt": "2026-05-13T12:00:00Z", // ISO 8601
"expiresAt": "2026-05-13T12:00:30Z" // ISO 8601 — 30-120 s out
}HTTP/1.1 200 OK
Content-Type: application/json
{
"challengeId": "a8f3c0b1-…", // echo the request's challengeId verbatim
"acknowledged": true,
"pendingCounterparties": [ // the calls in flight right now
"ASTRAE-vCSpmS2N0RX0JgeuxtB8Ew"
],
"respondedAt": "2026-05-13T12:00:00.842Z" // your clock at response time
}The challenge passes when all of these hold:
challengeId matches;pendingCounterparties;respondedAt is within 5 seconds of AstraSync's clock.HTTP status semantics:
200 with a valid body — passed; with a mismatch — failed.400 (malformed), 410 (expired) or any other status — failed.timeout. Unreachable — skipped.not_configured.The outcome is reported on the response as verificationContext.runtimeChallenge.status.
| Challenge status | Not required (observe only) | Required (endpoint or route) |
|---|---|---|
passed | no change | no change |
failed | no change | deny |
timeout | no change | step_up_required |
not_configured / skipped | no change | deny |
discovery, research, delegate_task, data_migration — 60 sA counterparty can override this per call with runtimeChallengeOptions.timeoutOverride (30–120 s). The response records latencyMs, from challenge sent to answer received.
So one agent's slow or dead handler can't stall a merchant's verifications: a handler that timed out or couldn't be reached isn't called again for 60 seconds (the same outcome is reported meanwhile), and at most 5 challenges to one handler run at a time — extra ones are reported as timeout at once.
challengeId is a fresh UUID per challenge and single-use: echo it verbatim. A mismatch fails the challenge.
The challenge is plain HTTP today, without signatures. A pass shows the agent's handler (or checkpoint) is reachable and that the agent is really making this call. Trust still comes from the whole verify-access decision (identity, PDLSS, attestations, trust score, endpoint policy), not from the challenge alone.
Next: agent-signed requests with DPoP (RFC 9449). Agents will declare a public key and sign their requests; the platform will check the signature. Enrolment will be additive, not breaking.