MyWritingTwin/API Reference
v1

API Reference

MyWritingTwin Public API v1 documentation

#Authentication

All API requests require a Bearer token in the Authorization header.

Header
Authorization: Bearer mwt_sk_...

To obtain an API key:

  1. Dashboard — Go to Dashboard → API Keys and create a new key.
  2. CLI — Run mwt auth login to authenticate and generate a key.
Keep your API key secret. Do not expose it in client-side code, public repositories, or browser requests. Use environment variables and server-side calls only.

#Base URL

All endpoints are relative to:

https://mywritingtwin.com/api/v1

#Response Format

Every response follows a consistent envelope format. All responses include a meta object with a unique request ID and the API version.

Success response

200 OK
{
  "data": {
    "id": "prof_a1b2c3d4e5f6...",
    "name": "My Profile",
    "status": "completed",
    ...
  },
  "meta": {
    "request_id": "req_abc123def456",
    "api_version": "v1"
  }
}

Paginated response

200 OK
{
  "data": [ ... ],
  "pagination": {
    "has_more": true,
    "next_cursor": "cur_..."
  },
  "meta": {
    "request_id": "req_abc123def456",
    "api_version": "v1"
  }
}

Error response

4xx / 5xx
{
  "error": {
    "type": "validation_error",
    "message": "Name is required.",
    "code": "missing_required_field",
    "details": {
      "field": "name"
    }
  },
  "meta": {
    "request_id": "req_abc123def456",
    "api_version": "v1"
  }
}

#Endpoints

GET/api/v1/account

Returns information about the authenticated account, including current plan tier and usage.

curl
curl https://mywritingtwin.com/api/v1/account \
  -H "Authorization: Bearer mwt_sk_..."

Response

200 OK
{
  "data": {
    "id": "user_...",
    "email": "you@example.com",
    "tier": "pro",
    "usage": {
      "profiles_created": 12,
      "profiles_limit": 50,
      "api_calls_used": 347,
      "api_calls_limit": 1000,
      "api_calls_reset_at": "2026-03-06T00:00:00Z"
    },
    "created_at": "2026-01-15T09:30:00Z"
  },
  "meta": {
    "request_id": "req_abc123def456",
    "api_version": "v1"
  }
}
GET/api/v1/profiles

List all writing profiles for the authenticated user. Results are returned with cursor-based pagination.

Query parameters

ParameterTypeRequiredDescription
limitintegerOptionalNumber of results per page (1-100, default 20).
cursorstringOptionalCursor for the next page. Use the next_cursor value from a previous response.
curl
curl "https://mywritingtwin.com/api/v1/profiles?limit=10" \
  -H "Authorization: Bearer mwt_sk_..."

Response

200 OK
{
  "data": [
    {
      "id": "prof_a1b2c3d4e5f67890...",
      "name": "Professional emails",
      "status": "completed",
      "locale": "en",
      "sample_count": 5,
      "created_at": "2026-02-10T14:22:00Z",
      "updated_at": "2026-02-10T14:25:00Z",
      "completed_at": "2026-02-10T14:25:00Z"
    }
  ],
  "pagination": {
    "has_more": false,
    "next_cursor": null
  },
  "meta": {
    "request_id": "req_abc123def456",
    "api_version": "v1"
  }
}
POST/api/v1/profiles

Create a new writing profile from text samples. The profile is generated asynchronously — poll the profile detail endpoint to check status.

Request body

ParameterTypeRequiredDescription
namestringRequiredProfile name (1-100 characters).
samplesstring[]RequiredArray of writing samples (1-10 items, each 100-50,000 characters).
localestringOptionalLanguage of the samples. One of: en, ja, fr, es. Default: en.
curl
curl -X POST https://mywritingtwin.com/api/v1/profiles \
  -H "Authorization: Bearer mwt_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Blog writing style",
    "samples": [
      "Your first writing sample text here (min 100 chars)...",
      "Your second writing sample text here..."
    ],
    "locale": "en"
  }'

Response

201 Created
{
  "data": {
    "id": "prof_a1b2c3d4e5f67890...",
    "name": "Blog writing style",
    "status": "generating",
    "locale": "en",
    "sample_count": 2,
    "created_at": "2026-03-05T10:00:00Z",
    "updated_at": "2026-03-05T10:00:00Z",
    "completed_at": null
  },
  "meta": {
    "request_id": "req_abc123def456",
    "api_version": "v1"
  }
}
GET/api/v1/profiles/:id

