Validate tokens
Redeem every token with POST /v1/siteverify before you act on a request. Request, response, error codes, single use and safe retries.
Your server receives a token with each protected request (the agentgate_token form field, or wherever your page put it) and must redeem it with AgentGate before doing anything with the request.
Important
Server-side validation is mandatory. The widget and execute() only produce a token; a token that is never redeemed protects nothing. Refuse the request whenever success is not true.
Request
POST /v1/siteverify HTTP/1.1
Host: agentgate.example
Authorization: Bearer ags_…
Content-Type: application/json
{"token": "agr1.…", "expectedAction": "signup", "expectedOrigin": "https://app.example.com", "requestId": "order-7f3a"}Authenticate with the site's backend secret, either as Authorization: Bearer ags_… (preferred) or as a secret field in the body. If both are sent they must be equal. The body is JSON, at most 4 KiB.
| Field | Required | Meaning |
|---|---|---|
token | yes | the token from the page |
expectedAction | yes | the action you expect this token to be for, for example signup |
expectedOrigin | no, recommended | the origin of the page that should have produced it, https://host[:port] |
requestId | no, recommended | your ID for this operation, [A-Za-z0-9._:-]{1,128}; makes retries safe |
secret | no | the backend secret, when you do not send Authorization |
expectedAction is required so that a token earned on one form can never authorise a different operation.
Response
Always JSON with success.
Success
{
"success": true,
"decision": "allow",
"decisionId": "dec_…",
"action": "signup",
"origin": "https://app.example.com",
"issuedAt": 1790000000000,
"mode": "enforce"
}Failure
{
"success": false,
"error": "token_used",
"message": "receipt already redeemed"
}| Field | Meaning |
|---|---|
success | true only when the token is valid, unspent, for this action and origin, and now redeemed |
decision | allow on success |
decisionId | the decision that issued the token; find it in the dashboard |
action, origin | what the token was issued for |
issuedAt | when the token was issued, Unix milliseconds |
mode | the site's mode: monitor or enforce |
wouldDecision | monitor mode only, when enforce mode would not have allowed: challenge or block |
idempotentRetry | true when this answer repeats an earlier success for the same requestId |
test | true for testing secrets |
error, message | on failure: a stable code and a human-readable message |
Errors
| HTTP | error | Meaning | What to do |
|---|---|---|---|
| 400 | invalid_request | malformed JSON, missing token or expectedAction, or a bad requestId | fix the call |
| 401 | invalid_credential | the secret is missing, malformed, revoked, or belongs to another site | check AGENTGATE_SECRET |
| 422 | invalid_token | not a token, bad signature, unknown site or key | refuse the request |
| 422 | expired_token | the token is past its expiry | refuse; the page should verify again |
| 422 | action_mismatch | the token was issued for another action | refuse |
| 422 | origin_mismatch | the token was issued to another origin | refuse |
| 409 | token_used | already redeemed (by another requestId) | refuse |
| 503 | unavailable | AgentGate's store failed | retry with the same requestId (Retry-After: 5) |
The order of checks is: secret format, token signature, secret against the token's site, expiry, action, origin, then atomic redemption. A mismatch or an expired token is never redeemed, so it cannot burn a token that a correct call would have accepted. Every code, including the browser and gateway ones, is on Error codes.
Single use and safe retries
- Atomic. Of any number of concurrent redemptions of one token, exactly one succeeds; the others get
409 token_used. - Idempotent with
requestId. If the response to a successful redemption is lost, call again with the samerequestId: within the token's lifetime AgentGate returns the same success with"idempotentRetry": true. A different or missingrequestIdgetstoken_used. - Not a second operation. An idempotent retry confirms the first redemption. Your code must still perform the operation once.
- Lifetime. Tokens expire
RECEIPT_TTL_SECONDSafter issue (default 120, allowed 30–300). Verify at submit time and redeem promptly.
Examples
curl
curl -sS https://agentgate.example/v1/siteverify \
-H "Authorization: Bearer $AGENTGATE_SECRET" \
-H "Content-Type: application/json" \
-d '{"token":"agr1.…","expectedAction":"signup","expectedOrigin":"https://app.example.com","requestId":"order-7f3a"}'Node
// Node 18+ (global fetch)
export async function verifyAgentGate(token, requestId) {
const r = await fetch("https://agentgate.example/v1/siteverify", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: "Bearer " + process.env.AGENTGATE_SECRET },
body: JSON.stringify({ token, expectedAction: "signup", expectedOrigin: "https://app.example.com", requestId }),
signal: AbortSignal.timeout(5000),
});
const v = await r.json();
if (!v.success) throw new Error("agentgate: " + v.error); // do not perform the operation
return v;
}Python
import os
import requests
def verify_agentgate(token: str, request_id: str) -> dict:
r = requests.post(
"https://agentgate.example/v1/siteverify",
headers={"Authorization": "Bearer " + os.environ["AGENTGATE_SECRET"]},
json={"token": token, "expectedAction": "signup",
"expectedOrigin": "https://app.example.com", "requestId": request_id},
timeout=5,
)
v = r.json()
if v.get("success") is not True:
raise PermissionError(v.get("error")) # do not perform the operation
return vGo
func verifyAgentGate(ctx context.Context, token, requestID string) error {
body, _ := json.Marshal(map[string]string{
"token": token, "expectedAction": "signup",
"expectedOrigin": "https://app.example.com", "requestId": requestID,
})
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://agentgate.example/v1/siteverify", bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+os.Getenv("AGENTGATE_SECRET"))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
var v struct {
Success bool `json:"success"`
Error string `json:"error"`
}
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
return err
}
if !v.Success {
return fmt.Errorf("agentgate: %s", v.Error) // do not perform the operation
}
return nil
}Ready-made helpers that also handle the gateway are in the repository: examples/go-middleware (Go net/http) and examples/express (Node / Express). See Go and Node middleware.
When AgentGate is unreachable
Decide explicitly what your backend does when the call fails (timeout, connection error, 503). The recommended default mirrors AgentGate's own modes: refuse (fail closed) for sites in enforce mode, accept and log (fail open) for sites in monitor mode. Retry a 503 once with the same requestId.
Test without a browser
Use the testing secrets: ags_test_always_pass, ags_test_always_fail (422 invalid_token) and ags_test_token_spent (409 token_used) return fixed answers for test tokens, so you can exercise both paths of your handler in unit tests.