curl --request POST \
--url https://app.surferseo.com/api/v2/workspaces/{workspace_id}/content_editors \
--header 'API-KEY: <api-key>' \
--header 'Content-Type: application/json' \
--data '
{
"main_keyword": "seo tools",
"custom_instructions": "Mention product X, write from perspective of Y",
"custom_template_id": 2,
"custom_voice_id": 123,
"device": "mobile",
"import_content_url": "https://example.com/article",
"location": "United States",
"notes": "Draft for Q2 campaign",
"secondary_keywords": [
"keyword research",
"best seo software"
],
"surfer_template": "blog_post",
"target_word_count": 2000,
"use_brand_knowledge": true
}
'import requests
url = "https://app.surferseo.com/api/v2/workspaces/{workspace_id}/content_editors"
payload = {
"main_keyword": "seo tools",
"custom_instructions": "Mention product X, write from perspective of Y",
"custom_template_id": 2,
"custom_voice_id": 123,
"device": "mobile",
"import_content_url": "https://example.com/article",
"location": "United States",
"notes": "Draft for Q2 campaign",
"secondary_keywords": ["keyword research", "best seo software"],
"surfer_template": "blog_post",
"target_word_count": 2000,
"use_brand_knowledge": True
}
headers = {
"API-KEY": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'API-KEY': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
main_keyword: 'seo tools',
custom_instructions: 'Mention product X, write from perspective of Y',
custom_template_id: 2,
custom_voice_id: 123,
device: 'mobile',
import_content_url: 'https://example.com/article',
location: 'United States',
notes: 'Draft for Q2 campaign',
secondary_keywords: ['keyword research', 'best seo software'],
surfer_template: 'blog_post',
target_word_count: 2000,
use_brand_knowledge: true
})
};
fetch('https://app.surferseo.com/api/v2/workspaces/{workspace_id}/content_editors', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.surferseo.com/api/v2/workspaces/{workspace_id}/content_editors",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'main_keyword' => 'seo tools',
'custom_instructions' => 'Mention product X, write from perspective of Y',
'custom_template_id' => 2,
'custom_voice_id' => 123,
'device' => 'mobile',
'import_content_url' => 'https://example.com/article',
'location' => 'United States',
'notes' => 'Draft for Q2 campaign',
'secondary_keywords' => [
'keyword research',
'best seo software'
],
'surfer_template' => 'blog_post',
'target_word_count' => 2000,
'use_brand_knowledge' => true
]),
CURLOPT_HTTPHEADER => [
"API-KEY: <api-key>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://app.surferseo.com/api/v2/workspaces/{workspace_id}/content_editors"
payload := strings.NewReader("{\n \"main_keyword\": \"seo tools\",\n \"custom_instructions\": \"Mention product X, write from perspective of Y\",\n \"custom_template_id\": 2,\n \"custom_voice_id\": 123,\n \"device\": \"mobile\",\n \"import_content_url\": \"https://example.com/article\",\n \"location\": \"United States\",\n \"notes\": \"Draft for Q2 campaign\",\n \"secondary_keywords\": [\n \"keyword research\",\n \"best seo software\"\n ],\n \"surfer_template\": \"blog_post\",\n \"target_word_count\": 2000,\n \"use_brand_knowledge\": true\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("API-KEY", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.surferseo.com/api/v2/workspaces/{workspace_id}/content_editors")
.header("API-KEY", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"main_keyword\": \"seo tools\",\n \"custom_instructions\": \"Mention product X, write from perspective of Y\",\n \"custom_template_id\": 2,\n \"custom_voice_id\": 123,\n \"device\": \"mobile\",\n \"import_content_url\": \"https://example.com/article\",\n \"location\": \"United States\",\n \"notes\": \"Draft for Q2 campaign\",\n \"secondary_keywords\": [\n \"keyword research\",\n \"best seo software\"\n ],\n \"surfer_template\": \"blog_post\",\n \"target_word_count\": 2000,\n \"use_brand_knowledge\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.surferseo.com/api/v2/workspaces/{workspace_id}/content_editors")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["API-KEY"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"main_keyword\": \"seo tools\",\n \"custom_instructions\": \"Mention product X, write from perspective of Y\",\n \"custom_template_id\": 2,\n \"custom_voice_id\": 123,\n \"device\": \"mobile\",\n \"import_content_url\": \"https://example.com/article\",\n \"location\": \"United States\",\n \"notes\": \"Draft for Q2 campaign\",\n \"secondary_keywords\": [\n \"keyword research\",\n \"best seo software\"\n ],\n \"surfer_template\": \"blog_post\",\n \"target_word_count\": 2000,\n \"use_brand_knowledge\": true\n}"
response = http.request(request)
puts response.read_body{
"inserted_at": "2024-04-12T15:23:45Z",
"ai_article": {
"id": 123,
"status": "completed"
},
"ai_search_guidelines": {
"status": "completed"
},
"auto_optimize": {
"status": "completed"
},
"content_score": {
"ai_search": 62,
"seo": 75,
"total": 75
},
"created_by": "[email protected]",
"device": "mobile",
"id": 123,
"location": "United States",
"main_keyword": "seo tools",
"outline": {
"status": "completed"
},
"permalinks": [
{
"hash": "abc123xyz",
"type": "edit",
"url": "https://app.surferseo.com/drafts/s/abc123xyz"
}
],
"secondary_keywords": [
"keyword research",
"ai visibility"
],
"seo_guidelines": {
"status": "completed"
},
"state": "scheduled",
"workspace_id": 123,
"updated_at": "2024-06-01T09:17:33Z",
"custom_template_id": 2,
"custom_voice_id": 123,
"error": {
"details": [
{}
],
"reason": "<string>",
"message": "<string>"
},
"import_content_url": "https://example.com/article",
"surfer_template": "blog_post",
"target_word_count": 2000,
"use_brand_knowledge": true,
"custom_instructions": "Write from perspective of a senior developer",
"meta_description": "A curated list of the top SEO tools for 2025.",
"meta_title": "Best SEO Tools in 2025",
"notes": "Draft for Q2 campaign"
}{
"error": {
"details": [
{
"field": "keywords",
"reason": "required"
}
],
"reason": "validation_error"
}
}{
"error": {
"details": [],
"reason": "unauthorized"
}
}{
"error": {
"details": [],
"reason": "permission_denied"
}
}{
"error": {
"details": [],
"reason": "not_found"
}
}{
"error": {
"details": [
{
"allowed": [
"application/json"
],
"field": "accept",
"reason": "unsupported"
}
],
"message": "Accept must be one of: application/json",
"reason": "not_acceptable"
}
}{
"error": {
"details": [],
"message": "Set Content-Type: application/json.",
"reason": "unsupported_media_type"
}
}{
"error": {
"details": [],
"message": "The request was understood but could not be processed.",
"reason": "unprocessable_entity"
}
}{
"error": {
"details": [
{
"retry_after": 30
}
],
"reason": "rate_limit_exceeded"
}
}Create Content Editor
Creates a new Content Editor. The operation is asynchronous — the response returns immediately
with the Content Editor in scheduled state.
use_brand_knowledge defaults to true. When enabled, workspace brand knowledge is used where available; set false for unbranded output.
Supports Idempotency-Key header.
Keywords: main_keyword (required) is the primary keyword that drives the SERP analysis.
Add up to 19 secondary_keywords (20 keywords total including the main keyword); exceeding the
limit returns 400 with a secondary_keywords / too_many detail.
Locations: location is a country or region display name (e.g. "United States", the default
when omitted). Fetch the full list of supported values from GET /api/v1/locations (no auth
required). device selects the SERP device profile — mobile (default) or desktop.
Templates: surfer_template and custom_template_id are mutually exclusive. When neither
is provided, Surfer asynchronously preselects a template — the workspace’s default custom
template if configured, otherwise a Surfer template inferred from the SERP. Read the final
values after state becomes completed. On a completed Content Editor, both fields null
represents the “SERP-based” template: the article structure follows the SERP analysis rather
than a preset.
Custom voice: when custom_voice_id is omitted, the workspace’s default custom voice (if any)
is used. Pass null explicitly to opt out.
Webhooks: content_editor.initialization.completed / content_editor.initialization.failed
fire when initialization settles. During initialization the AI Search score is computed, firing
content_editor.ai_search_score.calculated / .failed when it lands (no event when the score ends
unavailable). The SEO score computed for imported content at creation sends no event;
content_editor.seo_score.calculated / .failed and content_editor.content_score.recalculated
fire after content updates made through the API. As a fallback, poll GET .../content_editors/:id.
Cost: each successful request consumes one Content Editor credit.
Rate limit: 10 requests/sec
curl --request POST \
--url https://app.surferseo.com/api/v2/workspaces/{workspace_id}/content_editors \
--header 'API-KEY: <api-key>' \
--header 'Content-Type: application/json' \
--data '
{
"main_keyword": "seo tools",
"custom_instructions": "Mention product X, write from perspective of Y",
"custom_template_id": 2,
"custom_voice_id": 123,
"device": "mobile",
"import_content_url": "https://example.com/article",
"location": "United States",
"notes": "Draft for Q2 campaign",
"secondary_keywords": [
"keyword research",
"best seo software"
],
"surfer_template": "blog_post",
"target_word_count": 2000,
"use_brand_knowledge": true
}
'import requests
url = "https://app.surferseo.com/api/v2/workspaces/{workspace_id}/content_editors"
payload = {
"main_keyword": "seo tools",
"custom_instructions": "Mention product X, write from perspective of Y",
"custom_template_id": 2,
"custom_voice_id": 123,
"device": "mobile",
"import_content_url": "https://example.com/article",
"location": "United States",
"notes": "Draft for Q2 campaign",
"secondary_keywords": ["keyword research", "best seo software"],
"surfer_template": "blog_post",
"target_word_count": 2000,
"use_brand_knowledge": True
}
headers = {
"API-KEY": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'API-KEY': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({
main_keyword: 'seo tools',
custom_instructions: 'Mention product X, write from perspective of Y',
custom_template_id: 2,
custom_voice_id: 123,
device: 'mobile',
import_content_url: 'https://example.com/article',
location: 'United States',
notes: 'Draft for Q2 campaign',
secondary_keywords: ['keyword research', 'best seo software'],
surfer_template: 'blog_post',
target_word_count: 2000,
use_brand_knowledge: true
})
};
fetch('https://app.surferseo.com/api/v2/workspaces/{workspace_id}/content_editors', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.surferseo.com/api/v2/workspaces/{workspace_id}/content_editors",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'main_keyword' => 'seo tools',
'custom_instructions' => 'Mention product X, write from perspective of Y',
'custom_template_id' => 2,
'custom_voice_id' => 123,
'device' => 'mobile',
'import_content_url' => 'https://example.com/article',
'location' => 'United States',
'notes' => 'Draft for Q2 campaign',
'secondary_keywords' => [
'keyword research',
'best seo software'
],
'surfer_template' => 'blog_post',
'target_word_count' => 2000,
'use_brand_knowledge' => true
]),
CURLOPT_HTTPHEADER => [
"API-KEY: <api-key>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://app.surferseo.com/api/v2/workspaces/{workspace_id}/content_editors"
payload := strings.NewReader("{\n \"main_keyword\": \"seo tools\",\n \"custom_instructions\": \"Mention product X, write from perspective of Y\",\n \"custom_template_id\": 2,\n \"custom_voice_id\": 123,\n \"device\": \"mobile\",\n \"import_content_url\": \"https://example.com/article\",\n \"location\": \"United States\",\n \"notes\": \"Draft for Q2 campaign\",\n \"secondary_keywords\": [\n \"keyword research\",\n \"best seo software\"\n ],\n \"surfer_template\": \"blog_post\",\n \"target_word_count\": 2000,\n \"use_brand_knowledge\": true\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("API-KEY", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.surferseo.com/api/v2/workspaces/{workspace_id}/content_editors")
.header("API-KEY", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"main_keyword\": \"seo tools\",\n \"custom_instructions\": \"Mention product X, write from perspective of Y\",\n \"custom_template_id\": 2,\n \"custom_voice_id\": 123,\n \"device\": \"mobile\",\n \"import_content_url\": \"https://example.com/article\",\n \"location\": \"United States\",\n \"notes\": \"Draft for Q2 campaign\",\n \"secondary_keywords\": [\n \"keyword research\",\n \"best seo software\"\n ],\n \"surfer_template\": \"blog_post\",\n \"target_word_count\": 2000,\n \"use_brand_knowledge\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.surferseo.com/api/v2/workspaces/{workspace_id}/content_editors")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["API-KEY"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"main_keyword\": \"seo tools\",\n \"custom_instructions\": \"Mention product X, write from perspective of Y\",\n \"custom_template_id\": 2,\n \"custom_voice_id\": 123,\n \"device\": \"mobile\",\n \"import_content_url\": \"https://example.com/article\",\n \"location\": \"United States\",\n \"notes\": \"Draft for Q2 campaign\",\n \"secondary_keywords\": [\n \"keyword research\",\n \"best seo software\"\n ],\n \"surfer_template\": \"blog_post\",\n \"target_word_count\": 2000,\n \"use_brand_knowledge\": true\n}"
response = http.request(request)
puts response.read_body{
"inserted_at": "2024-04-12T15:23:45Z",
"ai_article": {
"id": 123,
"status": "completed"
},
"ai_search_guidelines": {
"status": "completed"
},
"auto_optimize": {
"status": "completed"
},
"content_score": {
"ai_search": 62,
"seo": 75,
"total": 75
},
"created_by": "[email protected]",
"device": "mobile",
"id": 123,
"location": "United States",
"main_keyword": "seo tools",
"outline": {
"status": "completed"
},
"permalinks": [
{
"hash": "abc123xyz",
"type": "edit",
"url": "https://app.surferseo.com/drafts/s/abc123xyz"
}
],
"secondary_keywords": [
"keyword research",
"ai visibility"
],
"seo_guidelines": {
"status": "completed"
},
"state": "scheduled",
"workspace_id": 123,
"updated_at": "2024-06-01T09:17:33Z",
"custom_template_id": 2,
"custom_voice_id": 123,
"error": {
"details": [
{}
],
"reason": "<string>",
"message": "<string>"
},
"import_content_url": "https://example.com/article",
"surfer_template": "blog_post",
"target_word_count": 2000,
"use_brand_knowledge": true,
"custom_instructions": "Write from perspective of a senior developer",
"meta_description": "A curated list of the top SEO tools for 2025.",
"meta_title": "Best SEO Tools in 2025",
"notes": "Draft for Q2 campaign"
}{
"error": {
"details": [
{
"field": "keywords",
"reason": "required"
}
],
"reason": "validation_error"
}
}{
"error": {
"details": [],
"reason": "unauthorized"
}
}{
"error": {
"details": [],
"reason": "permission_denied"
}
}{
"error": {
"details": [],
"reason": "not_found"
}
}{
"error": {
"details": [
{
"allowed": [
"application/json"
],
"field": "accept",
"reason": "unsupported"
}
],
"message": "Accept must be one of: application/json",
"reason": "not_acceptable"
}
}{
"error": {
"details": [],
"message": "Set Content-Type: application/json.",
"reason": "unsupported_media_type"
}
}{
"error": {
"details": [],
"message": "The request was understood but could not be processed.",
"reason": "unprocessable_entity"
}
}{
"error": {
"details": [
{
"retry_after": 30
}
],
"reason": "rate_limit_exceeded"
}
}Authorizations
Headers
Client-generated key for safely retrying create requests. Reusing the same key for the same path answers with the original status code and the resource as it is now; reusing it for a different path returns 422. Keys expire after 24 hours.
Path Parameters
x >= 1123
Body
Content Editor params
Primary keyword for SERP analysis
700"seo tools"
"Mention product X, write from perspective of Y"
Workspace-level custom template ID. Mutually exclusive with surfer_template.
x >= 1x >= 1123
mobile, desktop "mobile"
"https://example.com/article"
Location; get the list of supported locations from /api/v1/locations (no auth required)
"United States"
"Draft for Q2 campaign"
Up to 19 secondary keywords (20 total including main keyword).
19700["keyword research", "best seo software"]
Surfer template preset. Mutually exclusive with custom_template_id.
blog_post, category_description, product_description, comparison, listicle, landing_page, local_business, llm_optimized_article "blog_post"
x >= 12000
Defaults to true. When enabled, workspace brand knowledge is used where available; set false for unbranded output.
true
Response
Created Content Editor
"2024-04-12T15:23:45Z"
Active AI article. null if no AI article has been generated for the Content Editor.
Show child attributes
Show child attributes
AI Search guidelines state. null when no AI Search guidelines exist for the Content Editor (e.g. created before the workflow existed) — distinct from {status: "executing"} (generation in progress).
Show child attributes
Show child attributes
Latest Auto-Optimize job state. null when no Auto-Optimize has ever been run on this Content Editor.
Show child attributes
Show child attributes
Show child attributes
Show child attributes
mobile, desktop "mobile"
x >= 1123
Location; get the list of supported locations from /api/v1/locations (no auth required)
"United States"
"seo tools"
Outline state. null when no outline exists for the Content Editor — distinct from {status: "scheduled"} (generation in progress).
Show child attributes
Show child attributes
Show child attributes
Show child attributes
["keyword research", "ai visibility"]
SEO guidelines state. null when no SEO guidelines exist for the Content Editor — distinct from {status: "executing"} (generation in progress).
Show child attributes
Show child attributes
completed, executing, failed, scheduled "scheduled"
x >= 1123
"2024-06-01T09:17:33Z"
Workspace-level custom template ID. Mutually exclusive with surfer_template.
x >= 1x >= 1123
Show child attributes
Show child attributes
"https://example.com/article"
Surfer template preset. Mutually exclusive with custom_template_id.
blog_post, category_description, product_description, comparison, listicle, landing_page, local_business, llm_optimized_article "blog_post"
2000
Whether workspace brand knowledge is enabled for this Content Editor.
true
"Write from perspective of a senior developer"
Draft meta description.
"A curated list of the top SEO tools for 2025."
Draft meta title.
"Best SEO Tools in 2025"
"Draft for Q2 campaign"
Was this page helpful?