Get details for a specific writing profile. Use this to poll for generation status.

Path parameters

ParameterTypeRequiredDescription
idstringRequiredProfile ID (e.g. prof_a1b2c3d4...).
curl
curl https://mywritingtwin.com/api/v1/profiles/prof_a1b2c3d4e5f67890... \
  -H "Authorization: Bearer mwt_sk_..."

Response

200 OK
{
  "data": {
    "id": "prof_a1b2c3d4e5f67890...",
    "name": "Blog writing style",
    "status": "completed",
    "locale": "en",
    "sample_count": 5,
    "created_at": "2026-02-10T14:22:00Z",
    "updated_at": "2026-02-10T14:25:00Z",
    "completed_at": "2026-02-10T14:25:00Z"
  },
  "meta": {
    "request_id": "req_abc123def456",
    "api_version": "v1"
  }
}
Status values: generating (in progress), completed (ready to export), failed (generation failed).
DELETE/api/v1/profiles/:id

Permanently delete a writing profile. This action cannot be undone.

Path parameters

ParameterTypeRequiredDescription
idstringRequiredProfile ID (e.g. prof_a1b2c3d4...).
curl
curl -X DELETE https://mywritingtwin.com/api/v1/profiles/prof_a1b2c3d4e5f67890... \
  -H "Authorization: Bearer mwt_sk_..."

Response

200 OK
{
  "data": {
    "id": "prof_a1b2c3d4e5f67890...",
    "deleted": true
  },
  "meta": {
    "request_id": "req_abc123def456",
    "api_version": "v1"
  }
}
GET/api/v1/profiles/:id/export

Export a completed writing profile in the specified format. The profile must have status "completed" — exporting a generating or failed profile returns a 409 Conflict error.

Path parameters

ParameterTypeRequiredDescription
idstringRequiredProfile ID (e.g. prof_a1b2c3d4...).

Query parameters

ParameterTypeRequiredDescription
formatstringRequiredExport format. One of: system-prompt, md, json, txt, runtime-block.
modestringOptionalFilter to a single communication mode (e.g. en:email:client). Works with system-prompt and runtime-block formats.
Formats:
  • system-prompt — Ready-to-use system prompt for LLMs (ChatGPT, Claude, etc.). Supports ?mode= filtering.
  • md — Full profile in Markdown
  • json — Writing DNA analysis data (structured JSON)
  • txt — Plain text export
  • runtime-block — RuntimeBlock v2.0 JSON (structured execution format for AI agents and MCP). Supports ?mode= filtering. Returns 409 if not available for older profiles.
curl
curl "https://mywritingtwin.com/api/v1/profiles/prof_a1b2c3d4.../export?format=system-prompt" \
  -H "Authorization: Bearer mwt_sk_..."
curl (runtime-block with mode filter)
curl "https://mywritingtwin.com/api/v1/profiles/prof_a1b2c3d4.../export?format=runtime-block&mode=en:email:client" \
  -H "Authorization: Bearer mwt_sk_..."

Response

200 OK
{
  "data": {
    "id": "prof_a1b2c3d4e5f67890...",
    "format": "system-prompt",
    "content": "You are a writing assistant that mimics the following style...",
    "content_type": "text/plain"
  },
  "meta": {
    "request_id": "req_abc123def456",
    "api_version": "v1"
  }
}
409 Conflict (runtime-block not available for older profiles)
{
  "error": {
    "type": "conflict_error",
    "message": "RuntimeBlock JSON is not available for this profile. It may need to be regenerated.",
    "code": "runtime_block_unavailable"
  },
  "meta": {
    "request_id": "req_abc123def456",
    "api_version": "v1"
  }
}
POST/api/v1/analyze

Analyze writing samples and return style metrics without creating a full profile. Useful for quick analysis or previewing results before profile creation.

Request body

ParameterTypeRequiredDescription
samplesstring[]RequiredArray of writing samples (1-10 items, each 100-50,000 characters).
localestringOptionalLanguage of the samples. One of: en, ja, fr, es. Default: en.
curl
curl -X POST https://mywritingtwin.com/api/v1/analyze \
  -H "Authorization: Bearer mwt_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "samples": [
      "Your writing sample text here (min 100 chars)..."
    ]
  }'

Response

