Skip to Content

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/projects

Authentication

Required: API key

Authorization: Bearer YOUR_API_KEY

Request

Headers

HeaderValueRequired
AuthorizationBearer YOUR_API_KEYYes
Content-Typeapplication/jsonYes

Request Body

{ "name": "AI Articles 2025", "description": "Collection of AI-generated articles for blog" }

Fields

FieldTypeRequiredConstraintsDescription
namestringYes1-255 chars, uniqueProject name
descriptionstringNoAny lengthProject description

Validation Rules:

  • name must be unique within your account (case-sensitive)
  • name cannot be empty or whitespace-only
  • name maximum length: 255 characters
  • description can 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

FieldTypeDescription
idUUIDUnique project identifier
namestringProject name as provided
descriptionstring|nullProject description
bucket_namestringAuto-generated GCS bucket name
is_defaultbooleanWhether this is the default project (always false for new projects)
is_activebooleanWhether project is active (always true for new projects)
file_countintegerNumber of files (always 0 for new projects)
total_size_bytesintegerTotal file size in bytes (always 0 for new projects)
created_datedatetimeWhen project was created (ISO 8601 UTC)
updated_datedatetimeWhen project was last updated (same as created_date initially)

Error Responses

StatusErrorDescriptionSolution
400Bad RequestInvalid JSON or missing required fieldsCheck request format and required fields
401UnauthorizedMissing or invalid authenticationVerify Bearer token or API key is valid
409ConflictProject with this name already existsChoose a unique name or use existing project
422Unprocessable EntityValidation error (e.g., name too long)Check field constraints
429Too Many RequestsRate limit exceededWait and retry with exponential backoff
500Internal Server ErrorServer errorContact 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

  1. Parallel creation: Create multiple projects concurrently
  2. Cache patterns: Store project ID after creation for subsequent operations
  3. Batch operations: Create projects during setup, not during runtime
  4. 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 errors

Best 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 pass

5. 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: raise

See Also

Last updated on