Skip to main content

Overview

Bots can create challenges to other players, and receive and respond to challenges from other players. Challenges are received through the event stream.

Accept a Challenge

POST /api/challenge//accept

Accept an incoming challenge.

Authentication

Requires OAuth token with challenge:write, bot:play, or board:play scope.

Parameters

challengeId
string
required
The challenge ID received from the event stream
color
string
Accept the challenge and request a specific color: white or black. If not specified, color is random.

Request

curl -X POST https://lichess.org/api/challenge/kZ4Zs7cN/accept \
  -H "Authorization: Bearer <your_token>"

# Request white
curl -X POST "https://lichess.org/api/challenge/kZ4Zs7cN/accept?color=white" \
  -H "Authorization: Bearer <your_token>"

Response

ok
boolean
Success status
{
  "ok": true
}

Rate Limits

Bots can accept challenges but are subject to:
  • Maximum concurrent games (typically 5 for bots)
  • Opponent-specific limits to prevent spam

Decline a Challenge

POST /api/challenge//decline

Decline an incoming challenge.

Authentication

Requires OAuth token with challenge:write, bot:play, or board:play scope.

Parameters

challengeId
string
required
The challenge ID to decline
reason
string
Reason for declining. Options:
  • generic - No specific reason
  • later - Will play later
  • tooFast - Time control too fast
  • tooSlow - Time control too slow
  • timeControl - Don’t like the time control
  • rated - Don’t want to play rated
  • casual - Don’t want to play casual
  • standard - Only play standard chess
  • variant - Don’t play this variant
  • noBot - Don’t play with bots
  • onlyBot - Only play with bots

Request

curl -X POST https://lichess.org/api/challenge/kZ4Zs7cN/decline \
  -H "Authorization: Bearer <your_token>" \
  -H "Content-Type: application/json" \
  -d '{"reason":"later"}'

Response

ok
boolean
Success status
{
  "ok": true
}

Cancel a Challenge

POST /api/challenge//cancel

Cancel a challenge that you created.

Authentication

Requires OAuth token with challenge:write, bot:play, or board:play scope.

Parameters

challengeId
string
required
The challenge ID to cancel
opponentToken
string
OAuth token of the opponent (only needed for special cases)

Request

curl -X POST https://lichess.org/api/challenge/kZ4Zs7cN/cancel \
  -H "Authorization: Bearer <your_token>"

Response

{
  "ok": true
}

Create a Challenge

POST /api/challenge/

Challenge another player to a game.

Authentication

Requires OAuth token with challenge:write, bot:play, or board:play scope.

Parameters

username
string
required
Username of the player to challenge
rated
boolean
default:"false"
Whether the game should be rated
clock.limit
integer
Clock initial time in seconds (for real-time games)
clock.increment
integer
Clock increment in seconds (for real-time games)
days
integer
Days per turn (for correspondence games, 1-15)
color
string
default:"random"
Your color: white, black, or random
variant
string
default:"standard"
Chess variant:
  • standard
  • chess960
  • crazyhouse
  • antichess
  • atomic
  • horde
  • kingOfTheHill
  • racingKings
  • threeCheck
fen
string
Custom initial position in FEN notation
keepAliveStream
boolean
default:"false"
Keep the stream open to receive challenge status updates
rules
string[]
Game rules/modifiers:
  • noAbort - Players cannot abort the game
  • noRematch - Players cannot offer a rematch
  • noGiveTime - Players cannot give extra time
  • noClaimWin - Players cannot claim victory when opponent is disconnected
  • noEarlyDraw - Players cannot offer draw before ply 40
acceptByToken
string
OAuth token of the opponent for instant acceptance

Request

# Create a 5+3 blitz challenge
curl -X POST https://lichess.org/api/challenge/opponent_username \
  -H "Authorization: Bearer <your_token>" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d 'rated=true&clock.limit=300&clock.increment=3&color=white'

# Create correspondence challenge
curl -X POST https://lichess.org/api/challenge/opponent_username \
  -H "Authorization: Bearer <your_token>" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d 'rated=true&days=3&color=random'

# Chess960 challenge
curl -X POST https://lichess.org/api/challenge/opponent_username \
  -H "Authorization: Bearer <your_token>" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d 'rated=false&clock.limit=600&clock.increment=0&variant=chess960'

Response

challenge
object
The created challenge
{
  "challenge": {
    "id": "kZ4Zs7cN",
    "url": "https://lichess.org/kZ4Zs7cN",
    "status": "created",
    "challenger": {
      "id": "mybot",
      "name": "MyBot",
      "title": "BOT",
      "rating": 1600,
      "provisional": false,
      "online": true
    },
    "destUser": {
      "id": "opponent",
      "name": "Opponent",
      "rating": 1550,
      "provisional": false,
      "online": true
    },
    "variant": {
      "key": "standard",
      "name": "Standard",
      "short": "Std"
    },
    "rated": true,
    "timeControl": {
      "type": "clock",
      "limit": 180,
      "increment": 2,
      "show": "3+2"
    },
    "color": "white",
    "perf": {
      "name": "Blitz"
    },
    "direction": "out"
  }
}

Open Challenge

POST /api/challenge/open