200 OK
{
  "data": {
    "analysis": {
      "tone": {
        "primary": "professional",
        "confidence": 0.87,
        "traits": ["authoritative", "concise", "warm"]
      },
      "formality": {
        "level": "semi-formal",
        "score": 0.65
      },
      "vocabulary": {
        "complexity": "moderate",
        "avg_word_length": 5.2,
        "unique_word_ratio": 0.72
      },
      "sentence_structure": {
        "avg_length": 18.4,
        "variety": "high",
        "dominant_pattern": "compound"
      }
    }
  },
  "meta": {
    "request_id": "req_abc123def456",
    "api_version": "v1"
  }
}

#Errors

The API uses conventional HTTP status codes and returns structured error objects. The type field identifies the error category, and code provides a machine-readable error code for programmatic handling.

TypeStatusDescription
authentication_error401Missing or invalid API key.
authorization_error403The API key does not have permission for this action.
validation_error400The request body or parameters are invalid.
not_found_error404The requested resource does not exist.
rate_limit_error429Too many requests. Retry after the period indicated in the response.
conflict_error409The resource is in a state that conflicts with the request (e.g. exporting a profile that is still generating).
server_error500An unexpected error on the server. If this persists, contact support.
Example: 401 Unauthorized
{
  "error": {
    "type": "authentication_error",
    "message": "Invalid or missing API key.",
    "code": "invalid_api_key"
  },
  "meta": {
    "request_id": "req_abc123def456",
    "api_version": "v1"
  }
}
Example: 429 Rate Limited
{
  "error": {
    "type": "rate_limit_error",
    "message": "Rate limit exceeded. Try again in 3600 seconds.",
    "code": "rate_limit_exceeded",
    "details": {
      "retry_after": 3600,
      "limit": 1000,
      "reset_at": "2026-03-06T00:00:00Z"
    }
  },
  "meta": {
    "request_id": "req_abc123def456",
    "api_version": "v1"
  }
}

#Rate Limits

Each API key has daily and hourly rate limits applied per tier. Daily limits use a 24-hour rolling window; hourly limits provide burst protection. Rate limit status is included in every response via standard headers:

HeaderDescription
RateLimit-LimitMaximum requests allowed in the daily window.
RateLimit-RemainingRequests remaining in the daily window.
RateLimit-ResetUnix timestamp when the daily window resets.
X-RateLimit-Hourly-LimitMaximum requests allowed per hour (burst protection).
X-RateLimit-Hourly-RemainingRequests remaining in the current hourly window.

When the limit is exceeded, the API returns a 429 response with a retry_after value in seconds. Implement exponential backoff for best results.

Specific limits vary by plan tier. Check your current usage and limits via the GET /account endpoint or your dashboard.

#MCP Server

MyWritingTwin exposes an MCP (Model Context Protocol) server, allowing AI assistants like Claude Desktop and Claude Code to access your writing profiles directly.

Prerequisites

Claude Desktop

Add to your Claude Desktop config (claude_desktop_config.json):

claude_desktop_config.json
{
  "mcpServers": {
    "mywritingtwin": {
      "url": "https://www.mywritingtwin.com/api/mcp",
      "headers": {
        "Authorization": "Bearer mwt_sk_..."
      }
    }
  }
}

Claude Code

Terminal
claude mcp add --transport http mywritingtwin https://www.mywritingtwin.com/api/mcp -h "Authorization: Bearer mwt_sk_..."

Claude.ai (web) connectors

Adding the connector inside claude.ai? Use https://www.mywritingtwin.com/api/auth/mcp instead: it signs you in with your MyWritingTwin account (OAuth) so no API key is needed. The plain /api/mcp endpoint never prompts for a login, so claude.ai will not show a Connect step there.

Available Tools

ToolTierDescription
mwt_list_profilesAllList your writing profiles
mwt_get_profileAllRetrieve a profile (supports format and mode filtering)
mwt_analyzeFreeQuick style analysis from writing samples
mwt_create_profilePaidGenerate a full Writing DNA profile (async, 30-90s)
mwt_check_generation_statusAllPoll profile generation progress
mwt_create_checkoutAllMint a Stripe checkout link (starter/pro) — user pays in-browser, no login required
mwt_get_accountAllAccount info, paid status, and usage — poll to confirm a purchase completed

Example workflow

In Claude, you can create and load a profile in one conversation:

Claude conversation
User: Analyze my writing style from these samples: [paste samples]
→ Claude calls mwt_analyze → returns style preview (free, no purchase)

User: Create a full profile from those samples
→ Claude calls mwt_create_profile
   → unpaid? returns a Stripe checkout_url
