Axum CAPTCHA Integration
This recipe shows how to integrate TrustCaptcha into an Axum application. The frontend setup is the same as for any other web application — this page focuses on the server-side validation.
The setup section gets you to a working integration in three small steps using a handler function directly. Below it, an optional refactor section shows a more reusable approach: a shared TrustCaptcha instance via Axum’s typed state plus a small helper called from each protected handler.
Preparation
Section titled “Preparation”You should have already completed the following steps before you wire TrustCaptcha into your Axum application.
Read Get-Started: Get a quick overview of the concepts behind TrustCaptcha and the integration process in get started.
Existing CAPTCHA: If you don’t have a CAPTCHA yet, sign in or create a new user account. Then create a new CAPTCHA.
1. Embed the frontend widget
Section titled “1. Embed the frontend widget”First, add the TrustCaptcha script to your page (see the JavaScript Guide for version pinning and self-hosting options).
Then place the <trustcaptcha-component> element inside your form. The widget appends a hidden tc-verification-token field on submit, which your Axum backend receives like any other form input.
<script type="module" src="https://cdn.trustcomponent.com/trustcaptcha/3.0.x/trustcaptcha.esm.min.js"></script>
<form method="post" action="/contact"> <label>Email</label> <input type="email" name="email" required>
<trustcaptcha-component sitekey="<your_site_key>"></trustcaptcha-component>
<button type="submit">Send</button></form>See the Widget Overview for the full property reference.
2. Install the Rust SDK
Section titled “2. Install the Rust SDK”cargo add trustcaptcha@^3.0cargo add axumcargo add tokio --features fullcargo add serde --features derive3. Validate the token in your handler
Section titled “3. Validate the token in your handler”use axum::{extract::Form, http::StatusCode, response::IntoResponse, routing::post, Router};use serde::Deserialize;use trustcaptcha::trust_captcha::TrustCaptcha;
#[derive(Deserialize)]struct ContactForm { email: String, #[serde(rename = "tc-verification-token")] tc_verification_token: String,}
async fn submit(Form(form): Form<ContactForm>) -> impl IntoResponse { // In production, load from env: std::env::var("TRUSTCAPTCHA_API_KEY").unwrap() let api_key = "<your_api_key>";
let trust_captcha = match TrustCaptcha::builder(api_key).build() { Ok(tc) => tc, Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "CAPTCHA setup failed.").into_response(), };
let result = match trust_captcha.get_verification_result(&form.tc_verification_token).await { Ok(result) => result, Err(_) => return (StatusCode::BAD_REQUEST, "CAPTCHA verification failed.").into_response(), };
if !result.verification_passed || result.score > 0.5 { return (StatusCode::BAD_REQUEST, "CAPTCHA verification failed.").into_response(); }
// CAPTCHA passed — request data is safe to use. // ... your business logic ...
(StatusCode::OK, "Thanks!").into_response()}
#[tokio::main]async fn main() { let app = Router::new().route("/contact", post(submit)); let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await.unwrap(); axum::serve(listener, app).await.unwrap();}That’s it — the form is now protected. For real deployments, move the API key out of the source code (see the comment) and consider explicit failover handling — see Failover Behavior for the reasoning and a code template.
Refactor: share the SDK and extract a helper
Section titled “Refactor: share the SDK and extract a helper”If you protect more than one route, build the TrustCaptcha once at startup, share it through Axum’s typed state, and wrap the verification logic in a small helper. Each protected handler then needs a single verify_token(&state.trust_captcha, ...).await? line — no copy/paste.
A custom Axum extractor (FromRequestParts) would be even more elegant, but it only sees request parts (headers, URI), not the form body — and since Form<...> consumes the body, you can’t easily share it between an extractor and the handler. A helper called from inside the handler is the simplest pattern that still keeps your <form method="post"> flow from the setup section unchanged.
Build the SDK once and share it via state
Section titled “Build the SDK once and share it via state”use axum::{routing::post, Router};use std::sync::Arc;use trustcaptcha::trust_captcha::TrustCaptcha;
#[derive(Clone)]struct AppState { trust_captcha: Arc<TrustCaptcha>,}
#[tokio::main]async fn main() { let api_key = std::env::var("TRUSTCAPTCHA_API_KEY").expect("TRUSTCAPTCHA_API_KEY missing"); let state = AppState { trust_captcha: Arc::new(TrustCaptcha::builder(api_key).build().expect("build")), };
let app = Router::new().route("/contact", post(submit)).with_state(state); let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await.unwrap(); axum::serve(listener, app).await.unwrap();}Create the helper
Section titled “Create the helper”use axum::{http::StatusCode, response::IntoResponse};use trustcaptcha::trust_captcha::TrustCaptcha;
/// Returns `Ok(())` if the CAPTCHA passed, otherwise an HTTP 400 response/// you can return directly from the handler with `?`.pub async fn verify_token( trust_captcha: &TrustCaptcha, token: &str,) -> Result<(), axum::response::Response> { let bad = || { (StatusCode::BAD_REQUEST, "CAPTCHA verification failed.").into_response() };
let result = trust_captcha .get_verification_result(token) .await .map_err(|_| bad())?;
if !result.verification_passed || result.score > 0.5 { return Err(bad()); }
Ok(())}Call the helper from your handlers
Section titled “Call the helper from your handlers”use axum::{extract::{Form, State}, response::IntoResponse};use crate::captcha::verify_token;
async fn submit( State(state): State<AppState>, Form(form): Form<ContactForm>,) -> Result<impl IntoResponse, axum::response::Response> { verify_token(&state.trust_captcha, &form.tc_verification_token).await?;
// CAPTCHA passed — request data is safe to use. // ... your business logic ...
Ok((StatusCode::OK, "Thanks!"))}A single verify_token(...).await? line per handler now opts the route into CAPTCHA verification.
Tokio runtime. The Rust SDK is async/reqwest-based. Run handlers inside #[tokio::main] and .await the verification call. Don’t block the runtime.
Sharing the SDK. A built TrustCaptcha is immutable and safe to share. Build it once at startup, wrap it in Arc<…>, share it via with_state(...), and read it via State<AppState> — don’t rebuild it per request. For configured usage (custom timeouts, proxy, custom API host), pass them via the builder: TrustCaptcha::builder(api_key).api_host(...).proxy(...).build(). See the Rust Guide for the full builder API.
Next steps
Section titled “Next steps”Once you have wired TrustCaptcha into your Axum application, you can use TrustCaptcha to its full extent. However, we still recommend the following additional technical and organizational measures:
Security rules: You can find many security settings for your CAPTCHA in the CAPTCHA settings. These include, for example, authorized websites, CAPTCHA bypass for specific IP addresses, bypass keys, IP based blocking, geoblocking, individual difficulty and duration of the CAPTCHA, and much more. Learn more about the security rules.
Privacy & GDPR compliance: Include a passage in your privacy policy that refers to the use of TrustCaptcha. We also recommend that you enter into a data processing agreement with us to stay GDPR-compliant. Learn more about data protection.
Accessibility & UX: Customize TrustCaptcha to your website so that your website is as accessible as possible and offers the best possible user experience. More about accessibility.
Failover behavior: Decide how your backend should behave when our service is temporarily unreachable. This is particularly important for high-availability flows where blocking real users during an outage is worse than letting through a small amount of unverified traffic. Learn more about failover behavior.
Testing: If you use automated testing, make sure that the CAPTCHA does not block it. Learn more about testing.