Get started
Protect a form in three steps: create a site, embed the widget, and validate the token on your server.
This guide protects one HTML form. It takes about ten minutes. You need access to the AgentGate console (request access) and a server that handles the form.
In the examples, https://agentgate.example stands for your AgentGate endpoint: the console shows the right value in its snippets.
Important
Step 3 is not optional. The widget only produces a token; your server must redeem that token with POST /v1/siteverify and refuse the request when it does not succeed. Without step 3 nothing is protected.
1. Create a site
A site is one website or app you protect. It has a public site key (site_…) for your pages, one or more backend secrets (ags_…) for your server, the origins allowed to use the site key, and the actions a token can be issued for.
In the console at /app:
- Choose Add site, enter the site address (for example
https://shop.example) and a name. The site is created in monitor mode with one browser-checked action,submit. - Create a backend secret and copy it. It is shown once; AgentGate keeps only a hash. Store it in your server's secret manager or environment as
AGENTGATE_SECRET, never in page code. - Keep the page open: its install check says "waiting" until AgentGate sees the first request for the site key, then "live".
Only the listed origins may use the site key from a browser. Add every origin that serves the form, including staging hosts. For local development and CI use the testing keys, which work from any origin.
2. Embed the widget
Add the script once per page and put the widget element inside the form. Use your site key and the action you configured:
HTML form
<script src="https://agentgate.example/sdk/v1/agentgate.js" async></script>
<form method="post" action="/contact">
<!-- your fields -->
<div class="agentgate" data-sitekey="site_…" data-action="submit"
data-mode="managed"></div>
<button>Send</button>
</form>JavaScript
// Load https://agentgate.example/sdk/v1/agentgate.js with a <script> tag first.
// Call init at page load, so the behaviour trace covers the whole visit.
AgentGate.init({ siteKey: "site_…", endpoint: "https://agentgate.example" });
form.addEventListener("submit", async (e) => {
e.preventDefault();
const { token } = await AgentGate.execute({ action: "submit" });
await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json", "X-AgentGate-Token": token },
body: JSON.stringify(Object.fromEntries(new FormData(form))),
});
});With the HTML form, the widget adds a hidden agentgate_token field and fills it when the form is submitted. The verification runs at submit time, so the token is fresh (tokens live two minutes by default). data-mode chooses what people see: nothing (invisible, the default), a small "Protected" status chip (managed), or a Verify button (interactive). See Widget modes.
With JavaScript, call AgentGate.init() at page load and AgentGate.execute() at submit time, then send the token with the request in any field or header your server reads. Calling execute() without init() works but is treated as suspicious (the visible check is asked for), because the behaviour trace is missing. See the JavaScript API.
If your page sets a Content Security Policy, allow the endpoint in script-src and connect-src and add worker-src blob: (details).
3. Validate the token
When the form arrives, send the token to /v1/siteverify with the backend secret, before you do anything with the request:
curl
curl -sS https://agentgate.example/v1/siteverify \
-H "Authorization: Bearer $AGENTGATE_SECRET" \
-H "Content-Type: application/json" \
-d '{"token":"PASTE_THE_TOKEN","expectedAction":"submit","expectedOrigin":"https://shop.example"}'Node
// Node 18+ (global fetch). token = the form's agentgate_token field.
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: "submit",
expectedOrigin: "https://shop.example",
requestId, // your ID for this operation; a retry with it is safe
}),
});
const v = await r.json();
return v.success === true; // false: do not perform the operation (v.error says why)
}Python
# pip install requests. token = the form's agentgate_token field.
import os
import requests
def verify_agentgate(token: str, request_id: str) -> bool:
r = requests.post(
"https://agentgate.example/v1/siteverify",
headers={"Authorization": "Bearer " + os.environ["AGENTGATE_SECRET"]},
json={
"token": token,
"expectedAction": "submit",
"expectedOrigin": "https://shop.example",
"requestId": request_id,
},
timeout=5,
)
return r.json().get("success") is True # False: reject (see "error")Go
// token = r.FormValue("agentgate_token")
func verifyAgentGate(ctx context.Context, token, requestID string) (bool, error) {
body, _ := json.Marshal(map[string]string{
"token": token,
"expectedAction": "submit",
"expectedOrigin": "https://shop.example",
"requestId": requestID,
})
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
"https://agentgate.example/v1/siteverify", bytes.NewReader(body))
if err != nil {
return false, 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 false, 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 false, err
}
return v.Success, nil // false: do not perform the operation
}A success looks like {"success":true,"decision":"allow","decisionId":"…","action":"submit","origin":"https://shop.example","issuedAt":…,"mode":"monitor"}. Anything else carries an error code such as token_used or expired_token: do not perform the operation. Each token can be redeemed once. The full contract, including safe retries, is in Validate tokens.
4. Watch, then enforce
In monitor mode the widget always gets a token and /v1/siteverify accepts every valid one; wouldDecision in the answer (and on the dashboard) shows what enforce mode would have done. When the decisions look right, switch the site to enforce in the console. See Monitor and enforce.
The site's integration health warns you if tokens are issued but your server never verifies them, if pages on other origins try to use the site key, or if siteverify calls keep failing.
Next steps
- Using React, Vue or a mobile WebView? Read Single-page apps and mobile.
- Protect routes without a page (APIs, webhooks, agents) with the gateway.
- Write end-to-end tests with the testing keys.