Create an open challenge that anyone can accept. Generates unique URLs for white and black.
Open challenges are useful for creating challenge links that can be shared publicly or embedded on websites.

Authentication

Requires OAuth token with challenge:write scope, or anonymous access for verified users.

Parameters

Same parameters as Create a Challenge, except no username is needed.
rated
boolean
default:"false"
Whether the game should be rated
clock.limit
integer
Clock initial time in seconds
clock.increment
integer
Clock increment in seconds
days
integer
Days per turn (for correspondence, 1-15)
variant
string
default:"standard"
Chess variant
fen
string
Custom initial position in FEN
name
string
Optional challenge name/description
rules
string[]
Game rules (same as regular challenges)

Request

curl -X POST https://lichess.org/api/challenge/open \
  -H "Authorization: Bearer <your_token>" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d 'rated=false&clock.limit=600&clock.increment=5&name=Practice Game'

Response

challenge
object
The created challenge
urlWhite
string
URL for accepting as white
urlBlack
string
URL for accepting as black
{
  "challenge": {
    "id": "Xm39fB2J",
    "url": "https://lichess.org/Xm39fB2J",
    "status": "created",
    "variant": {
      "key": "standard",
      "name": "Standard"
    },
    "rated": false,
    "timeControl": {
      "type": "clock",
      "limit": 600,
      "increment": 5,
      "show": "10+5"
    },
    "perf": {
      "name": "Rapid"
    },
    "direction": "out",
    "open": true
  },
  "urlWhite": "https://lichess.org/Xm39fB2J?color=white",
  "urlBlack": "https://lichess.org/Xm39fB2J?color=black"
}

Get Challenge Info

GET /api/challenge//show

Get information about a challenge.

Authentication

Requires OAuth token with challenge:read scope.

Request

curl https://lichess.org/api/challenge/kZ4Zs7cN/show \
  -H "Authorization: Bearer <your_token>"

List Challenges

GET /api/challenge

List all challenges created by or targeted at the authenticated user.

Authentication

Requires OAuth token.

Request

curl https://lichess.org/api/challenge \
  -H "Authorization: Bearer <your_token>"

Response

in
array
Challenges received
out
array
Challenges sent
{
  "in": [
    {
      "id": "kZ4Zs7cN",
      "url": "https://lichess.org/kZ4Zs7cN",
      "status": "created",
      "challenger": {
        "id": "player1",
        "name": "Player1",
        "rating": 1500
      },
      "variant": {
        "key": "standard",
        "name": "Standard"
      },
      "rated": true,
      "timeControl": {
        "type": "clock",
        "limit": 180,
        "increment": 2,
        "show": "3+2"
      }
    }
  ],
  "out": []
}

Challenge Handling Best Practices

Auto-Accept Logic

import requests
import json

class ChallengeFilter:
    def __init__(self, token):
        self.token = token
        self.headers = {"Authorization": f"Bearer {token}"}
        self.base_url = "https://lichess.org"
    
    def should_accept(self, challenge):
        """Determine if we should accept this challenge"""
        
        # Only standard chess
        if challenge['variant']['key'] != 'standard':
            return False, 'variant'
        
        # Only rated games
        if not challenge['rated']:
            return False, 'casual'
        
        # Check time control
        tc = challenge.get('timeControl', {})
        if tc.get('type') == 'clock':
            limit = tc.get('limit', 0)
            increment = tc.get('increment', 0)
            
            # Only blitz (3-5 minutes)
            if not (180 <= limit <= 300):
                return False, 'timeControl'
        
        # Check opponent rating
        challenger = challenge.get('challenger', {})
        rating = challenger.get('rating', 0)
        
        # Only play within rating range
        if abs(rating - 1600) > 200:  # Our rating ±200
            return False, 'generic'
        
        return True, None
    
    def handle_challenge(self, challenge):
        """Accept or decline a challenge based on criteria"""
        challenge_id = challenge['id']
        should_accept, reason = self.should_accept(challenge)
        
        if should_accept:
            url = f"{self.base_url}/api/challenge/{challenge_id}/accept"
            response = requests.post(url, headers=self.headers)
            
            if response.status_code == 200:
                print(f"Accepted challenge from {challenge['challenger']['name']}")
            else:
                print(f"Failed to accept: {response.json()}")
        else:
            url = f"{self.base_url}/api/challenge/{challenge_id}/decline"
            data = {"reason": reason}
            response = requests.post(
                url, 
                headers={**self.headers, "Content-Type": "application/json"},
                json=data
            )
            
            print(f"Declined challenge (reason: {reason})")

# Usage in event stream
filter = ChallengeFilter("lip_yourtoken")

# When receiving a challenge event:
# filter.handle_challenge(event['challenge'])

Rate Limiting

Be aware of rate limits:
  • Bot concurrent games: Typically 5 active games maximum
  • Challenge creation: Limited to prevent spam
  • Challenge to specific user: Higher rate limit for friends, lower for strangers

Error Handling

Common errors when creating challenges:
  • "No such user" - Username doesn’t exist
  • "User does not follow you" - Required for non-friends in some cases
  • "Cannot challenge yourself" - Self-explanatory
  • "This user does not accept challenges" - User has disabled challenges
  • "Too many requests" - Rate limited

See Also

Build docs developers (and LLMs) love