Skip to content

Offline Validation

Offline Validation refers to confirming a solution is valid to minimize calls to the siteverify API with solutions that are malformed, expired or otherwise invalid. It does not replace the siteverify API because a valid solution makes no humanity assertion.

Offline Validation is optional but recommended to prevent getting ratelimited.

For debugging, we recommend using this webpage: https://dinochiesa.github.io/jwt/

Solution format

After a user solves the challenge, a JWT is issued (referred to as solution). The JWT payload contains the following claims:

{
"iss": "global.captcha.party",
"aud": "<YOUR SITEKEY>",
"exp": 1704063900,
"iat": 1704063620,
"nbf": 1704063600,
"jti": "<UUID>",
"#ip": "<HASHED IP ADDRESS>",
"#ua": "<HASHED USER AGENT>",
"#url": "<URL>",
"#data": "<DATA>",
"#action": "<ACTION>",
"##": "<ENCRYPTED DATA>"
}

Custom claims are prefixed with #, see Custom claims. The ## claim is encrypted, see Encrypted claims.

Validate a solution

To validate a solution, you must obtain the correct public key from our API.

Extract the kid from the JWT header and find the corresponding key in the JWK set. Then, use the key to validate the JWT signature.

All of the following must be true for a solution to be valid:

  1. The JWT signature is valid
  2. The JWT signature algorithm is RS256 (also found in the JWT header)
  3. The JWT iss claim is global.captcha.party (or the region you are using)
  4. The JWT aud claim is your sitekey
  5. The JWT exp claim is in the future
  6. The JWT nbf claim is in the past

Additionally you may choose to validate any or all of the custom claims:

  1. The JWT #url claim matches the URL of the page where the challenge was solved
  2. The JWT #data claim matches the data value you are expecting (requires Professional plan)
  3. The JWT #action claim matches the action value you are expecting (requires Business plan)
  4. The JWT #ip claim matches the hashed IP of the user
  5. The JWT #ua claim matches the hashed User-Agent of the user

Obtaining the public key

The public key is a JSON Web Key (JWK) and can be obtained from the following endpoint: https://captcha.party/.well-known/jwks.json

The JWK set contains a list of keys. Each key has a kid property. The kid property is a unique identifier for the key. You can find the kid of the key you need in the JWT header.

We regularly rotate the keys in the JWK set. However, we will not use new keys for at least 4 weeks, so you can cache the JWK set for up to one week.

Preventing replay attacks

The jti claim is a unique identifier for the solution. It is a UUID and is generated by the server. It is used to prevent replay attacks. If you receive a solution with a jti that you have already seen, you should reject it. You only need to store jti values for a short period of time (until the exp claim expires).

Difference between iat and nbf

The iat claim is the time when the challenge was solved, and the nbf claim is the time when the challenge started. You can use the difference between these two values to determine how long it took the user to solve the challenge.

You can also use the nbf claim to prevent CAPTCHAs from being loaded before a certain time. For example, if you have a sale that starts at 12:00 and requires a CAPTCHA, you shouldn’t be allowing solutions from 11:59, as a human couldn’t have accessed the CAPTCHA yet.

Key rotation

We issue a new key at the start of every month and add it to the JWK set. We will not use new keys for at least 4 weeks, so you can cache the JWK set for up to one week.

Keys are in one of four states:

  • issued - The key has been issued, is added to the JWK set but is not yet in use.
  • active - The key is currently in use for signing solutions.
  • inactive - The key is no longer in use for signing solutions, but is still in the JWK set.
  • revoked - The key is no longer in use for signing solutions and has been removed from the JWK set.

Each key will advance to the next state at the start of every month. For example, a key that is issued in January will become active in February, inactive in March and revoked in April.

Custom claims

Custom claims are prefixed with # to prevent collisions with standard claims. The following custom claims are available:

ClaimDescription
#remoteipThe IP address of the user (hashed)
#useragentThe user agent of the user (hashed)
#urlThe URL of the page where the challenge was solved
#dataThe data value of the challenge (requires Professional plan)
#actionThe action value of the challenge (requires Business plan)

Hashed claims

Some claims are hashed to protect the privacy of the user. The hash is a HMAC hash (using SHA-256) with only the first 2 bytes used (to prevent bruteforce attacks at the expense of opening up collision attacks).

To validate a hashed claim, HMAC the value of the claim with your secret key and compare the first 2 bytes of the result with the value of the claim.

For example, to validate the IP address, concatenate the IP address with your secret key and compare the SHA-256 hash of the result with the value of the #remoteip claim.

Encrypted claims

There are additional claims that are encrypted to protect the integrity of our service.

Enterprise customers can decrypt these claims using the decryption key provided to them using Offline Verification.