# 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