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

HTTP
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.

FieldRequiredMeaning
tokenyesthe token from the page
expectedActionyesthe action you expect this token to be for, for example signup
expectedOriginno, recommendedthe origin of the page that should have produced it, https://host[:port]
requestIdno, recommendedyour ID for this operation, [A-Za-z0-9._:-]{1,128}; makes retries safe
secretnothe 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

JSON
{
  "success": true,
  "decision": "allow",
  "decisionId": "dec_…",
  "action": "signup",
  "origin": "https://app.example.com",
  "issuedAt": 1790000000000,
  "mode": "enforce"
}

Failure

JSON
{
  "success": false,
  "error": "token_used",
  "message": "receipt already redeemed"
}
FieldMeaning
successtrue only when the token is valid, unspent, for this action and origin, and now redeemed
decisionallow on success
decisionIdthe decision that issued the token; find it in the dashboard
action, originwhat the token was issued for
issuedAtwhen the token was issued, Unix milliseconds
modethe site's mode: monitor or enforce
wouldDecisionmonitor mode only, when enforce mode would not have allowed: challenge or block
idempotentRetrytrue when this answer repeats an earlier success for the same requestId
testtrue for testing secrets
error, messageon failure: a stable code and a human-readable message

Errors

HTTPerrorMeaningWhat to do
400invalid_requestmalformed JSON, missing token or expectedAction, or a bad requestIdfix the call
401invalid_credentialthe secret is missing, malformed, revoked, or belongs to another sitecheck AGENTGATE_SECRET
422invalid_tokennot a token, bad signature, unknown site or keyrefuse the request
422expired_tokenthe token is past its expiryrefuse; the page should verify again
422action_mismatchthe token was issued for another actionrefuse
422origin_mismatchthe token was issued to another originrefuse
409token_usedalready redeemed (by another requestId)refuse
503unavailableAgentGate's store failedretry 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 same requestId: within the token's lifetime AgentGate returns the same success with "idempotentRetry": true. A different or missing requestId gets token_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_SECONDS after issue (default 120, allowed 30–300). Verify at submit time and redeem promptly.

Examples

curl

Shell
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

JavaScript
// 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

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 v

Go

Go
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.

View as Markdown