Integration Guide
Add CAPTCHA protection to any page in three steps.
How it works
The widget issues a challenge and lets the user solve it in the browser.
Once solved, it gives you a challengeId and solution.
Your backend sends those to BasharYab to confirm — the widget never contacts your backend directly.
Browser Your Backend BasharYab
│ │ │
├─ POST /v1/captcha/issue ──────────────────────────► │ X-API-Key
│◄─ { challenge_id, payload } ────────────────────── ──┤
│ │ │
│ [user solves the challenge] │ │
│ │ │
│ onSuccess(challengeId, solution) │ │
│ │ │
│ ── Form mode ──────────────────► │ │
│ hidden inputs auto-injected │ │
│ │ │
│ ── Callback mode ───────────────► │ │
│ your code sends via fetch/XHR │ │
│ │ │
│ ├─ POST /v1/captcha/verify ──►│
│ │ { client_id, │
│ │ challenge_id, │
│ │ solution } │
│ │◄─ { valid: true }┤
│◄─ success ─────────────────────────┤ │
Create an account & get credentials
Register, create a Workspace, then add a Project. Projects are active immediately — no domain verification required. You get three credentials:
| Credential | Where to use it | Safe to expose? |
|---|---|---|
API Key | Browser JS (BasharYab.init) | ✓ Yes |
Client ID | Your backend verify call | Low risk — keep server-side |
Client Secret | Your backend verify call | ✗ Never in browser |
Add the widget to your page
Load captcha-widget.js from our server and set window.CAPTCHA_CONFIG
before the script runs. The widget supports all provider types (proof-of-work, image-click,
slide, rotate, and text). Pick the integration mode that fits your page.
Mode A — Form (auto-inject)
Place the widget container inside your <form>.
On success the widget automatically injects two hidden inputs:
captcha_challenge_id and captcha_solution.
Your form POST carries them to your backend with zero extra code.
<form method="POST" action="/submit">
<input type="text" name="email" placeholder="Email">
<!-- Widget lives inside the form — hidden inputs are injected here -->
<div id="my-captcha"></div>
<button type="submit">Submit</button>
</form>
<script src="https://basharyab.com/js/captcha-widget.js"></script>
<script>
window.CAPTCHA_CONFIG = {
apiBase: 'https://basharyab.com',
apiKey: 'YOUR_API_KEY',
container: '#my-captcha'
// no onSuccess needed — hidden inputs injected automatically
};
</script>
Your backend receives a standard form POST:
POST /submit
Content-Type: application/x-www-form-urlencoded
email=user%40example.com
&captcha_challenge_id=550e8400-e29b-41d4-a716-446655440000
&captcha_solution=abc123
Mode B — Callback (AJAX / SPA)
Use onSuccess(challengeId, solution) when the widget is outside a form,
or when you submit via fetch / XHR. The callback receives the
challengeId and solution — send both to your backend.
<!-- Widget can be anywhere — does NOT need to be inside a form -->
<div id="my-captcha"></div>
<script src="https://basharyab.com/js/captcha-widget.js"></script>
<script>
window.CAPTCHA_CONFIG = {
apiBase: 'https://basharyab.com',
apiKey: 'YOUR_API_KEY',
container: '#my-captcha',
onSuccess: function (challengeId, solution) {
fetch('/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: document.getElementById('email').value,
captcha_challenge_id: challengeId,
captcha_solution: solution,
}),
});
}
};
</script>
onSuccess is called. You can use whichever
delivery path suits your stack — or both.
Verify on your server
When your backend receives captcha_challenge_id and captcha_solution,
call POST /v1/captcha/verify before processing the request.
Never call this endpoint from the browser.
func verifyCaptcha(r *http.Request) (bool, error) {
challengeID := r.FormValue("captcha_challenge_id")
solution := r.FormValue("captcha_solution")
if challengeID == "" || solution == "" {
return false, nil
}
body, _ := json.Marshal(map[string]string{
"client_id": "YOUR_CLIENT_ID",
"challenge_id": challengeID,
"solution": solution,
})
req, _ := http.NewRequest("POST", "https://basharyab.com/v1/captcha/verify",
bytes.NewReader(body))
req.Header.Set("X-Client-Secret", "YOUR_CLIENT_SECRET")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return false, fmt.Errorf("captcha verify: %w", err)
}
defer resp.Body.Close()
var result struct{ Valid bool `json:"valid"` }
_ = json.NewDecoder(resp.Body).Decode(&result)
return result.Valid, nil
}
<?php
$challengeId = $_POST['captcha_challenge_id'] ?? '';
$solution = $_POST['captcha_solution'] ?? '';
if (empty($challengeId) || empty($solution)) {
http_response_code(400);
exit('CAPTCHA required');
}
$ch = curl_init('https://basharyab.com/v1/captcha/verify');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'X-Client-Secret: YOUR_CLIENT_SECRET',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'client_id' => 'YOUR_CLIENT_ID',
'challenge_id' => $challengeId,
'solution' => $solution,
]),
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($result['valid'])) {
http_response_code(400);
exit('CAPTCHA verification failed');
}
import requests
def verify_captcha(form_data: dict) -> bool:
challenge_id = form_data.get("captcha_challenge_id", "")
solution = form_data.get("captcha_solution", "")
if not challenge_id or not solution:
return False
resp = requests.post(
"https://basharyab.com/v1/captcha/verify",
headers={"X-Client-Secret": "YOUR_CLIENT_SECRET"},
json={
"client_id": "YOUR_CLIENT_ID",
"challenge_id": challenge_id,
"solution": solution,
},
timeout=5,
)
return resp.json().get("valid", False)
curl -X POST https://basharyab.com/v1/captcha/verify \
-H "X-Client-Secret: YOUR_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"client_id": "YOUR_CLIENT_ID",
"challenge_id": "550e8400-e29b-41d4-a716-446655440000",
"solution": "abc123"
}'
Response
// Success
{ "success": true, "valid": true }
// Challenge not found or expired (2-min TTL)
{ "error": "challenge not found" } // 404
// Challenge already used — replay blocked
{ "error": "challenge already used" } // 409
// Wrong credentials
{ "error": "invalid credentials" } // 401
challenge_id returns 409 — replay attacks are blocked by design.API reference
| Method | Endpoint | Caller | Auth | Description |
|---|---|---|---|---|
| GET | /v1/health |
Anyone | — | Liveness check |
| POST | /v1/captcha/issue |
Browser | X-API-Key |
Issue a challenge — returns image + challenge_id |
| POST | /v1/captcha/verify |
Backend only | X-Client-Secret |
Confirm challenge_id + solution — returns { valid: true } |
Credentials
| Credential | Header / Field | Used on | Expose in browser? |
|---|---|---|---|
| API Key | X-API-Key |
/issue |
✓ Yes |
| Client ID | JSON body client_id |
/verify |
Low risk — keep server-side |
| Client Secret | X-Client-Secret |
/verify |
✗ Never |
Rate limits
| Endpoint | Limit |
|---|---|
/v1/captcha/issue | 60 req / min per IP · 6,000 req / hour per site |
/v1/captcha/verify | 120 req / min per site |
Exceeded limits → HTTP 429 with Retry-After: <seconds> header.
Error codes
| HTTP | Endpoint | Meaning |
|---|---|---|
401 | any | Missing or invalid API key / credentials |
404 | /verify | Token not found or TTL expired (5 min) |
409 | /verify | Token already used — replay blocked |
410 | /verify | Challenge expired (2-min TTL) |
429 | any | Rate limit or monthly quota exceeded |