→ You open the link and pay (Stripe-hosted page — no login needed)
→ Claude calls mwt_get_account → polls until "paid": true
→ Claude calls mwt_create_profile again → returns profile_id + "generating"
→ Claude calls mwt_check_generation_status → "completed!"
→ Claude calls mwt_get_profile → loads your full Writing DNA as context

Paying from inside the chat

No browser login required. mwt_create_checkout (or an unpaid mwt_create_profile) returns a Stripe-hosted checkout_url. Open it, pay, and the purchase is linked to your account automatically. Poll mwt_get_account until paid is true, then create your profile. (Executive is purchased on the pricing page.)

#Webhooks

Receive real-time notifications when profiles are generated, fail, or are deleted. Register a webhook endpoint to get HTTPS POST requests with signed payloads.

Event Types

EventDescription
profile.generation.completedA profile finished generating successfully.
profile.generation.failedProfile generation failed.
profile.deletedA profile was deleted.
test.pingTest event sent via the test endpoint.

Payload Example

profile.generation.completed
{
  "event": "profile.generation.completed",
  "timestamp": "2026-03-30T12:00:00Z",
  "data": {
    "profile_id": "prof_a1b2c3d4e5f67890...",
    "version": 3,
    "mode_count": 7,
    "modes": ["en:email:internal_team", "ja:email:customers_clients"],
    "generated_at": "2026-03-30T12:00:00Z"
  }
}

Signature Verification

Every delivery includes X-MWT-Signature and X-MWT-Timestamp headers. Verify the signature to ensure the payload is authentic:

Node.js
const crypto = require('crypto');

function verifyWebhook(secret, timestamp, body, signature) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(timestamp + '.' + body)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from('sha256=' + expected),
    Buffer.from(signature)
  );
}

// In your handler:
const sig = req.headers['x-mwt-signature'];
const ts = req.headers['x-mwt-timestamp'];
const valid = verifyWebhook(WEBHOOK_SECRET, ts, JSON.stringify(req.body), sig);
Python
import hmac, hashlib

def verify_webhook(secret: str, timestamp: str, body: str, signature: str) -> bool:
    expected = hmac.new(
        secret.encode(), f"{timestamp}.{body}".encode(), hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(f"sha256={expected}", signature)

Retry Behavior

Failed deliveries are retried with exponential backoff: 5s, 30s, 2m, 10m, 30m (5 attempts max). After all attempts fail, the delivery is marked as failed. Check delivery status via GET /api/v1/webhooks/:id.

API Endpoints

MethodEndpointDescription
POST/api/v1/webhooksRegister a webhook endpoint
GET/api/v1/webhooksList your webhook endpoints
DELETE/api/v1/webhooks/:idDelete a webhook endpoint
GET/api/v1/webhooks/:idView delivery log (last 50)
POST/api/v1/webhooks/:idSend a test.ping event
Register a webhook
curl -X POST https://mywritingtwin.com/api/v1/webhooks \
  -H "Authorization: Bearer mwt_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"url": "https://yourapp.com/webhooks/mwt", "event_types": ["profile.generation.completed", "profile.deleted"]}'

Zapier / Make Integration

No custom app needed — use MWT webhooks directly with Zapier or Make:

  1. In Zapier, create a Webhooks by Zapier trigger (Catch Hook). Copy the webhook URL.
  2. Register it via POST /api/v1/webhooks with your desired event types.
  3. Add a second step: Webhooks by Zapier > GET to fetch the updated profile via GET /api/v1/profiles/{profile_id}/export?format=system-prompt
  4. Push the content to downstream tools: Gmail drafts, Slack channels, Notion pages, your CMS, etc.

The same flow works with Make (Integromat) using the HTTP webhook module.

#Versioning

The API version is embedded in the URL path (/api/v1/). You can also send a preferred version via the API-Version header:

Header
API-Version: v1

When a breaking change is planned, the API will include a deprecation warning header in responses:

Deprecation header
X-MWT-Deprecation-Warning: This endpoint version is deprecated. Migrate to /api/v2/ by 2027-01-01.

Older API versions will remain available for at least 12 months after a deprecation notice is issued. Monitor response headers for deprecation warnings and subscribe to our changelog for updates.

Enterprise & Agencies

Need to manage voice profiles for multiple users or integrate MyWritingTwin into your product? We offer custom plans with multi-tenant support, higher rate limits, and dedicated onboarding.

Contact enterprise@mywritingtwin.com