The unofficial Python client library to easily interface with Strapi from your Python project.
pip install strapi-pyfrom strapi_client import strapi
# Create a client instance
client = strapi(base_url="http://localhost:1337/api")
# Fetch data from a custom endpoint
response = client.fetch('/articles')
data = response.json()
print(data)from strapi_client import strapi
# Initialize the client with yor API token
client = strapi(
base_url="http://localhost:1337/api",
auth="your-api-token-here"
)
# All requests will include the Authorization header
response = client.fetch('/articles')client = strapi(
base_url="http://localhost:1337/api",
headers={
"X-Custom-Header": "value",
"Accept-Language": "en"
}
)Collection types represent multiple entries (e.g., articles, products, users).
# Get all articles
articles = client.collection('articles')
response = articles.find()
print(response.json())articles = client.collection('articles')
# Filter and sort
response = articles.find(params={
'filters': {
'title': {
'$contains': 'Python'
},
'publishedAt': {
'$notNull': True
}
},
'sort': ['createdAt:desc'],
'pagination': {
'page': 1,
'pageSize': 10
},
'populate': '*'
})articles = client.collection('articles')
# Get article with documentId j964065dnjrdr4u89weh79xl
response = articles.find_one("j964065dnjrdr4u89weh79xl", params={
'populate': ['author', 'comments']
})
print(response.json())Strapi 5 replaces the numeric
idused in Strapi 4 with a new 24-character alphanumeric identifier calleddocumentId. When working with Strapi 5, usedocumentIdas the primary resource identifier. This library continues to support the legacyidfield for Strapi 4 projects.
articles = client.collection('articles')
response = articles.create(data={
'title': 'My New Article',
'content': 'This is the article content',
'publishedAt': '2024-01-01T00:00:00.000Z'
})
created_article = response.json()
print(f"Created article with ID: {created_article['data']['documentId']}")articles = client.collection('articles')
response = articles.update(1, data={
'title': 'Updated Article Title',
'content': 'Updated content'
})articles = client.collection('articles')
response = articles.delete(1)Single types represent a single entry (e.g., homepage, about page, settings).
homepage = client.single('homepage')
response = homepage.find(params={'populate': '*'})
print(response.json())homepage = client.single('homepage')
response = homepage.update(data={
'title': 'Welcome to My Site',
'description': 'A brief description'
})homepage = client.single('homepage')
response = homepage.delete()# Access users endpoint
users = client.collection(
resource='users',
plugin={'name': 'users-permissions', 'prefix': ''}
)
# Create a new user
response = users.create(data={
'username': 'johndoe',
'email': 'john@example.com',
'password': 'SecurePassword123!'
})# Access a custom plugin endpoint
blog_posts = client.collection(
resource='posts',
plugin={'name': 'blog', 'prefix': 'blog'}
)
# This will make requests to /blog/posts
response = blog_posts.find()# Disable plugin prefix
custom_content = client.collection(
resource='items',
plugin={'name': 'custom-plugin', 'prefix': ''}
)
# This will make requests to /items# Upload from bytes
with open('image.jpg', 'rb') as f:
file_data = f.read()
response = client.files.upload(
file_data=file_data,
filename='image.jpg',
mimetype='image/jpeg',
file_info={
'alternativeText': 'A sample image',
'caption': 'My caption'
}
)
print(response.json())import io
# Upload from file-like object
with open('document.pdf', 'rb') as f:
response = client.files.upload(
file_data=f,
filename='document.pdf',
mimetype='application/pdf'
)# Get all files
response = client.files.find()
files = response.json()
# Filter files
response = client.files.find(params={
'filters': {
'mime': {'$contains': 'image'}
},
'sort': 'createdAt:desc'
})response = client.files.find_one(file_id="clkgylmcc000008lcdd868feh")
file_data = response.json()
print(f"File URL: {file_data['url']}")response = client.files.update(
file_id="clkgylmcc000008lcdd868feh",
file_info={
'name': 'renamed-file.jpg',
'alternativeText': 'Updated alt text',
'caption': 'Updated caption'
}
)response = client.files.delete(file_id="clkgylmcc000008lcdd868feh")You can specify custom API paths for content types:
# Use a custom path instead of the default /articles
custom_articles = client.collection(
resource='articles',
path='/v2/custom-articles'
)
response = custom_articles.find()
# Makes request to /v2/custom-articlesThe library provides detailed error messages from Strapi, making debugging much easier. When an error occurs, you'll see:
- Error type/name (e.g., ValidationError, ApplicationError)
- Clear error message from Strapi
- Detailed field-level validation errors with paths
- Access to the original response for debugging
from strapi_client import (
strapi,
StrapiHTTPError,
StrapiHTTPNotFoundError,
StrapiHTTPUnauthorizedError,
StrapiHTTPBadRequestError,
StrapiValidationError
)
try:
client = strapi(base_url="http://localhost:1337/api")
articles = client.collection('articles')
response = articles.find_one(999)
except StrapiHTTPNotFoundError as e:
print(f"Article not found: {e}")
print(f"Status code: {e.response.status_code}")
except StrapiHTTPUnauthorizedError as e:
print(f"Authentication failed: {e}")
except StrapiHTTPBadRequestError as e:
print(f"Bad request: {e}")
print(f"Full error: {e.response.json()}")
except StrapiHTTPError as e:
print(f"HTTP error occurred: {e}")
print(f"Response: {e.response.text}")
except StrapiValidationError as e:
print(f"Validation error: {e}")When you have validation errors (e.g., in dynamic zones or complex fields), the error message will show exactly which fields are problematic:
try:
blog = client.collection('blogs')
response = blog.create(data={
'title': '', # Invalid: too short
'blocks': [
{
# Missing __component field
'body': 'Some content'
}
]
})
except StrapiHTTPBadRequestError as e:
print(e)
# Output:
# Strapi API Error (400): [ValidationError] Invalid data provided
# Validation errors:
# - title: title must be at least 1 characters
# - blocks.0.__component: component is requiredAll HTTP errors preserve the original response, allowing you to access additional details:
try:
response = client.collection('articles').create(data={...})
except StrapiHTTPBadRequestError as e:
# Get status code
print(f"Status: {e.response.status_code}")
# Get full error details
error_details = e.response.json()
print(f"Error name: {error_details['error']['name']}")
print(f"Error details: {error_details['error']['details']}")
# Get request Url
print(f"Request URL: {e.response.request.url}")StrapiError- Base error classStrapiValidationError- Invalid input or configurationStrapiHTTPError- Base HTTP error (non-2xx responses)StrapiHTTPBadRequestError- 400 Bad Request (validation errors, malformed requests)StrapiHTTPUnauthorizedError- 401 Unauthorized (authentication required)StrapiHTTPForbiddenError- 403 Forbidden (insufficient permissions)StrapiHTTPNotFoundError- 404 Not Found (resource doesn't exist)StrapiHTTPTimeoutError- 408 Request TimeoutStrapiHTTPInternalServerError- 500 Internal Server Error
articles = client.collection('articles')
# Fetch French content
response = articles.find(params={'locale': 'fr'})
# Fetch specific entry in Spanish
response = articles.find_one(1, params={'locale': 'es'})articles = client.collection('articles')
# Populate all relations
response = articles.find(params={'populate': '*'})
# Populate specific relations
response = articles.find(params={
'populate': ['author', 'categories', 'cover']
})
# Deep population
response = articles.find(params={
'populate': {
'author': {
'populate': ['avatar']
},
'categories': '*'
}
})articles = client.collection('articles')
# Select specific fields only
response = articles.find(params={
'fields': ['title', 'description', 'publishedAt']
})- Python >= 3.10
- httpx >= 0.27.0
Contributions are welcome! Please feel free to submit a Pull Request.
MIT License - see LICENSE file for details.