@@ -486,3 +486,183 @@ if (result.valid) {
486486 console .log (' Validation failed:' , result .reason );
487487}
488488```
489+
490+ ---
491+
492+ ## API response format
493+
494+ <Tabs >
495+ <TabItem label = " Successful response" >
496+ ``` json title="Example"
497+ {
498+ "success" : true ,
499+ "challenge_ts" : " 2022-02-28T15:14:30.096Z" ,
500+ "hostname" : " example.com" ,
501+ "error-codes" : [],
502+ "action" : " login" ,
503+ "cdata" : " sessionid-123456789" ,
504+ "metadata" : {
505+ "ephemeral_id" : " x:9f78e0ed210960d7693b167e"
506+ }
507+ }
508+ ```
509+ </TabItem >
510+ <TabItem label = " Failed response" >
511+ ``` json title="Example"
512+ {
513+ "success" : false ,
514+ "error-codes" : [" invalid-input-response" ]
515+ }
516+ ```
517+ </TabItem >
518+ </Tabs >
519+
520+ ### Response fields
521+
522+ | Field | Description |
523+ | --- | --- |
524+ | ` success ` | Boolean indicating if validation was successful |
525+ | ` challenge_ts ` | ISO timestamp when the challenge was solved |
526+ | ` hostname ` | Hostname where the challenge was served |
527+ | ` error-codes ` | Array of error codes (if validation failed) |
528+ | ` action ` | Custom action identifier from client-side |
529+ | ` cdata ` | Custom data payload from client-side |
530+ | ` metadata.ephemeral_id ` | Device fingerprint ID (Enterprise only) |
531+
532+ ### Error codes reference
533+
534+ | Error code | Description | Action required |
535+ | --- | --- | --- |
536+ | ` missing-input-secret ` | Secret parameter not provided | Ensure secret key is included |
537+ | ` invalid-input-secret ` | Secret key is invalid or expired | Check your secret key in the Cloudflare dashboard |
538+ | ` missing-input-response ` | Response parameter was not provided | Ensure token is included |
539+ | ` invalid-input-response ` | Token is invalid, malformed, or expired | User should retry the challenge |
540+ | ` bad-request ` | Request is malformed | Check request format and parameters |
541+ | ` timeout-or-duplicate ` | Token has already been validated | Each token can only be used once |
542+ | ` internal-error ` | Internal error occurred | Retry the request |
543+
544+ ---
545+
546+ ## Implementation
547+
548+ ``` js title="Example implementation"
549+ class TurnstileValidator {
550+ constructor (secretKey , timeout = 10000 ) {
551+ this .secretKey = secretKey;
552+ this .timeout = timeout;
553+ }
554+
555+ async validate (token , remoteip , options = {}) {
556+ // Input validation
557+ if (! token || typeof token !== ' string' ) {
558+ return { success: false , error: ' Invalid token format' };
559+ }
560+
561+ if (token .length > 2048 ) {
562+ return { success: false , error: ' Token too long' };
563+ }
564+
565+ // Prepare request
566+ const controller = new AbortController ();
567+ const timeoutId = setTimeout (() => controller .abort (), this .timeout );
568+
569+ try {
570+ const formData = new FormData ();
571+ formData .append (' secret' , this .secretKey );
572+ formData .append (' response' , token);
573+
574+ if (remoteip) {
575+ formData .append (' remoteip' , remoteip);
576+ }
577+
578+ if (options .idempotencyKey ) {
579+ formData .append (' idempotency_key' , options .idempotencyKey );
580+ }
581+
582+ const response = await fetch (' https://challenges.cloudflare.com/turnstile/v0/siteverify' , {
583+ method: ' POST' ,
584+ body: formData,
585+ signal: controller .signal
586+ });
587+
588+ const result = await response .json ();
589+
590+ // Additional validation
591+ if (result .success ) {
592+ if (options .expectedAction && result .action !== options .expectedAction ) {
593+ return {
594+ success: false ,
595+ error: ' Action mismatch' ,
596+ expected: options .expectedAction ,
597+ received: result .action
598+ };
599+ }
600+
601+ if (options .expectedHostname && result .hostname !== options .expectedHostname ) {
602+ return {
603+ success: false ,
604+ error: ' Hostname mismatch' ,
605+ expected: options .expectedHostname ,
606+ received: result .hostname
607+ };
608+ }
609+ }
610+
611+ return result;
612+
613+ } catch (error) {
614+ if (error .name === ' AbortError' ) {
615+ return { success: false , error: ' Validation timeout' };
616+ }
617+
618+ console .error (' Turnstile validation error:' , error);
619+ return { success: false , error: ' Internal error' };
620+
621+ } finally {
622+ clearTimeout (timeoutId);
623+ }
624+ }
625+ }
626+
627+ // Usage
628+ const validator = new TurnstileValidator (process .env .TURNSTILE_SECRET_KEY );
629+
630+ const result = await validator .validate (token, remoteip, {
631+ expectedAction: ' login' ,
632+ expectedHostname: ' example.com'
633+ });
634+
635+ if (result .success ) {
636+ // Process the request
637+ } else {
638+ // Handle failure
639+ console .log (' Validation failed:' , result .error );
640+ }
641+
642+ ```
643+
644+ ---
645+
646+ ## Best practices
647+
648+ ### Security
649+
650+ - Store secret keys securely. Use environment variables or secure key management.
651+ - Validate the token on every request. Never trust client-side validation alone.
652+ - Check additional fields. Validate the action and hostname when specified.
653+ - Monitor for abuse. Log failed validations and unusual patterns.
654+ - Use HTTPS. Always validate over secure connections.
655+
656+ ### Performance
657+
658+ - Set reasonable timeouts. Do not wait indefinitely for Siteverify responses.
659+ - Implement retry logic.
660+ - Cache validation results for the same token, if needed for your flow.
661+ - Monitor API latency. Track Siteverify response times.
662+
663+ ### Error handling
664+
665+ - Have fallback behavior for API failures.
666+ - Do not expose internal error details to users.
667+ - Properly log errors for debugging without exposing secrets.
668+ - Rate limit. Protect against validation flooding.
0 commit comments