API Documentation
CTOJobsHQ Draft Posting API — v1.1.0
Overview
The CTOJobsHQ API lets an LLM agent (or any HTTP client) post a job draft without a user account. The two-step flow is:
- Call
POST /api/v1/access_tokensto receive a bearer token. - Call
POST /api/v1/jobs/draftwith that token to create a draft and receive a Stripe payment link. - Send the poster to the
payment_urlto complete checkout. The Stripe session is pre-filled with the email fromposter.email— make sure it is the correct address, as the payment receipt and any follow-up are sent there. - After successful payment the job post enters review. Once approved it will be published on CTOJobsHQ.
Base URL: https://ctojobshq.com
Authentication
Pass the bearer token in the Authorization header on every
authenticated request:
Authorization: Bearer ctojobs_<your_token>
Tokens are shown once at creation time and are not retrievable afterwards.
Calling POST /api/v1/access_tokens again for the same
owner email + client name pair revokes the previous token and issues a new one.
Rate limits
When a limit is hit the API returns 429 Too Many Requests
with a Retry-After header indicating seconds until the window resets.
Step 1 — Issue an access token
POST /api/v1/access_tokens
Request
curl -s -X POST https://ctojobshq.com/api/v1/access_tokens \
-H "Content-Type: application/json" \
-d '{
"owner": {
"email": "[email protected]",
"name": "Jane Smith"
},
"client": {
"name": "Claude Job Poster"
}
}'
Successful response — 201 Created
{
"success": true,
"access_token": "ctojobs_0123456789abcdef0123456789abcdef0123456789abcdef",
"token_type": "Bearer",
"api_key": {
"name": "Claude Job Poster",
"key_prefix": "ctojobs_01234567",
"created_by_email": "[email protected]",
"created_at": "2026-03-13T10:15:00Z"
},
"rate_limit": {
"draft_requests_per_hour": 100,
"token_requests_per_hour_per_email": 5,
"token_requests_per_hour_per_ip": 20
},
"warnings": [
"Store this token securely. It is only returned once.",
"Creating a new token for the same owner email and client name revokes the previous token."
]
}
Save the token to a shell variable
TOKEN=$(curl -s -X POST https://ctojobshq.com/api/v1/access_tokens \
-H "Content-Type: application/json" \
-d '{"owner":{"email":"[email protected]","name":"Jane Smith"},"client":{"name":"Claude Job Poster"}}' \
| jq -r '.access_token')
echo "Token: $TOKEN"
Step 2 — Create a job draft
POST /api/v1/jobs/draft — requires bearer token
Minimum viable request
curl -s -X POST https://ctojobshq.com/api/v1/jobs/draft \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"job": {
"title": "Chief Technology Officer",
"company": "Acme Corp",
"location_type": "remote",
"job_type": "full_time",
"description": "Lead engineering and product.",
"apply_email": "[email protected]"
},
"poster": {
"email": "[email protected]",
"name": "Jane Smith"
}
}'
Full request (all fields)
curl -s -X POST https://ctojobshq.com/api/v1/jobs/draft \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"job": {
"title": "Chief Technology Officer",
"company": "Acme Corp",
"company_website": "https://acme.com",
"company_logo_url": "https://acme.com/logo.png",
"location_type": "remote",
"office_location": "Remote",
"location_restricted": ["United States", "Canada"],
"job_type": "full_time",
"description": "Lead engineering and product at a fast-growing SaaS company.",
"salary_min": 200000,
"salary_max": 300000,
"salary_currency": "USD",
"salary_period": "Yearly",
"apply_email": "[email protected]",
"apply_website": "https://acme.com/careers/cto",
"tech_stack": ["Ruby", "React", "AWS"],
"is_recruitment_agency": false
},
"poster": {
"email": "[email protected]",
"name": "Jane Smith"
},
"options": {
"duration_months": 3
}
}'
Field reference
| Field | Req? | Details & example |
|---|---|---|
| title | Yes |
Job title shown on the listing. e.g. Chief Technology Officer, VP of Engineering, Fractional CTO
|
| company | Yes |
Company name as it should appear on the listing. e.g. Acme Corp
|
| company_logo_url | No |
Publicly accessible URL to the company logo. Accepted formats: PNG, JPG, SVG. Recommended size: 200×200 px or larger, square aspect ratio. e.g. https://acme.com/logo.pngNot yet processed by the API — field is accepted but not persisted until implemented in a future update. |
| description | Yes |
The full job description. Accepts HTML or plain text.
HTML is strongly recommended — when the draft is published the description is stored and rendered as rich text (ActionText),
so HTML headings, paragraphs, and lists are displayed as intended.
Plain text is also accepted and will be auto-wrapped in Recommended tags: Show HTML example (recommended)"description": "<h2>About the role</h2>\n<p>We are looking for a hands-on CTO to own our technical strategy and lead a 12-person engineering team across product, platform, and data.</p>\n\n<h2>What you will do</h2>\n<ul>\n <li>Define and own the technical roadmap in close collaboration with the CEO</li>\n <li>Hire and mentor senior engineers and engineering managers</li>\n <li>Drive the migration from a Rails monolith to a service-oriented architecture</li>\n <li>Own reliability, security, and compliance (SOC 2)</li>\n</ul>\n\n<h2>Requirements</h2>\n<ul>\n <li>10+ years of software engineering experience, at least 3 in a leadership role</li>\n <li>Prior experience scaling B2B SaaS from seed to Series B</li>\n <li>Strong Ruby or Python background; familiarity with React</li>\n</ul>\n\n<h2>Compensation</h2>\n<p><strong>$200,000–$300,000</strong> base salary + equity (0.5–1.5%).</p>" Show plain text example (no formatting)"description": "About the role\n\nWe are looking for a hands-on CTO to own our technical strategy and lead a 12-person engineering team.\n\nWhat you will do\n- Define and own the technical roadmap\n- Hire and mentor senior engineers\n- Drive the migration from a monolith to a service-oriented architecture\n\nRequirements\n- 10+ years of software engineering experience\n- Prior experience scaling B2B SaaS products\n\nCompensation\n$200,000 - $300,000 base + equity." |
| location_type | Yes |
Work arrangement. One of:
remote,
hybrid,
onsite
|
| job_type | Yes |
Employment type. One of:
full_time,
part_time,
contract,
fractional,
freelance
|
| apply_email | * |
Email address candidates use to apply. Required when apply_website is omitted.
At least one of apply_email or apply_website must be present.e.g. [email protected]
|
| apply_website | * |
URL of the application form or careers page. Required when apply_email is omitted.e.g. https://acme.com/careers/cto
|
| company_website | No |
Public website of the hiring company. Must be a valid http or https URL.e.g. https://acme.com
|
| office_location | No |
Human-readable city/region shown on the listing. Most relevant for hybrid and onsite roles. e.g. New York, NY, London, Remote – US/EU
|
| location_restricted | No |
Countries or regions where applicants must be based. Use full country names. Array of strings. e.g. ["United States", "Canada"], ["Germany", "Netherlands", "France"]
|
| salary_min salary_max |
No |
Salary range as whole numbers in salary_currency.
Both fields must be provided together; salary_max must be ≥ salary_min.e.g. "salary_min": 180000, "salary_max": 250000
|
| salary_currency | No |
ISO 4217 currency code. Supported values: USD, EUR, GBP, AUD, CHF, JPY
|
| salary_period | No |
Pay frequency. One of: Yearly, Monthly, Daily, Hourly
|
| tech_stack | No |
Technologies used at the company. Array of strings; displayed as tags on the listing. e.g. ["Ruby on Rails", "React", "PostgreSQL", "AWS"]
|
| is_recruitment_agency | No |
Set to true if the poster is a recruitment or staffing agency rather than the direct employer. Default: false
|
| Yes |
Email of the person or system posting the job. Used to link the draft to the poster and for follow-up communications. e.g. [email protected]
|
|
| name | No |
Full name of the poster. e.g. Jane Smith
|
| duration_months | No |
How long the listing stays active after publication. Determines the price charged at checkout.
Accepted values:
Default: |
Successful response — 200 OK
{
"success": true,
"draft": {
"id": "draft_42",
"preview_url": "https://ctojobshq.com/drafts/42/preview?token=abc123def456",
"edit_url": "https://ctojobshq.com/drafts/42/edit?token=abc123def456",
"expires_at": "2026-03-21T10:15:00Z"
},
"payment": {
"required": true,
"amount": 39900,
"currency": "USD",
"payment_url": "https://checkout.stripe.com/pay/cs_live_...",
"checkout_session_id": "cs_live_..."
},
"pricing": {
"base_price": 399.0,
"total": 399.0
}
}
Error responses
All errors follow a consistent shape:
{
"success": false,
"error": {
"code": "validation_error",
"message": "Validation failed",
"details": {
"job.title": ["can't be blank"],
"job.location_type": ["is not included in the list"]
}
}
}
| HTTP status | Error code | Meaning |
|---|---|---|
| 401 | auth_error | Missing or invalid bearer token |
| 422 | validation_error | Invalid or missing request fields |
| 429 | rate_limit | Too many requests; check Retry-After |
| 502 | payment_error | Stripe checkout session could not be created |
Full shell script example
Copy-paste this into your terminal (requires curl and
jq). Replace BASE_URL
with http://localhost:3000 for local testing.
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="https://ctojobshq.com"
# --- Step 1: get a token ---
echo "==> Issuing access token..."
TOKEN_RESPONSE=$(curl -s -X POST "$BASE_URL/api/v1/access_tokens" \
-H "Content-Type: application/json" \
-d '{
"owner": { "email": "[email protected]", "name": "Jane Smith" },
"client": { "name": "Claude Job Poster" }
}')
echo "$TOKEN_RESPONSE" | jq .
TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token')
echo ""
echo "Token prefix: $(echo "$TOKEN" | cut -c1-18)..."
# --- Step 2: create a job draft ---
echo ""
echo "==> Creating job draft..."
DRAFT_RESPONSE=$(curl -s -X POST "$BASE_URL/api/v1/jobs/draft" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"job": {
"title": "Chief Technology Officer",
"company": "Acme Corp",
"company_website": "https://acme.com",
"location_type": "remote",
"job_type": "full_time",
"description": "Lead our engineering team and define the technical strategy.",
"salary_min": 200000,
"salary_max": 300000,
"salary_currency": "USD",
"salary_period": "Yearly",
"apply_email": "[email protected]",
"tech_stack": ["Ruby", "React", "AWS"]
},
"poster": {
"email": "[email protected]",
"name": "Jane Smith"
},
"options": {
"duration_months": 3
}
}')
echo "$DRAFT_RESPONSE" | jq .
PREVIEW_URL=$(echo "$DRAFT_RESPONSE" | jq -r '.draft.preview_url')
PAYMENT_URL=$(echo "$DRAFT_RESPONSE" | jq -r '.payment.payment_url')
echo ""
echo "Preview URL : $PREVIEW_URL"
echo "Payment URL : $PAYMENT_URL"
Next steps
1. Send the poster to the payment URL
The draft response includes a payment.payment_url — a Stripe Checkout link.
Direct the poster there to complete payment. The checkout is pre-filled with the email address from
poster.email in the request, so make sure that field contains
the correct address. The payment receipt and all follow-up communications are sent to that address.
2. We act on payment, not on draft creation
Creating a draft does not publish anything. The job post enters our queue only after a successful Stripe payment.
Until then the draft exists at its preview_url and
edit_url but is not visible to the public.
Drafts expire after 7 days if no payment is made.
3. Every post is reviewed before going live
After payment we review the listing to make sure it meets our quality standards and is relevant to the CTO/senior technology leadership audience. Most listings are reviewed and published within one business day. If we have questions or need changes we will reach out to the poster email.
4. Editing after submission
The poster can update the draft at any time before payment — and after publication — using the
edit_url returned in the draft response.
No account is required; the token in the URL grants access.
/docs/openapi/job-drafts-api.yaml.