# CaptchaLa — Full documentation for LLMs > CaptchaLa is a bot-protection platform: smart CAPTCHA, human verification, and content-moderation APIs that stop credential stuffing, scraping, fake signups, and spam across web, mobile, and desktop apps. Free tier: 10,000 verifications/month. This file inlines the full developer documentation so an LLM can answer integration questions without fetching individual pages. Challenge types: slider, click, rotate, 3D click, text, image (47 UI languages). Native SDKs: Web (JS), React, Vue, iOS, Android, Flutter, Electron, server-side PHP and Go. - Marketing: https://captcha.la - Dashboard & API keys: https://dash.captcha.la - GitHub (open-source SDKs): https://github.com/Captcha-La - Contact: supply@captcha.la The standard flow: render the widget on the client → receive a one-time token → POST it to your server → verify with your secret key via the API or a server SDK. --- # Welcome to CaptchaLa CaptchaLa provides smart CAPTCHA and content moderation API services. With simple SDK integration, you can add security protection to your apps in minutes. ::: tip See it work in 30 seconds [demo-v1.captcha.la](https://demo-v1.captcha.la) — pure HTML + PHP, MIT-licensed, view-source on every page. - [popup.html](https://demo-v1.captcha.la/popup.html) — popup mode - [float.html](https://demo-v1.captcha.la/float.html) — floating widget - [bind.html](https://demo-v1.captcha.la/bind.html) — bind to button - [inline.html](https://demo-v1.captcha.la/inline.html) — inline embed - [server-token.html](https://demo-v1.captcha.la/server-token.html) — server-issued token (anti-replay) ::: ## Quick Links - **[5-Minute Quickstart](./quickstart)** — This guide will help you integrate CaptchaLa CAPTCHA in 5 minutes. - **[Web SDK Guide](./web-sdk)** — Web SDK supports all modern browsers with multiple integration methods. - **[API Reference](./api-reference)** — CaptchaLa provides RESTful API for all server-side integrations. ## SDK Docs - [Web SDK](./web-sdk) - [Android SDK](./sdk/android) - [iOS / macOS SDK](./sdk/ios) - [Flutter SDK](./sdk/flutter) - [Electron SDK](./sdk/electron) ## Security Modes - **[Server-issued Token — Quick Start](./quickstart#server-token)** — Architecture and code samples for the backend-first token flow used in production. - **[Server-issued Token — API Reference](./api-reference#server-token)** — POST /v1/server/challenge/issue — parameters, response schema, and error codes. # Quickstart This guide will help you integrate CaptchaLa CAPTCHA in 5 minutes. ::: tip See it work in 30 seconds [demo-v1.captcha.la](https://demo-v1.captcha.la) — pure HTML + PHP, MIT-licensed, view-source on every page. - [popup.html](https://demo-v1.captcha.la/popup.html) — popup mode - [float.html](https://demo-v1.captcha.la/float.html) — floating widget - [bind.html](https://demo-v1.captcha.la/bind.html) — bind to button - [inline.html](https://demo-v1.captcha.la/inline.html) — inline embed - [server-token.html](https://demo-v1.captcha.la/server-token.html) — server-issued token (anti-replay) ::: ## 1. Create Account Sign up for a free account in the dashboard [Sign Up Now →](https://dash.captcha.la/register) ## 2. Create Application Create an app in the dashboard to get your App Key ## 3. Install SDK Choose your preferred installation method ```html ``` ```bash # or via npm npm install captchala ``` ## 4. Initialize CAPTCHA Initialize the CAPTCHA component in your page ```html ``` ## 5. Server Verification Verify the token on your server ```bash POST https://apiv1.captcha.la/v1/validate X-App-Key: YOUR_APP_KEY X-App-Secret: YOUR_APP_SECRET Content-Type: application/json { "pass_token": "", "client_ip": "" } ``` ```json { "code": 0, "data": { "valid": true, "action": "login", "challenge_id": "ch_xxx", "uid": null, "client_ip": "1.2.3.4", "risk_score": 12 } } ``` ::: warning Always check `data.valid === true` **and** `data.action` matches the scene you expected. Pass tokens are single-use; the same `pt_xxx` cannot be validated twice. ::: ## Server-issued Token Mode (recommended for production) {#server-token} For sensitive actions (login, register, payment) we recommend the server-issued token flow: your backend first requests a short-lived server_token, which the browser then uses to initialize the CAPTCHA. This prevents abuse from a leaked app_key. ### When to use - Recommended: register, login, password reset, payment, points redemption, and any endpoint an attacker can script. - Optional: casual public forms, search boxes, and low-value interactions where convenience matters more. ### 1. Backend issues server_token Call /v1/server/challenge/issue from your own server, using the X-App-Key and X-App-Secret headers. Never expose these headers to the browser. ```bash # Server-side only — never call this from a browser curl -X POST https://apiv1.captcha.la/v1/server/challenge/issue \ -H "X-App-Key: YOUR_APP_KEY" \ -H "X-App-Secret: YOUR_APP_SECRET" \ -d "action=login&ttl=300&max_uses=1&bind_ip=1.2.3.4" # → { "code": 0, "data": { "server_token": "sct_...", "expires_in": 300 } } ``` ### 2. Frontend renders CaptchaLa with the server_token Pass the token to your CaptchaLa component. The SDK forwards it to the challenge initialization when initializing the challenge. ```js // Browser fetches the token from YOUR backend, not from CaptchaLa directly const { serverToken } = await fetch('/api/captcha/issue').then(r => r.json()); Captchala.init({ appKey: 'YOUR_APP_KEY', serverToken, // single-use, short-lived product: 'popup', action: 'login', }) .appendTo('#captcha-container') .onSuccess(res => submitForm(res.token)); ``` ### Security notes - Never put app_secret in frontend code, mobile apps, or public repositories. It must stay server-side. - Enable "Require server-issued challenge token" in the dashboard to reject any challenge attempted without a server_token. - Keep ttl short (300s default, 900s max) and prefer max_uses=1 to reduce the impact of token leakage. ## Next steps - [Web SDK](./web-sdk) - [API Reference](./api-reference) # Web SDK Web SDK supports all modern browsers with multiple integration methods. ::: tip See it work in 30 seconds [demo-v1.captcha.la](https://demo-v1.captcha.la) — pure HTML + PHP, MIT-licensed, view-source on every page. - [popup.html](https://demo-v1.captcha.la/popup.html) — popup mode - [float.html](https://demo-v1.captcha.la/float.html) — floating widget - [bind.html](https://demo-v1.captcha.la/bind.html) — bind to button - [inline.html](https://demo-v1.captcha.la/inline.html) — inline embed - [server-token.html](https://demo-v1.captcha.la/server-token.html) — server-issued token (anti-replay) ::: ## Quick start ```html ``` ## Installation ### CDN ```html ``` ### NPM ```bash npm install captchala # or framework wrappers npm install @captcha-la/vue npm install @captcha-la/react ``` ```js import Captchala from 'captchala'; import 'captchala/dist/captchala.css'; ``` ## Modes ### Popup Mode ```js Captchala.init({ appKey: 'YOUR_APP_KEY', product: 'popup', action: 'login' }) .bindTo('#login-btn') .onSuccess(res => sendToBackend(res.token)); ``` ### Float Mode ```js Captchala.init({ appKey: 'YOUR_APP_KEY', product: 'float', action: 'browse' }) .appendTo('#captcha-container') .onSuccess(res => sendToBackend(res.token)); ``` ### Bind Mode ```js Captchala.init({ appKey: 'YOUR_APP_KEY', product: 'bind', action: 'login' }) .bindTo('#submit-button') .onSuccess(res => submitForm(res.token)); // fires only after challenge passes ``` ### Embed Mode ```js Captchala.init({ appKey: 'YOUR_APP_KEY', product: 'embed', action: 'register' }) .appendTo('#captcha-container') .onSuccess(res => sendToBackend(res.token)); ``` ## Common options | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `appKey` | string | — | Application Key (required) | | `product` | string | `popup` | Display mode: popup | float | embed | bind | | `action` | string | `default` | Business scene (e.g. login, register, pay). The server applies different security policies per scene. | | `lang` | string | `auto` | BCP-47 tag (e.g. `en`, `ja`, `pt-BR`) or `auto` to follow `navigator.language`. See [Supported languages](/supported-languages) — 54 locales. | | `serverToken` | string | — | Single-use token (sct_xxx) issued by your server. Strongly recommended in production — prevents unbounded challenge refresh abuse. | | `onServerTokenExpired` | `() => Promise` | — | Called when serverToken expires; return a new one so SDK can continue without interrupting the flow. | | `enableVoice` | boolean | `true` | Show the audio-captcha entry point (accessibility support for visually impaired users). | ## Server-side validation After `onSuccess`, send `res.token` (prefix `pt_`) to your own backend, then validate it server-side: ```bash POST https://apiv1.captcha.la/v1/validate X-App-Key: YOUR_APP_KEY X-App-Secret: YOUR_APP_SECRET Content-Type: application/json { "pass_token": "", "client_ip": "" } ``` See the [API Reference](./api-reference) for the full validation endpoint. # API Reference CaptchaLa provides RESTful API for all server-side integrations. ## Base URL ``` https://apiv1.captcha.la ``` ## Authentication All API requests require authentication headers: ``` X-App-Key: YOUR_APP_KEY X-App-Secret: YOUR_APP_SECRET ``` ::: warning `X-App-Secret` is server-side only. Never expose it to browsers, mobile apps, or public repos. ::: ## Server-side Validation API > 💡 **Use a server SDK to skip the boilerplate.** Wraps the endpoint, handles retries, surfaces typed errors: > - **PHP** — [`Captcha-La/captchala-php`](https://github.com/Captcha-La/captchala-php) ([中文](https://github.com/Captcha-La/captchala-php/blob/main/README_zh.md)) > - **Go** — `go get github.com/Captcha-La/captchala-go` · [README](https://github.com/Captcha-La/captchala-go) After user passes the frontend CAPTCHA, your server needs to validate the token returned by SDK. Below are the server-side validation endpoints. ### Server-side Token Validation Validate pass_token from frontend SDK (with pt_ prefix). Requires X-App-Key and X-App-Secret headers. ```bash POST /v1/validate X-App-Key: YOUR_APP_KEY X-App-Secret: YOUR_APP_SECRET Content-Type: application/json { "pass_token": "pt_xxx", "client_ip": "1.2.3.4" } ``` ```json { "code": 0, "data": { "valid": true, "challenge_id": "ch_xxx", "action": "login", "uid": null, "client_ip": "1.2.3.4", "risk_score": 12 } } ``` ::: tip Validate `data.valid === true` **and** that `data.action` matches the scene you expected (reject if a token from a `pay` flow is presented at `/login`). Tokens are single-use. ::: ### Token Types The SDK may hand your frontend different token types depending on service conditions. The prefix tells you everything: | Prefix | Issued when | `/v1/validate` returns | Recommended handling | | --- | --- | --- | --- | | `pt_` | Verification passed normally | `valid: true` (single-use) | Accept | | `dg_` | Plan quota exhausted (degraded mode) | `valid: false`, `degraded: true`, `reason` | Your decision — see below | | `offline_` | Main API unreachable; issued by the bypass worker | Verified against the backup API automatically when using a server SDK | Accept after backup verification | | `co_` | Main **and** backup unreachable; generated client-side | Cannot be verified server-side | Apply your own risk controls (rate limits, etc.) | ### Degraded Mode (quota exhausted) {#degraded-mode} When an app has exhausted its monthly quota (and overage billing is not available), the widget does **not** interrupt your end-user flow. Instead of showing an error, the SDK completes immediately with a `dg_` token: - The end user sees no error — your form submits normally. - Your server's `/v1/validate` returns: ```json { "code": 0, "data": { "valid": false, "degraded": true, "reason": "quota_exhausted", "expired": false } } ``` Key properties: - `valid` is **always `false`** for degraded tokens — secure by default. If your code only checks `valid`, nothing changes for you. - Whether to let the request through is **your decision**: accept `valid || degraded` to keep conversions flowing while you upgrade your plan, or reject and show your own fallback. - `dg_` tokens are HMAC-signed per app and expire after 5 minutes. Issuing them costs no quota and is never billed. - Server SDKs expose this as `isDegraded()` / `getDegradedReason()` (PHP) and `result.Degraded` / `result.DegradedReason` (Go). ::: info Expired subscriptions never trigger degraded mode — they automatically fall back to the Free plan and keep serving within the free quota. Degraded mode only covers usage beyond your plan's quota. ::: ## Server-issued Challenge Token {#server-token} Issue a short-lived server_token from your backend. The browser then passes this token when initializing a challenge, proving the request originated from your trusted server. ### Issue Server Token Call this endpoint from your own server only. Requires X-App-Key + X-App-Secret. Response contains a server_token (sct_ prefix) that the frontend forwards to the challenge initialization. ```bash POST /v1/server/challenge/issue X-App-Key: YOUR_APP_KEY X-App-Secret: YOUR_APP_SECRET Content-Type: application/x-www-form-urlencoded action=login&ttl=300&max_uses=1&bind_ip=1.2.3.4 ``` ```json { "code": 0, "data": { "server_token": "sct_xxxxxxxxxxxx", "expires_in": 300, "issued_at": 1713600000 } } ``` #### Body Parameters (form-urlencoded) | Field | Description | | --- | --- | | `action` | Business scene, e.g. login, register, pay. Must match the action used in the challenge initialization. | | `ttl` | Token lifetime in seconds. Default 300, max 900. | | `max_uses` | Maximum times the token may be consumed. Default 10. | | `bind_ip` | Bind token to a client IP. Initialization from another IP is rejected. | | `bind_device_id` | Bind token to a specific device id. | | `bind_fingerprint` | Bind token to a specific browser fingerprint. | ### Initialize Challenge Called by the SDK to start a CAPTCHA challenge. Accepts an optional server_token issued by /v1/server/challenge/issue. | Field | Description | | --- | --- | | `app_key` | Your public App Key. | | `action` | Business scene. Must match the action used when issuing the server_token. | | `server_token` | Optional; required when the app has server_token_required = true. | ::: info If server_token_required is enabled for this app in the dashboard, the challenge initialization will reject requests that do not carry a valid server_token. ::: ## Error Codes | Code | Description | | --- | --- | | `invalid_app_key` | Invalid App Key | | `invalid_app_secret` | Invalid App Secret | | `challenge_expired` | Challenge expired | | `challenge_not_found` | Challenge not found | | `invalid_answer` | Invalid answer | | `token_expired` | Token expired | | `token_already_used` | Token already used | | `token_not_found` | Token not found | | `quota_exceeded` | Quota exceeded | | `rate_limited` | Rate limited | | `rate_limit_exceeded` | Too many issuance requests for this app. Back off and retry. | # Supported languages Default `lang: 'auto'` follows the browser language. Pass an explicit BCP-47 tag (e.g. `'ja'`, `'pt-BR'`) to lock the widget. **54 locales shipped:** | Tag | Native | Language | | --- | --- | --- | | `ar` | العربية | Arabic | | `bg` | Български | Bulgarian | | `bn` | বাংলা | Bengali | | `cs` | Čeština | Czech | | `da` | Dansk | Danish | | `de` | Deutsch | German | | `el` | Ελληνικά | Greek | | `en` | English | English | | `es` | Español | Spanish | | `et` | Eesti | Estonian | | `fa` | فارسی | Persian | | `fi` | Suomi | Finnish | | `fil` | Filipino | Filipino | | `fr` | Français | French | | `gu` | ગુજરાતી | Gujarati | | `he` | עברית | Hebrew | | `hi` | हिन्दी | Hindi | | `hr` | Hrvatski | Croatian | | `hu` | Magyar | Hungarian | | `id` | Bahasa Indonesia | Indonesian | | `it` | Italiano | Italian | | `ja` | 日本語 | Japanese | | `km` | ខ្មែរ | Khmer | | `kn` | ಕನ್ನಡ | Kannada | | `ko` | 한국어 | Korean | | `lo` | ລາວ | Lao | | `ml` | മലയാളം | Malayalam | | `mr` | मराठी | Marathi | | `ms` | Bahasa Melayu | Malay | | `my` | မြန်မာ | Burmese | | `ne` | नेपाली | Nepali | | `nl` | Nederlands | Dutch | | `no` | Norsk | Norwegian | | `pa` | ਪੰਜਾਬੀ | Punjabi | | `pl` | Polski | Polish | | `pt` | Português | Portuguese | | `pt-BR` | Português (Brasil) | Portuguese (Brazil) | | `ro` | Română | Romanian | | `ru` | Русский | Russian | | `si` | සිංහල | Sinhala | | `sk` | Slovenčina | Slovak | | `sl` | Slovenščina | Slovenian | | `sr` | Српски | Serbian | | `sv` | Svenska | Swedish | | `sw` | Kiswahili | Swahili | | `ta` | தமிழ் | Tamil | | `te` | తెలుగు | Telugu | | `th` | ไทย | Thai | | `tr` | Türkçe | Turkish | | `uk` | Українська | Ukrainian | | `ur` | اردو | Urdu | | `vi` | Tiếng Việt | Vietnamese | | `zh-CN` | 简体中文 | Simplified Chinese | | `zh-TW` | 繁體中文 | Traditional Chinese | RTL: `ar`, `fa`, `he`, `ur`. Region suffixes normalize via prefix match (`en-US` → `en`, `fr-CA` → `fr`). # React SDK Official React component + hook for CaptchaLa CAPTCHA — published as [`@captcha-la/react`](https://www.npmjs.com/package/@captcha-la/react). ## Live demo ::: tip 📦 [demo-v1.captcha.la/react](https://demo-v1.captcha.la/react/) — runnable demo with all four product modes. Source: [Captcha-La/react-example](https://github.com/Captcha-La/react-example). ::: ## Install ```bash npm install @captcha-la/react # or yarn add @captcha-la/react # or pnpm add @captcha-la/react ``` Peer dependencies: `react@^17 || ^18 || ^19`, `react-dom@^17 || ^18 || ^19`. ## Quick start ### Component ```tsx import { Captchala } from '@captcha-la/react' function App() { return ( console.log('pass_token:', result.token)} onError={(err) => console.error(err)} /> ) } ``` ### Hook ```tsx import { useCaptchala } from '@captcha-la/react' function LoginForm() { const { ready, verify } = useCaptchala({ appKey: 'your-app-key', product: 'bind', action: 'login', }) async function handleSubmit(e) { e.preventDefault() const result = await verify() // result.token → submit with the form } return (
) } ``` ## Props | Prop | Type | Default | Description | |------|------|---------|-------------| | `appKey` | `string` | *required* | CaptchaLa application key | | `serverToken` | `string` | - | One-time, server-issued challenge token. Required when the app has `server_token_required=true`. | | `product` | `'popup' \| 'float' \| 'embed' \| 'bind'` | `'popup'` | Display mode | | `action` | `string` | `'default'` | Action identifier | | `lang` | `string` | `'zh-CN'` | Language code | | `onSuccess` | `(result) => void` | - | Success callback | | `onError` | `(error) => void` | - | Error callback | | `onClose` | `() => void` | - | Close callback | | `onReady` | `() => void` | - | Ready callback | | `className` | `string` | - | Container class name | | `style` | `CSSProperties` | - | Container inline styles | ## Methods (via `ref`) ```tsx import { useRef } from 'react' import { Captchala, type CaptchalaRef } from '@captcha-la/react' function App() { const ref = useRef(null) return ( <> ) } ``` | Method | Description | |--------|-------------| | `verify()` | Trigger verification | | `reset()` | Reset CAPTCHA state | | `destroy()` | Destroy the instance | | `bindTo(selector)` | Bind to element (for `bind` mode) | | `setLang(lang)` | Switch language in place | ## Production: `serverToken` mode ```tsx import { useState, useEffect } from 'react' import { Captchala } from '@captcha-la/react' function LoginPage() { const [token, setToken] = useState() useEffect(() => { fetch('/api/captcha-token') // your own backend .then((r) => r.json()) .then((d) => setToken(d.server_token)) }, []) if (!token) return
Loading…
return ( console.log('pass_token:', r.token)} /> ) } ``` See [API Reference](/api-reference) for the full backend contract. ## Links - [npm](https://www.npmjs.com/package/@captcha-la/react) · [GitHub](https://github.com/Captcha-La/react) · [example](https://github.com/Captcha-La/react-example) - [Web SDK overview](/web-sdk) · [API Reference](/api-reference) # Vue SDK Official Vue 3 component for CaptchaLa CAPTCHA — published as [`@captcha-la/vue`](https://www.npmjs.com/package/@captcha-la/vue). ## Live demo ::: tip 📦 [demo-v1.captcha.la/vue](https://demo-v1.captcha.la/vue/) — runnable demo with all four product modes. Source: [Captcha-La/vue-example](https://github.com/Captcha-La/vue-example). ::: ## Install ```bash npm install @captcha-la/vue # or yarn add @captcha-la/vue # or pnpm add @captcha-la/vue ``` Peer dependency: `vue@^3.2.0`. ## Quick start ```vue ``` ## Props | Prop | Type | Default | Description | |------|------|---------|-------------| | `appKey` | `string` | *required* | CaptchaLa application key | | `serverToken` | `string` | - | One-time, server-issued challenge token. Required when the app has `server_token_required=true`. | | `product` | `'popup' \| 'float' \| 'embed' \| 'bind'` | `'popup'` | Display mode | | `action` | `string` | `'default'` | Action identifier (e.g. `login`, `register`, `checkout`) | | `lang` | `string` | `'zh-CN'` | Language code (`zh-CN`, `en`, `ja`, …) | | `bindTo` | `string \| HTMLElement` | - | Element selector or node to bind to (only for `product="bind"`) | ## Events | Event | Payload | Description | |-------|---------|-------------| | `success` | `{ token, type, action }` | Verification succeeded | | `error` | `error` | Verification failed | | `close` | — | CAPTCHA closed | | `ready` | — | CAPTCHA ready | ## Methods (via `ref`) ```vue ``` | Method | Description | |--------|-------------| | `verify()` | Trigger verification | | `reset()` | Reset CAPTCHA state | | `destroy()` | Destroy the instance | | `bindTo(selector)` | Bind to element (for `bind` mode) | | `setLang(lang)` | Switch language in place | ## Production: `serverToken` mode For high-value flows (login, register, payment) we recommend the **server-issued token flow**. Your backend mints a one-time `server_token` (5-minute TTL) via `POST /v1/server/challenge/issue` and hands it to the browser: ```vue ``` See [API Reference](/api-reference) for the full backend contract. ## Links - [npm](https://www.npmjs.com/package/@captcha-la/vue) · [GitHub](https://github.com/Captcha-La/vue) · [example](https://github.com/Captcha-La/vue-example) - [Web SDK overview](/web-sdk) · [API Reference](/api-reference) # iOS / macOS SDK Apple-platform SDK for **iOS 15+** and **macOS 11+ (Big Sur)** via Mac Catalyst, shipped as an `.xcframework` plus a `.bundle` of localized assets. Drop-in for SwiftUI, UIKit, and AppKit projects. ## Demo on GitHub ::: tip 📦 [Captcha-La/iosmacos-demo](https://github.com/Captcha-La/iosmacos-demo) — full runnable example with every integration step. ::: ## Install Add CaptchaLa to your `Podfile` (CocoaPods 1.10+): ```ruby # Podfile platform :ios, '13.0' target 'YourApp' do use_frameworks! pod 'Captchala', '~> 1.0.2' end ``` ```bash pod install ``` Or, if you prefer manual integration: Download the latest iOS release from the [CaptchaLa dashboard](https://dash.captcha.la). The archive contains: - `Captchala.xcframework` — the compiled SDK - `Captchala.bundle` — localized resources Drop both next to your `.xcodeproj`. The demo project references them by file name; no manual linking is needed beyond keeping their location stable. For manual integration, download the xcframework directly: [dash.captcha.la/downloads](https://dash.captcha.la/downloads) ```text YourApp/ ├── YourApp.xcodeproj ├── YourApp/ ├── Captchala.xcframework └── Captchala.bundle ``` Open the project and run on the simulator, a device, or **My Mac (Mac Catalyst)**: ```bash open YourApp.xcodeproj # Cmd-R in Xcode ``` ## Quick start ```swift import SwiftUI import Captchala final class CaptchaDelegateBridge: NSObject, CaptchalaDelegate { var onSuccess: ((CaptchalaResult) -> Void)? var onFailure: ((CaptchalaError) -> Void)? var onClose: (() -> Void)? func captcha(didSucceedWith result: CaptchalaResult) { onSuccess?(result) } func captcha(didFailWithError error: CaptchalaError) { onFailure?(error) } func captchaDidClose() { onClose?() } } struct LoginView: View { @State private var bridge = CaptchaDelegateBridge() @State private var status = "Tap to verify" var body: some View { Button("Verify with CAPTCHA", action: startVerify) Text(status).font(.caption) } private func startVerify() { bridge.onSuccess = { r in // Send r.passToken to your backend for validation. status = "OK: \(r.passToken)" } bridge.onFailure = { e in status = "ERROR [\(e.code)] \(e.message)" } Task { @MainActor in // 1. Fetch a one-shot server_token from YOUR backend. let token = await fetchServerTokenFromYourBackend() // 2. Build config and present. let config = CaptchalaConfigBuilder() .appKey("YOUR_APP_KEY") .action("login") .lang("en") // en, zh-CN, zh-TW, ja, ko, ms, vi, id .theme("light") // "light" | "dark" .enableVoice(true) .enableOfflineMode(true) .serverToken(token) .onServerTokenExpired { await fetchServerTokenFromYourBackend() } .build() guard let presenter = topViewController() else { return } CaptchalaClient.shared .initialize(config: config) .setDelegate(bridge) .verify(from: presenter) } } } ``` ::: tip Mac Catalyst & native macOS The exact same Swift code runs on iOS, Mac Catalyst, and native macOS. On Catalyst pass any `UIViewController`. On native macOS use `NSViewController` and call `.verify()` without an argument — the SDK presents in its own `NSWindow`. ::: ## API surface | Symbol | Purpose | | --- | --- | | `CaptchalaClient.shared` | Shared singleton. All entry points hang off this. | | `CaptchalaConfigBuilder()` | Fluent builder. Set `appKey`, `action`, `lang`, `theme`, `enableVoice`, `enableOfflineMode`, `serverToken`. | | `initialize(config:)` | Apply the built config. Returns `self` so you can chain `setDelegate`. | | `setDelegate(_:)` | Provide an `NSObject` conforming to `CaptchalaDelegate`. The SDK keeps a weak reference. | | `verify(from: presenter)` | Present the CAPTCHA over the given UIViewController (iOS / Catalyst). The SDK pushes a modal sheet. | | `CaptchalaResult` | Returned via `captcha(didSucceedWith:)`. Fields: `passToken`, `challengeId`, `ttl`, `isOffline`, `isClientOnly`. | | `onServerTokenExpired { … }` | Async closure that re-fetches a fresh `server_token` if the prior one expires mid-challenge. | ## Server-side validation Forward `result.passToken` (or `result.token`) to your backend and validate it against the CaptchaLa API. Never expose `X-App-Secret` in client code. ```bash POST https://apiv1.captcha.la/v1/validate X-App-Key: YOUR_APP_KEY X-App-Secret: YOUR_APP_SECRET Content-Type: application/json { "pass_token": "", "client_ip": "" } ``` See the [API Reference](../api-reference) for the full validation endpoint and `X-App-Key` / `X-App-Secret` flow. ## Troubleshooting - **`Captchala.xcframework` not found** The `.xcframework` and `.bundle` must sit next to `Example.xcodeproj`. The demo references both by file name; keep their location stable when updating the SDK. - **Mac Catalyst destination missing** In Xcode, enable *Mac (Mac Catalyst)* under your target's *Supported Destinations*. The demo target ships with `SUPPORTS_MACCATALYST = YES`. - **Modal not appearing** Pass a real `UIViewController` to `verify(from:)`. The demo walks `UIApplication.connectedScenes` to find the topmost active key-window controller — copy that helper if you only have a SwiftUI `View`. - **`Info.plist` privacy strings on macOS** For Catalyst / native macOS targets enable the **Outgoing Connections (Client)** sandbox capability. The SDK only makes HTTPS calls, no microphone or camera access. ## Requirements - iOS 15+ (device or simulator) - macOS 11+ (Big Sur) via Mac Catalyst, macOS 13+ for native targets - Xcode 15+ - Swift 5.7+ (async/await) # Android SDK Android SDK for **Android 5.0+ (API 21+)**, packaged as a single `.aar`. Compose- and View-based apps both work; the SDK is UI-framework-agnostic. ## Demo on GitHub ::: tip 📦 [Captcha-La/android-demo](https://github.com/Captcha-La/android-demo) — full runnable example with every integration step. ::: ## Install The SDK ships as a single `.aar` you download from the [CaptchaLa dashboard](https://dash.captcha.la). Drop it into your app module's `libs/` folder and reference it from Gradle: ```groovy // settings.gradle (or repositories block) dependencyResolutionManagement { repositories { mavenCentral() } } // app/build.gradle android { defaultConfig { minSdk 21 } } dependencies { implementation 'la.captcha:captchala:1.0.2' // Maven Central } ``` Or download the AAR for manual integration: ```groovy // app/build.gradle (drop captchala.aar into app/libs/) android { defaultConfig { minSdk 21 ndk { abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' } } } dependencies { implementation files('libs/captchala.aar') implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.code.gson:gson:2.10.1' implementation 'org.bouncycastle:bcprov-jdk18on:1.77' } ``` Download: [dash.captcha.la/downloads](https://dash.captcha.la/downloads) (latest AAR). ```xml ``` Build and install the demo with: ```bash ./gradlew installDebug ``` ## Quick start ```kotlin import la.captcha.sdk.CaptchalaClient import la.captcha.sdk.CaptchalaConfig import la.captcha.sdk.CaptchalaError import la.captcha.sdk.CaptchalaListener import la.captcha.sdk.CaptchalaResult class LoginActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 1. Fetch a one-shot server_token from YOUR backend // (which calls the CaptchaLa server API with X-App-Key + X-App-Secret). val serverToken = runBlocking { fetchServerTokenFromYourBackend() } // 2. Build config — destroy any prior client so a fresh state is built. CaptchalaClient.destroy() val client = CaptchalaClient.getClient(applicationContext).init( CaptchalaConfig.Builder() .appKey("YOUR_APP_KEY") .action("login") // login, register, pay, … .lang("en") // en, zh-CN, zh-TW, ja, ko, ms, vi, id .theme("light") // "light" | "dark" .enableVoice(true) .enableOfflineMode(true) .serverToken(serverToken) .onServerTokenExpired { fetchServerTokenFromYourBackend() } .build() ) // 3. Listen for terminal events. client.setListener(object : CaptchalaListener { override fun onReady() { /* challenge UI ready */ } override fun onSuccess(result: CaptchalaResult) { // Send result.passToken to your backend for validation. sendToBackend(result.passToken) } override fun onFail(error: CaptchalaError) { /* recoverable */ } override fun onError(error: CaptchalaError) { /* terminal */ } override fun onClose() { /* user dismissed */ } }) // 4. Open the CAPTCHA from a button tap. findViewById