Create Project
Create a new project to organize your Markdown and MDX files. Each project gets its own Google Cloud Storage bucket and maintains independent file statistics.
Endpoint
POST /api/projectsAuthentication
Required: API key
Authorization: Bearer YOUR_API_KEYRequest
Headers
| Header | Value | Required |
|---|---|---|
Authorization | Bearer YOUR_API_KEY | Yes |
Content-Type | application/json | Yes |
Request Body
{
"name": "AI Articles 2025",
"description": "Collection of AI-generated articles for blog"
}Fields
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
name | string | Yes | 1-255 chars, unique | Project name |
description | string | No | Any length | Project description |
Validation Rules:
namemust be unique within your account (case-sensitive)namecannot be empty or whitespace-onlynamemaximum length: 255 charactersdescriptioncan be null, empty, or any length- Both fields support UTF-8 characters (international names allowed)
Response
Success Response (201 Created)
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "AI Articles 2025",
"description": "Collection of AI-generated articles for blog",
"bucket_name": "markdown-api-user123-a1b2c3d4",
"is_default": false,
"is_active": true,
"file_count": 0,
"total_size_bytes": 0,
"created_date": "2025-01-09T10:00:00Z",
"updated_date": "2025-01-09T10:00:00Z"
}Response Fields
| Field | Type | Description |
|---|---|---|
id | UUID | Unique project identifier |
name | string | Project name as provided |
description | string|null | Project description |
bucket_name | string | Auto-generated GCS bucket name |
is_default | boolean | Whether this is the default project (always false for new projects) |
is_active | boolean | Whether project is active (always true for new projects) |
file_count | integer | Number of files (always 0 for new projects) |
total_size_bytes | integer | Total file size in bytes (always 0 for new projects) |
created_date | datetime | When project was created (ISO 8601 UTC) |
updated_date | datetime | When project was last updated (same as created_date initially) |
Error Responses
| Status | Error | Description | Solution |
|---|---|---|---|
| 400 | Bad Request | Invalid JSON or missing required fields | Check request format and required fields |
| 401 | Unauthorized | Missing or invalid authentication | Verify Bearer token or API key is valid |
| 409 | Conflict | Project with this name already exists | Choose a unique name or use existing project |
| 422 | Unprocessable Entity | Validation error (e.g., name too long) | Check field constraints |
| 429 | Too Many Requests | Rate limit exceeded | Wait and retry with exponential backoff |
| 500 | Internal Server Error | Server error | Contact support if error persists |
Example Error Response (409 Conflict):
{
"detail": "Project with name 'AI Articles 2025' already exists"
}Example Error Response (422 Validation):
{
"detail": [
{
"loc": ["body", "name"],
"msg": "field required",
"type": "value_error.missing"
}
]
}Examples
cURL
curl -X POST "https://markdownapi.io/api/projects" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "AI Articles 2025",
"description": "Collection of AI-generated articles for blog"
}'Python (httpx)
import httpx
import os
async def create_project():
"""Create a new project with comprehensive error handling."""
api_key = os.getenv("MARKDOWN_API_KEY")
async with httpx.AsyncClient() as client:
try:
response = await client.post(
"https://markdownapi.io/api/projects",
headers={"Authorization": f"Bearer {api_key}"},
json={
"name": "AI Articles 2025",
"description": "Collection of AI-generated articles for blog"
},
timeout=30.0
)
response.raise_for_status()
project = response.json()
print(f"✓ Project created successfully")
print(f" ID: {project['id']}")
print(f" Name: {project['name']}")
print(f" Bucket: {project['bucket_name']}")
return project
except httpx.HTTPStatusError as e:
if e.response.status_code == 409:
print(f"✗ Project name already exists")
elif e.response.status_code == 422:
print(f"✗ Validation error: {e.response.json()}")
else:
print(f"✗ HTTP error: {e.response.status_code}")
raise
except httpx.TimeoutException:
print(f"✗ Request timed out")
raise
except Exception as e:
print(f"✗ Unexpected error: {e}")
raise
# Usage
# project = await create_project()TypeScript
interface CreateProjectRequest {
name: string;
description?: string;
}
interface ProjectResponse {
id: string;
name: string;
description: string | null;
bucket_name: string;
is_default: boolean;
is_active: boolean;
file_count: number;
total_size_bytes: number;
created_date: string;
updated_date: string;
}
async function createProject(
name: string,
description?: string
): Promise<ProjectResponse> {
const apiKey = process.env.MARKDOWN_API_KEY;
try {
const response = await fetch('https://markdownapi.io/api/projects', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey!}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name,
description: description || null,
}),
});
if (!response.ok) {
if (response.status === 409) {
throw new Error('Project name already exists');
} else if (response.status === 422) {
const error = await response.json();
throw new Error(`Validation error: ${JSON.stringify(error)}`);
}
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const project: ProjectResponse = await response.json();
console.log('✓ Project created successfully');
console.log(` ID: ${project.id}`);
console.log(` Name: ${project.name}`);
console.log(` Bucket: ${project.bucket_name}`);
return project;
} catch (error) {
console.error('✗ Failed to create project:', error);
throw error;
}
}
// Usage
// const project = await createProject('AI Articles 2025', 'Blog articles collection');Common Use Cases
1. Create Project for New Client
async def create_client_project(client_name: str, client_description: str):
"""Create a project for a new client with standardized naming."""
return await create_project(
name=f"Client: {client_name}",
description=f"Content for {client_name}. {client_description}"
)
# Usage
# project = await create_client_project("Acme Corp", "B2B tech content")2. Create Environment-Specific Project
async def create_environment_project(environment: str):
"""Create project for specific environment (dev, staging, prod)."""
return await create_project(
name=f"Articles - {environment.title()}",
description=f"{environment.title()} environment for article generation"
)
# Usage
# dev_project = await create_environment_project("development")
# prod_project = await create_environment_project("production")3. Create Agent Workspace
async def create_agent_project(agent_name: str, agent_purpose: str):
"""Create dedicated project for AI agent."""
return await create_project(
name=f"Agent: {agent_name}",
description=f"Workspace for {agent_name} agent. Purpose: {agent_purpose}"
)
# Usage
# agent_project = await create_agent_project("Writer-Bot", "Blog content generation")4. Bulk Project Creation
import asyncio
async def create_multiple_projects(project_configs: list[dict]):
"""Create multiple projects in parallel."""
async def create_single(config):
try:
return await create_project(**config)
except Exception as e:
print(f"Failed to create {config['name']}: {e}")
return None
projects = await asyncio.gather(*[
create_single(config) for config in project_configs
])
successful = [p for p in projects if p is not None]
print(f"Created {len(successful)}/{len(project_configs)} projects")
return successful
# Usage
# configs = [
# {"name": "Q1 2025 Articles", "description": "Q1 content"},
# {"name": "Q2 2025 Articles", "description": "Q2 content"},
# {"name": "Q3 2025 Articles", "description": "Q3 content"},
# ]
# projects = await create_multiple_projects(configs)Edge Cases and Considerations
Naming Conflicts
Scenario: Project with same name already exists
Response: 409 Conflict
Solution:
async def create_project_with_unique_name(base_name: str, description: str):
"""Create project with unique name by appending number if needed."""
name = base_name
counter = 1
while True:
try:
return await create_project(name=name, description=description)
except httpx.HTTPStatusError as e:
if e.response.status_code == 409:
counter += 1
name = f"{base_name} ({counter})"
else:
raise
# Usage
# project = await create_project_with_unique_name("My Project", "Description")
# # Creates "My Project" or "My Project (2)" or "My Project (3)" etc.Long Project Names
Scenario: Project name exceeds 255 characters
Response: 422 Unprocessable Entity
Solution:
def truncate_name(name: str, max_length: int = 255) -> str:
"""Truncate project name to maximum length."""
if len(name) <= max_length:
return name
return name[:max_length-3] + "..."
# Usage
# safe_name = truncate_name(very_long_name)
# project = await create_project(name=safe_name, description=description)International Characters
Scenario: Project name contains non-ASCII characters
Behavior: Fully supported - UTF-8 names work perfectly
# All of these work
await create_project(name="项目一", description="Chinese project")
await create_project(name="Проект", description="Russian project")
await create_project(name="مشروع", description="Arabic project")
await create_project(name="プロジェクト", description="Japanese project")Empty or Whitespace Names
Scenario: Name is empty or only whitespace
Response: 422 Unprocessable Entity
Prevention:
def validate_project_name(name: str) -> str:
"""Validate and clean project name."""
cleaned = name.strip()
if not cleaned:
raise ValueError("Project name cannot be empty")
return cleaned
# Usage
# safe_name = validate_project_name(user_input)Performance Characteristics
Timing
- Typical duration: 100-300ms
- GCS bucket creation: 50-150ms
- Database insertion: 20-50ms
- Response serialization: 10-20ms
Optimization Tips
- Parallel creation: Create multiple projects concurrently
- Cache patterns: Store project ID after creation for subsequent operations
- Batch operations: Create projects during setup, not during runtime
- Retry logic: Implement exponential backoff for 429/5xx errors
async def create_project_with_retry(
name: str,
description: str,
max_retries: int = 3
):
"""Create project with retry logic for transient failures."""
for attempt in range(max_retries):
try:
return await create_project(name=name, description=description)
except httpx.HTTPStatusError as e:
if e.response.status_code >= 500 or e.response.status_code == 429:
if attempt < max_retries - 1:
wait_time = 2 ** attempt # Exponential backoff: 1s, 2s, 4s
print(f"Retrying in {wait_time}s...")
await asyncio.sleep(wait_time)
else:
raise
else:
raise # Don't retry 4xx errorsBest Practices
1. Descriptive Names
Use clear, descriptive names that indicate purpose:
# Good
"Blog Articles 2025"
"Client ABC - Marketing"
"Agent Memory - Production"
# Avoid
"Project 1"
"Test"
"asdf"2. Include Metadata in Description
Use description for searchability and context:
await create_project(
name="Client ABC",
description="Client: ABC Corp. Type: B2B Tech. Started: 2025-01. Contact: john@abc.com"
)3. Standard Naming Conventions
Establish naming patterns for consistency:
# Environment pattern
f"Articles - {environment}" # "Articles - Production"
# Client pattern
f"Client: {client_name}" # "Client: Acme Corp"
# Agent pattern
f"Agent: {agent_name}" # "Agent: Writer-Bot"
# Date pattern
f"Articles {year} Q{quarter}" # "Articles 2025 Q1"4. Error Handling
Always handle potential errors:
try:
project = await create_project(name=name, description=description)
except httpx.HTTPStatusError as e:
if e.response.status_code == 409:
# Name conflict - handle appropriately
pass
elif e.response.status_code == 422:
# Validation error - check inputs
pass
else:
# Other HTTP error - log and alert
pass5. Store Project IDs
Cache project IDs after creation:
# Store in environment variable or config
os.environ["PROJECT_ID"] = project["id"]
# Or in application state
app.state.project_id = project["id"]
# Or in database
await db.save_project_mapping(client_id, project["id"])Security Considerations
- Authentication required: Every request must include valid credentials
- User isolation: Can only create projects in your own account
- Bucket security: GCS buckets created with restricted access
- No sharing: Projects cannot be shared between users (yet)
- Audit trail: created_date and updated_date tracked automatically
Rate Limiting
Project creation is rate-limited to prevent abuse:
- Standard accounts: 1000 requests/hour (includes all API calls)
- Project creation: Typically 10-50 projects/hour recommended
- Burst allowance: Short bursts up to 100 projects/minute
If rate limited (429), implement exponential backoff:
import asyncio
async def create_with_rate_limit_handling(name: str, description: str):
"""Handle rate limiting gracefully."""
while True:
try:
return await create_project(name=name, description=description)
except httpx.HTTPStatusError as e:
if e.response.status_code == 429:
retry_after = int(e.response.headers.get("Retry-After", "60"))
print(f"Rate limited. Waiting {retry_after}s...")
await asyncio.sleep(retry_after)
else:
raiseSee Also
- List Projects - View all your projects
- Update Project - Modify project details
- Delete Project - Remove projects
- Upload File - Add files to your project
- Projects Concepts - Deep dive into project concepts