Complete example

Complete example

In this tutorial we will create a simple html form, serve it locally with a simple web-server, then protect using Private Captcha and, finally, verify form submission. And all this in the comfort of your own computer.

Basic webpage and server

    • index.html
    • main.go
  • Create a simple page with a form element in the middle of the page.

    index.html
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <title>Private Captcha Example</title>
        <style>
            body {
                display:flex; 
                flex-direction: column; 
                min-height: 100vh;
            }
    
            form {
                max-width: 32rem; 
                margin: auto; 
                display: flex; 
                flex-direction: column; 
                gap: 20px; 
                border: 1px #ccc solid;
                padding: 20px;
            }
        </style>
    </head>
    <body>
        <div style="display: flex; flex: 1 1 0%">
            <form action='/submit' method="POST">
                <label> Email: </label>
                <input type="email" name="email" placeholder="Email address" required />
                <button id="formSubmit" type="submit" disabled> Submit </button>
            </form>
        </div>
    </body>
    </html>

    And a web-server that will serve it (Go is used here as an example).

    main.go
    package main
    
    import (
    	"log"
    	"net/http"
    )
    
    func main() {
    	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    		if r.URL.Path == "/" {
    			http.ServeFile(w, r, "index.html")
    			return
    		}
    		// Return 404 for any other paths
    		http.NotFound(w, r)
    	})
    
    	if err := http.ListenAndServe(":8081", nil); err != nil {
    		log.Fatal(err)
    	}
    }

    You can run it using go run main.go and open http://localhost:8081/ in the browser.

    Here’s how it looks like in Firefox:

    Bare form

    (optional) Making this server available publicly

    We can use ngrok for this purpose, but you can use any compatible solution, such as CloudFlare Tunnel or even reverse SSH tunnel from your own server.

    # run ngrok to our previously exposed port 8081
    ngrok http 8081

    This will give you a public domain, in this case, https://27ca-193-138-7-216.ngrok-free.app. You can verify that it’s working by opening it from your terminal.

    You can use this domain as a property domain below.

    Add captcha widget to the form

    This assumes that you already have an account with Private Captcha. If you don’t, go ahead and create one.

    Create new property

    In the dashboard, click “Add new property”:

    Create new property

    For a domain, enter any valid domain (if you used ngrok in the previous step, add the generated domain):

    Enter domain domain

    After property is created, we will be presented with the integration snippet:

    Integration snippet

    Add captcha widget to the form

    To integrate the widget, we need to add javascript include for privatecaptcha.js and the widget itself to the form. You can get them from the integration snippet above.

    Note

    Make sure to use your own sitekey

    index.html
         </style>
    +    <script defer src="https://cdn.staging.privatecaptcha.com/widget/js/privatecaptcha.js"></script>
     </head>
     <body>
         <div style="display: flex; flex: 1 1 0%">
             <form action='/submit' method="POST">
                 <label> Email: </label>
                 <input type="email" name="email" placeholder="Email address" required />
    +            <div class="private-captcha" data-sitekey="xyz"></div>
                 <button id="formSubmit" type="submit" disabled> Submit </button>
             </form>
         </div>
    

    If you did everything correctly, when you refresh the page (and/or restart your server), you will see the captcha widget inside your form:

    Captcha widget

    Warning

    Captcha has a strict CORS policy and, by default, it will load only on the domain configured during property creation. Subdomains and localhost access needs to be explicitly allowed.

    In order to make captcha widget to load on localhost domain, we need to allow it in the settings of the property you just created (this is not required if you used ngrok domain).

    Allow localhost domain

    However, currently captcha widget is not yet particularly useful as we do not take it into account when submitting the form.

    Integrating with Private Captcha

    Client-side

    In our simple web-page, let’s add a JavaScript function to enable the “Submit” button when captcha is solved.

    index.html
    @@ -18,7 +18,13 @@
                 padding: 20px;
             }
         <script defer src="https://cdn.staging.privatecaptcha.com/widget/js/privatecaptcha.js"></script>
    +    <script type="text/javascript">
    +        function onCaptchaSolved() {
    +            const submitButton = document.querySelector('#formSubmit');
    +            submitButton.disabled = false;
    +        }
    +    </script>
     </head>
     <body>
    
    index.html
    <script type="text/javascript">
        function onCaptchaSolved() {
            const submitButton = document.querySelector('#formSubmit');
            submitButton.disabled = false;
        }
    </script>

    and connect this function to the widget itself by adding data-finished-callback attribute:

    index.html
    @@ -32,7 +32,7 @@
             <form action='/submit' method="POST">
                 <label> Email: </label>
                 <input type="email" name="email" placeholder="Email address" required />
    -            <div class="private-captcha" data-sitekey="xyz"></div>
    +            <div class="private-captcha" data-sitekey="xyz"
    +                data-finished-callback="onCaptchaSolved"></div>
                 <button id="formSubmit" type="submit" disabled> Submit </button>
             </form>
         </div>
    

    Now client-side should be ready. What is left is only to verify captcha on the server-side.

    Server-side

    For server-side, we need to add a handler for the form and verify captcha solution.

    Create a new API key

    To verify captcha solutions, we need an API key. Head to the portal, open your user’s Settings, then API keys and click “Create new key”.

    Create new API key

    Add code to verify solution

    After captcha widget has finished solving the puzzle, it adds a hidden form field with solution (defined by data-solution-field attribute).

    <form>
        <!-- ... -->
        <input name="private-captcha-solution" type="hidden" value="AAAAAAACAhQEAOiDAAAAAAC...IsoSTgYAAA=">
        <!-- ... -->
    </form>

    To verify solution we need to send a POST request with the contents of this field to /siteverify endpoint and check the result. This is done in the server-side handler of the form.

    Note

    Make sure to use your own API key

    main.go
    @@ -1,11 +1,60 @@
     )
     
    +func checkSolution(solution, apiKey string) error {
    +       req, err := http.NewRequest("POST", "https://api.staging.privatecaptcha.com/siteverify", strings.NewReader(solution))
    +       if err != nil {
    +               return err
    +       }
    +
    +       req.Header.Set("X-Api-Key", apiKey)
    +
    +       resp, err := http.DefaultClient.Do(req)
    +       if err != nil {
    +               return err
    +       }
    +       defer resp.Body.Close()
    +
    +       response := struct {
    +               Success bool `json:"success"`
    +               // NOTE: other fields omitted for brevity
    +       }{}
    +
    +       if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
    +           return err
    +       }
    +
    +       if !response.Success {
    +               return errors.New("solution is not correct")
    +       }
    +
    +       return nil
    +}
    +
     func main() {
    +       http.HandleFunc("POST /submit", func(w http.ResponseWriter, r *http.Request) {
    +               const page = `<!DOCTYPE html><html><body style="background-color: %s;"></body></html>`
    +               captchaSolution := r.FormValue("private-captcha-solution")
    +               if err := checkSolution(captchaSolution, "your-api-key"); err != nil {
    +                       fmt.Fprintf(w, page, "red")
    +                       return
    +               }
    +               fmt.Fprintf(w, page, "green")
    +       })
    +
            http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
                    if r.URL.Path == "/" {
                            http.ServeFile(w, r, "index.html")
    
    main.go
    import (
        "encoding/json"
        "errors"
        "fmt"
        "log"
        "net/http"
        "strings"
    )
    
    func checkSolution(solution, apiKey string) error {
    	req, err := http.NewRequest("POST", "https://api.staging.privatecaptcha.com/siteverify", strings.NewReader(solution))
    	if err != nil {
    		return err
    	}
    
    	req.Header.Set("X-Api-Key", apiKey)
    
    	resp, err := http.DefaultClient.Do(req)
    	if err != nil {
    		return err
    	}
    	defer resp.Body.Close()
    
    	response := struct {
    		Success bool `json:"success"`
    		// NOTE: other fields omitted for brevity
    	}{}
    
    	if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
    		return err
    	}
    
    	if !response.Success {
    		return errors.New("solution is not correct")
    	}
    
    	return nil
    }
    
    func main() {
    	http.HandleFunc("POST /submit", func(w http.ResponseWriter, r *http.Request) {
            const page = `<!DOCTYPE html><html><body style="background-color: %s;"></body></html>`
    		captchaSolution := r.FormValue("private-captcha-solution")
    		if err := checkSolution(captchaSolution, "your-api-key"); err != nil {
    			fmt.Fprintf(w, page, "red")
    			return
    		}
    		fmt.Fprintf(w, page, "green")
    	})
        // ....
    }

    Finale

    Test your form

    Now you can finally click “Submit” on your page and see if you get a “green result” in the end.

    Verified

    Now, if you did everything correct, your property dashboard in portal will also show some activity.

    Reports

    And, if you print verify response to the console, you will get this json:

    {"success":true,"challenge_ts":"2025-01-14T11:19:34Z","hostname":"27ca-193-138-7-216.ngrok-free.app"}

    Full code

    Congratulations on completing this tutorial! You can find full code in this gist.

    Troubleshooting

    To access browser logs you can add data-debug="true" attribute to the widget and then see if there are any errors in the console.

    Captcha verification fails (you see a red page)
    • localhost was not allowed in the property settings
    • for deployments, different from privatecaptcha.com, you also need to set data-puzzle-endpoint="https://api.your-domain.com/puzzle" attribute
    Last updated on