Stepeee Stepeee Fully Automated AI Platform

Authentication

1

Check your API key

You can check it from the menu in the top right > Settings > Account > API Keys.

2

Add it to the request header

Add your issued API key to the Authorization header in Bearer format.

3

Send a request to the base URL

Send requests to the base URL below followed by the endpoint path.

Each account has one API key. When you change the key, the old key stops working immediately. Keep your key private.

Base URL

https://stepeee.com/public-api/v1

Request example

curl "https://stepeee.com/public-api/v1/projects" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/projects",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/projects", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/projects")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/projects"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/projects");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;

Response format & errors

All responses are returned in the following envelope format.

On success

{ "data": ... }

On error

{ "error": { "code": "...", "message": "...", "details": { ... } } }

Status codes

StatusMeaning
401The API key is missing, invalid, or has been revoked
403You do not have permission for this request (for example, calling an endpoint that requires the Pro plan or higher with an API key of an account below the Pro plan, or a team staff member requesting the team's token usage)
404The resource does not exist (this also includes cases where you do not have access permission)
405The HTTP method cannot be used for this path
409This action cannot be performed in the current state (for example, deleting content whose automation is running, or a request with the same Idempotency-Key still being processed). Use error.code to tell the reason
422The parameters are invalid or the request cannot be accepted. For validation_error, error.details contains the reason for each parameter
429The rate limit has been exceeded
500An unexpected error occurred
503The action could not be started (for example, when starting the automation run failed). Please try again later

Conventions

What you can access and change

You can read and change the same things the API key owner can see in the app. In addition to your own projects, on the Team plan the team owner and admins can read and change all of the team's projects, and staff can read and change the projects they have been granted. Resources outside this scope return 404, the same as resources that do not exist.

Deleted data

Deleted projects, concepts, titles, content, and materials are not returned, including during the period before they are permanently removed. When you delete a title or concept, the content under it is not returned either.

Pagination

Lists use page (starting at 1) and per_page (1–100, default 20) to choose the range to return. meta contains the total count (total), the current page (current_page), the number of items per page (per_page), and the last page (last_page).

Dates and times

All dates and times in responses are in UTC ISO 8601 format (for example, 2026-10-21T09:30:00Z). The from / to parameters accept a date (2026-10-21), a date and time (2026-10-21T09:30:00), or a date and time with an offset (2026-10-21T18:30:00+09:00); values without an offset are treated as UTC. from includes that moment, and when you pass only a date to to, the whole day up to 23:59:59 is included.

IDs

Resources are identified by alphanumeric public IDs such as project_unique and variation_unique. IDs do not change.

File URLs

URLs for images, videos, and audio are returned as {url, expires_at}. Private files use signed URLs that stop working after expires_at (one hour after they are issued). To keep using a file, download and store it before it expires, or call the API again to get a new URL. Files set to public use URLs that do not expire, and expires_at is null.

Value types

Values such as status and platform are returned as the lowercase strings listed on this page. New values may be added as features are added, so make sure your code keeps working when it receives a value it does not know.

Amounts

Amounts are integers in the smallest unit of the currency (cents for USD, yen for JPY). The currency is in currency as a lowercase ISO 4217 code (usd, jpy, and so on).

Write requests (Pro plan)

With methods other than GET, you can perform actions such as adding titles, deleting content, starting automations, approving posts, and registering webhooks. Each action runs the same process as the same action in the app, and the same rules apply. Write requests are available only when the API key owner is on the Pro plan or higher; below the Pro plan they return 403 plan_required. Team members get 403 team_plan_lost when the team owner is no longer on the Team plan. However, stopping automations, listing and deleting webhooks, and listing posts awaiting review are available on any plan (so you can stop running automations and notifications even after changing your plan). Creating something returns 201; everything else returns 200.

Idempotency-Key

So that you can safely retry an action when the connection drops and you do not receive the response, some write endpoints accept the Idempotency-Key header (the endpoints that accept it list it in their parameter table; it is required for starting automations). Use a unique string that you generate for each action (such as a UUID; 1–255 printable ASCII characters). If you resend the same content (method, path, and body) with the same key, the action is not performed again; the first response is returned as is, with the Idempotent-Replayed: true header. Keys are valid for 24 hours per API key owner. Sending different content with the same key returns 422 idempotency_key_reused, and if the first request is still being processed, 409 idempotency_request_in_progress is returned. 5xx responses are not stored, so you can retry with the same key. On endpoints where the header is required, a missing header returns 422 idempotency_key_required, and an invalid format returns 422 idempotency_key_invalid.

Rate limit

You can send up to 120 requests per minute per API key (write requests also have the write rate limit described below). You can check X-RateLimit-Limit (the limit) and X-RateLimit-Remaining (the remaining count) in the response headers. When you exceed the limit, 429 is returned, and Retry-After contains the number of seconds until you can retry.

Write rate limit

Write requests (methods other than GET) are limited to 30 requests per minute per API key, in addition to the 120 requests per minute limit. When you exceed it, 429 rate_limited is returned, and Retry-After contains the number of seconds until you can retry. For write requests, the X-RateLimit-Limit and X-RateLimit-Remaining response headers show the values of whichever of the two limits has fewer requests remaining.

Error codes

On error, error.code contains the type of error and error.message contains a description in English. The codes that any endpoint can return are listed below. Codes specific to an endpoint (such as title_limit_reached) are listed in that endpoint's error table. Use error.code in your code to tell errors apart.

CodeMeaning
unauthorizedThe API key is missing, invalid, or has been revoked (401)
forbiddenYou do not have permission (403)
plan_requiredThe action requires the Pro plan or higher (403)
team_plan_lostThe team owner is no longer on the Team plan, so team members cannot write (403)
not_foundThe resource does not exist or is outside what you can access (404)
method_not_allowedThe HTTP method cannot be used for this path (405)
conflictThis action cannot be performed in the current state (409)
idempotency_request_in_progressA request with the same Idempotency-Key is still being processed (409)
validation_errorThe parameters are invalid (422)
idempotency_key_requiredThe Idempotency-Key header is missing on an endpoint where it is required (422)
idempotency_key_invalidThe Idempotency-Key is not 1–255 printable ASCII characters (422)
idempotency_key_reusedThe same Idempotency-Key was used for a request with different content (422)
rate_limitedThe rate limit was exceeded (429)
internal_errorAn unexpected error occurred (500)

422 example

{
  "error": {
    "code": "validation_error",
    "message": "The per page field must not be greater than 100.",
    "details": {
      "per_page": ["The per page field must not be greater than 100."]
    }
  }
}

Account

Information about the API key owner's account. You can get the plan, token balance, usage, storage, payments, and referral program status.

GET › /me

Account information

Gets the API key owner's profile, plan, plan limits, and team.

Response fields
FieldTypeDescription
user_unique string The user ID
nickname string | null Nickname
email string The email address used to sign in
plan string The plan. free / vault / plus / pro / team
limits object The limits of the current plan. null means unlimited
limits.projects integer | null The number of projects you can create
limits.moviemode_variations_per_title integer | null The number of content items per video title
limits.blogmode_variations_per_title integer | null The number of content items per blog title
limits.carouselmode_variations_per_title integer | null The number of content items per carousel title
limits.postmode_variations_per_concept integer | null The number of content items per post concept
limits.team boolean Whether team features are available
limits.script_duration_max integer The maximum length of a video script (seconds)
limits.reading_time_max integer The maximum reading time of a blog article (minutes)
limits.reading_time_max_fulltext integer The maximum reading time of a blog article when the full text is read aloud (minutes)
limits.slide_count_max integer The maximum number of carousel slides
limits.cta_enabled boolean Whether CTAs (such as a final slide that prompts action) are available
limits.thread_enabled boolean Whether threads and posts with images are available for posts
limits.schedule_title_variation_max integer The number of titles and content items you can specify at once when creating scheduled items
limits.schedule_variation_create_max integer The number of content items you can create at once when creating scheduled items
language string | null The display language of the app
timezone string Your time zone setting (IANA format, for example Asia/Tokyo)
team object | null The team you belong to. null if you are not on the Team plan or do not belong to a team
team.team_unique string The team ID
team.role string Your role in the team. owner / admin / staff
created_at string When the account was created
Request example
curl "https://stepeee.com/public-api/v1/me" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/me",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/me", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/me")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/me"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/me");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": {
    "user_unique": "aB3dE5gH7j",
    "nickname": "Alice",
    "email": "alice@example.com",
    "plan": "pro",
    "limits": {
      "projects": 3,
      "moviemode_variations_per_title": null,
      "blogmode_variations_per_title": null,
      "carouselmode_variations_per_title": null,
      "postmode_variations_per_concept": null,
      "team": false,
      "script_duration_max": 1800,
      "reading_time_max": 15,
      "reading_time_max_fulltext": 4,
      "slide_count_max": 20,
      "cta_enabled": true,
      "thread_enabled": true,
      "schedule_title_variation_max": 50,
      "schedule_variation_create_max": 5
    },
    "language": "ja",
    "timezone": "Asia/Tokyo",
    "team": null,
    "created_at": "2026-09-01T10:20:30Z"
  }
}
GET › /me/subscription

Subscription

Gets the status of your current plan, the billing period, any scheduled cancellation, and any scheduled plan change. Team members get the team owner's subscription.

Response fields
FieldTypeDescription
plan string The plan. free / vault / plus / pro / team
status string The subscription status. active / past_due (payment overdue) / trialing (in a trial) / none (no paid plan)
managed_by_team boolean Whether you use the plan through the team owner's subscription. When true, the period and cancellation fields are the owner's values
current_period_start string | null When the current billing period started
current_period_end string | null When the current billing period ends (the next billing date)
cancel_at_period_end boolean Whether the subscription is scheduled to cancel at the end of the current billing period
cancel_at string | null When the subscription will be canceled
canceled_at string | null When the cancellation was requested
scheduled_change object | null A scheduled plan change (null if none)
scheduled_change.plan string The plan after the change
scheduled_change.effective_at string | null When the change takes effect
Request example
curl "https://stepeee.com/public-api/v1/me/subscription" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/me/subscription",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/me/subscription", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/me/subscription")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/me/subscription"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/me/subscription");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": {
    "plan": "pro",
    "status": "active",
    "managed_by_team": false,
    "current_period_start": "2026-10-01T00:00:00Z",
    "current_period_end": "2026-11-01T00:00:00Z",
    "cancel_at_period_end": false,
    "cancel_at": null,
    "canceled_at": null,
    "scheduled_change": {
      "plan": "plus",
      "effective_at": "2026-11-01T00:00:00Z"
    }
  }
}
GET › /me/tokens

Token balance

Gets your available token balance. On a team, it returns the balance shared by the team and the quota assigned to you.

Response fields
FieldTypeDescription
balance integer Balance
reserved integer The amount held for jobs that are running
available integer The amount you can use now (the balance minus the held amount)
wallet string personal (your own balance) / team (the balance shared by the team)
team_quota object | null Your quota set by the team owner. null when you use your own balance and for the team owner
team_quota.unlimited boolean Whether the quota is unlimited
team_quota.view_only boolean Whether you can only view and cannot use tokens
team_quota.budget integer | null The limit per period (null means unlimited, 0 means view only)
team_quota.used integer The amount used in the current period
team_quota.available integer | null The amount left in the current period (null if unlimited)
team_quota.period string | null The limit period. day / week / month (null when there is no limit)
team_quota.next_reset_at string | null When usage resets
auto_purchase.enabled boolean Whether automatic purchase when the balance runs low is on
auto_purchase.plan string | null The token package bought automatically (for example, token_100). null when off
auto_purchase.threshold integer A purchase is made automatically when the balance falls below this value
Request example
curl "https://stepeee.com/public-api/v1/me/tokens" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/me/tokens",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/me/tokens", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/me/tokens")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/me/tokens"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/me/tokens");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": {
    "balance": 90000,
    "reserved": 1200,
    "available": 88800,
    "wallet": "team",
    "team_quota": {
      "unlimited": false,
      "view_only": false,
      "budget": 10000,
      "used": 2500,
      "available": 7500,
      "period": "week",
      "next_reset_at": "2026-10-26T00:00:00Z"
    },
    "auto_purchase": {
      "enabled": true,
      "plan": "token_100",
      "threshold": 5000
    }
  }
}
GET › /me/token-usage

Token usage

Gets token usage and additions by day or by month. Usage is totaled one day at a time, and the previous day is reflected around 2:45 UTC (today's usage is not included). Additions from purchases and bonuses are reflected immediately.

Parameters
Name In Type Required Default Description
from Query string (YYYY-MM-DD) Optional — The start date (UTC). 29 days before to when omitted
to Query string (YYYY-MM-DD) Optional — The end date (UTC, inclusive). Today when omitted. The range can be up to 366 days
group_by Query string Optional day How to group. day / month
Response fields
FieldTypeDescription
data[].period string The period. YYYY-MM-DD by day, YYYY-MM by month. Periods with no usage or additions are not included
data[].consumed integer Usage
data[].added integer Additions (net of decreases such as revoked referral rewards)
data[].consumed_by_type object Usage by type (text / image / video / audio, and so on)
data[].added_by_type object Additions by type. purchase / bonus / refund / referral_revoke (revoked referral reward, negative)
meta.from string The start date of the totaled period
meta.to string The end date of the totaled period
meta.group_by string How the results are grouped
meta.total_consumed integer Total usage in the period
meta.total_added integer Total additions in the period
Errors
StatusCodeWhen
403 forbidden A staff member who uses the team's tokens (only the team owner and admins can get the team's token usage)
422 validation_error The range is longer than 366 days, to is earlier than from, a value cannot be read as a date, or group_by is not day / month
Request example
curl "https://stepeee.com/public-api/v1/me/token-usage?from=2026-10-01&to=2026-10-20&group_by=day" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/me/token-usage?from=2026-10-01&to=2026-10-20&group_by=day",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/me/token-usage?from=2026-10-01&to=2026-10-20&group_by=day", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/me/token-usage?from=2026-10-01&to=2026-10-20&group_by=day")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/me/token-usage?from=2026-10-01&to=2026-10-20&group_by=day"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/me/token-usage?from=2026-10-01&to=2026-10-20&group_by=day");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": [
    {
      "period": "2026-10-18",
      "consumed": 450,
      "added": 0,
      "consumed_by_type": { "image": 300, "text": 150 },
      "added_by_type": {}
    },
    {
      "period": "2026-10-20",
      "consumed": 1000,
      "added": 4800,
      "consumed_by_type": { "video": 1000 },
      "added_by_type": { "purchase": 5000, "referral_revoke": -200 }
    }
  ],
  "meta": {
    "from": "2026-10-01",
    "to": "2026-10-20",
    "group_by": "day",
    "total_consumed": 1450,
    "total_added": 4800
  }
}
GET › /me/storage

Storage

Gets storage usage, capacity, and a breakdown by file type. On a team, it returns the storage shared by the team.

Response fields
FieldTypeDescription
used_bytes integer Usage (bytes). The same as the total of by_type
capacity_bytes integer | null Capacity (bytes). null means unlimited, and 0 means no storage (Free plan)
by_type[].file_type string | null The file type. material / image / video / bgm / voice (audio) / movie (finished video)
by_type[].bytes integer Usage of that type (bytes)
by_type[].files integer The number of files of that type
Request example
curl "https://stepeee.com/public-api/v1/me/storage" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/me/storage",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/me/storage", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/me/storage")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/me/storage"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/me/storage");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": {
    "used_bytes": 734003200,
    "capacity_bytes": null,
    "by_type": [
      { "file_type": "image", "bytes": 52428800, "files": 120 },
      { "file_type": "movie", "bytes": 629145600, "files": 8 },
      { "file_type": "voice", "bytes": 52428800, "files": 15 }
    ]
  }
}
GET › /me/payments

Payment history

Gets payments such as plan fees and token purchases, newest first. You can use this to import payments into your accounting system.

Parameters
Name In Type Required Default Description
status Query string Optional — Filter by payment status (comma-separated). succeeded / refunded / partially_refunded. All when omitted
type Query string Optional — Filter by payment type (comma-separated). The values are the same as data[].type
from Query string Optional — Returns only items created at or after this date and time
to Query string Optional — Returns only items created at or before this date and time
page Query integer Optional 1 The page to return (starting at 1)
per_page Query integer Optional 20 The number of items per page (1–100)
Response fields
FieldTypeDescription
data[].payment_unique string The payment ID
data[].type string The type. token_first_time (first token purchase) / token_regular (token purchase) / subscription_vault, subscription_plus, subscription_pro, subscription_team (plan fees) / smm_application, smm_contract, smm_usage (X account management fees)
data[].description string The name of the payment (in English)
data[].status string The status. succeeded (paid) / refunded (fully refunded) / partially_refunded
data[].currency string The currency (lowercase ISO 4217)
data[].amount_subtotal integer The amount before tax (smallest unit)
data[].tax_amount integer The tax (smallest unit)
data[].amount_total integer The amount including tax (smallest unit)
data[].line_items[] object Line items. type (base_plan: plan fee / storage_addon: additional storage / extra_member: additional member), quantity, amount, and description (null if none)
data[].receipt_url string | null The receipt URL (Stripe). It may stop opening some time after it is issued
data[].invoice_pdf_url string | null The invoice PDF URL (Stripe). It may stop opening some time after it is issued
data[].created_at string When the payment was made
meta.total integer The total number of matching items
meta.current_page integer The current page
meta.per_page integer The number of items per page
meta.last_page integer The last page
Errors
StatusCodeWhen
422 validation_error The parameters are invalid
Request example
curl "https://stepeee.com/public-api/v1/me/payments?status=succeeded&per_page=10" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/me/payments?status=succeeded&per_page=10",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/me/payments?status=succeeded&per_page=10", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/me/payments?status=succeeded&per_page=10")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/me/payments?status=succeeded&per_page=10"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/me/payments?status=succeeded&per_page=10");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": [
    {
      "payment_unique": "Xy12AbCd34Ef",
      "type": "subscription_team",
      "description": "Team Monthly",
      "status": "succeeded",
      "currency": "usd",
      "amount_subtotal": 5000,
      "tax_amount": 500,
      "amount_total": 5500,
      "line_items": [
        { "type": "base_plan", "quantity": 1, "amount": 4000, "description": "Team plan" },
        { "type": "extra_member", "quantity": 2, "amount": 1000, "description": null }
      ],
      "receipt_url": "https://pay.stripe.com/receipts/...",
      "invoice_pdf_url": "https://pay.stripe.com/invoice/.../pdf",
      "created_at": "2026-10-01T08:00:00Z"
    }
  ],
  "meta": { "total": 1, "current_page": 1, "per_page": 10, "last_page": 1 }
}
GET › /me/referral

Referral program

Gets your referral code and the status of your referrals and rewards.

Response fields
FieldTypeDescription
code string | null The referral code (null if you do not have one yet)
referral_url string | null The referral URL (null if you do not have a referral code)
is_active boolean Whether the referral code is active
reward_type string | null How you receive rewards. cash / token. null if you do not have a referral code
clicks integer The number of times the referral URL was opened
signups integer The number of people who signed up through your referral
conversions integer The number of people who subscribed to a paid plan through your referral
earned object Rewards confirmed so far {amount (smallest unit), currency}
earned_tokens integer Tokens received so far
pending object Rewards waiting to be confirmed {amount, currency}
available object Rewards you can request to receive {amount, currency}
Request example
curl "https://stepeee.com/public-api/v1/me/referral" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/me/referral",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/me/referral", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/me/referral")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/me/referral"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/me/referral");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": {
    "code": "REFk5m2j8x9ab",
    "referral_url": "https://stepeee.com?ref=REFk5m2j8x9ab",
    "is_active": true,
    "reward_type": "cash",
    "clicks": 12,
    "signups": 4,
    "conversions": 2,
    "earned": { "amount": 1300, "currency": "usd" },
    "earned_tokens": 0,
    "pending": { "amount": 300, "currency": "usd" },
    "available": { "amount": 800, "currency": "usd" }
  }
}

Projects

Get projects and the structure of concepts and titles inside them.

GET › /projects

Project list

Gets the projects you can access, newest first.

Parameters
Name In Type Required Default Description
page Query integer Optional 1 The page to return (starting at 1)
per_page Query integer Optional 20 The number of items per page (1–100)
Response fields
FieldTypeDescription
data[].project_unique string The project ID
data[].project_name string The project name
data[].description string | null The project description
data[].access_level string Your permission. owner (your own project) / admin (full permission as the team owner or an admin) / edit (edit permission granted in the team)
data[].team object | null The team for a team project, or null for a project that is only yours
data[].team.team_unique string The team ID
data[].is_readonly boolean Whether the project is view-only because it exceeds the limit after a plan downgrade (this is removed when you upgrade)
data[].readonly_since string | null When the project became view-only
data[].created_at string Created at
data[].updated_at string Updated at
meta.total integer The total number of matching items
meta.current_page integer The current page
meta.per_page integer The number of items per page
meta.last_page integer The last page
Errors
StatusCodeWhen
422 validation_error The parameters are invalid
Request example
curl "https://stepeee.com/public-api/v1/projects" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/projects",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/projects", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/projects")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/projects"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/projects");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": [
    {
      "project_unique": "a1b2c3d4e5",
      "project_name": "Sample Project",
      "description": "Sample project description",
      "access_level": "owner",
      "team": null,
      "is_readonly": false,
      "readonly_since": null,
      "created_at": "2026-01-10T09:00:00Z",
      "updated_at": "2026-10-01T12:00:00Z"
    }
  ],
  "meta": { "total": 1, "current_page": 1, "per_page": 20, "last_page": 1 }
}
GET › /projects/{project_unique}

Project detail

Gets the details of one project, with the number of concepts and titles per mode and the number of content items by progress.

Parameters
Name In Type Required Default Description
project_unique Path string Required — The project ID
Response fields
FieldTypeDescription
project_unique string The project ID
project_name string The project name
description string | null The project description
access_level string Your permission. owner (your own project) / admin (full permission as the team owner or an admin) / edit (edit permission granted in the team)
team object | null The team for a team project, or null for a project that is only yours
team.team_unique string The team ID
is_readonly boolean Whether the project is view-only because it exceeds the limit after a plan downgrade (this is removed when you upgrade)
readonly_since string | null When the project became view-only
created_at string Created at
updated_at string Updated at
scope string The target region. global (not specified) / region (a specific country)
scope_region string | null The target country (ISO 3166-1 alpha-2, for example JP). null when global
counts.{mode}.concepts integer The number of concepts per mode (moviemode / blogmode / carouselmode / postmode)
counts.{mode}.titles integer | null The number of titles (null for postmode, which has no titles)
counts.{mode}.contents object The number of content items {total, not_started, in_progress, complete}
Errors
StatusCodeWhen
404 not_found The project does not exist or is outside what you can access (Project not found.)
Request example
curl "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": {
    "project_unique": "a1b2c3d4e5",
    "project_name": "Sample Project",
    "description": "Sample project description",
    "access_level": "owner",
    "team": null,
    "is_readonly": false,
    "readonly_since": null,
    "created_at": "2026-01-10T09:00:00Z",
    "updated_at": "2026-10-01T12:00:00Z",
    "scope": "region",
    "scope_region": "JP",
    "counts": {
      "moviemode": {
        "concepts": 2,
        "titles": 5,
        "contents": { "total": 8, "not_started": 1, "in_progress": 3, "complete": 4 }
      },
      "blogmode": {
        "concepts": 1,
        "titles": 3,
        "contents": { "total": 3, "not_started": 0, "in_progress": 1, "complete": 2 }
      },
      "carouselmode": {
        "concepts": 0,
        "titles": 0,
        "contents": { "total": 0, "not_started": 0, "in_progress": 0, "complete": 0 }
      },
      "postmode": {
        "concepts": 1,
        "titles": null,
        "contents": { "total": 1, "not_started": 0, "in_progress": 1, "complete": 0 }
      }
    }
  }
}
GET › /projects/{project_unique}/concepts

Concept list

Gets the concepts in a project across all modes, newest first.

Parameters
Name In Type Required Default Description
project_unique Path string Required — The project ID
mode Query string Optional — Filter by mode. You can pass several of moviemode (video), blogmode (blog), carouselmode (carousel), and postmode (post), separated by commas. All modes when omitted
page Query integer Optional 1 The page to return (starting at 1)
per_page Query integer Optional 20 The number of items per page (1–100)
Response fields
FieldTypeDescription
data[].concept_unique string The concept ID
data[].mode string moviemode (video) / blogmode (blog) / carouselmode (carousel) / postmode (post)
data[].concept_name string The concept name
data[].title_count integer | null The number of titles (null for postmode)
data[].content_count integer The number of content items
data[].created_at string Created at
data[].updated_at string Updated at
meta.total integer The total number of matching items
meta.current_page integer The current page
meta.per_page integer The number of items per page
meta.last_page integer The last page
Errors
StatusCodeWhen
404 not_found The project does not exist or is outside what you can access (Project not found.)
422 validation_error The parameters are invalid
Request example
curl "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/concepts?mode=moviemode,postmode" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/concepts?mode=moviemode,postmode",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/concepts?mode=moviemode,postmode", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/concepts?mode=moviemode,postmode")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/concepts?mode=moviemode,postmode"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/concepts?mode=moviemode,postmode");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": [
    {
      "concept_unique": "pC7d2KqLx01m",
      "mode": "postmode",
      "concept_name": "SaaS growth tips",
      "title_count": null,
      "content_count": 1,
      "created_at": "2026-10-02T09:00:00Z",
      "updated_at": "2026-10-02T09:00:00Z"
    },
    {
      "concept_unique": "mC4f9TnB2q",
      "mode": "moviemode",
      "concept_name": "Cat facts",
      "title_count": 3,
      "content_count": 5,
      "created_at": "2026-09-20T03:00:00Z",
      "updated_at": "2026-09-28T03:00:00Z"
    }
  ],
  "meta": { "total": 2, "current_page": 1, "per_page": 20, "last_page": 1 }
}
GET › /projects/{project_unique}/concepts/{concept_unique}

Concept detail

Gets the details of one concept. Video, blog, and carousel concepts include the list of titles, and post (postmode) concepts include the persona of the poster and the posting rules.

Parameters
Name In Type Required Default Description
project_unique Path string Required — The project ID
concept_unique Path string Required — The concept ID
Response fields
FieldTypeDescription
concept_unique / mode / concept_name / title_count / content_count / created_at / updated_at — Same as the concept list
description string | null The concept description
scope string The target region. global (not specified) / region (a specific country)
scope_region string | null The target country (ISO 3166-1 alpha-2, for example JP). null when global
default_template_unique string | null The setup template used for new content (template_unique in the template list). null when not set or for postmode
titles array | null The list of titles (newest first). null for postmode
titles[].title_unique string The title ID
titles[].title_name string The title
titles[].article_type string | null The type. standard / news for moviemode, standard / news / promotion (product introduction) for blogmode. null for carouselmode
titles[].content_count integer The number of content items for that title
persona object | null The persona of the poster (postmode only, null if not created)
persona.persona_unique string The persona ID
persona.gender string Gender. male / female / undisclosed
persona.generation string Age group. 10s / 20s / 30s / 40s / 50s / 60s_plus
persona.region string | null The country code of the cultural region
persona.keywords object Persona keywords. occupation, industry, residence, workstyle, household, and personality are strings, and interest and expertise are arrays of strings. Only the ones that are set are included
persona.description string The persona text
persona.casual_description string | null The persona text used for everyday posts (null if none)
persona.taboos array Things to avoid in posts
persona.timezone_mode string How the time zone is decided. auto (from the region, or from the posting language when there is no region) / specify (the specified time zone)
persona.timezone string | null The time zone used for the posting schedule (IANA format). null when it cannot be decided because the posting rules have not been created
rule object | null The posting rules (postmode only, null if not created)
rule.rule_unique string The posting rules ID
rule.language string The posting language
rule.formality string Tone. very_casual / casual / normal / polite / honorific
rule.emoji string Emoji. never / sometimes / often / always
rule.line_break string How many line breaks. none / moderate / frequent
rule.post_type_share object The share of each post type (%). assertion (opinion) / knowhow / experience / casual (everyday)
Errors
StatusCodeWhen
404 not_found The project does not exist or is outside what you can access (Project not found.)
404 not_found The concept does not exist (Concept not found.)
Request example
curl "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/concepts/pC7d2KqLx01m" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/concepts/pC7d2KqLx01m",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/concepts/pC7d2KqLx01m", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/concepts/pC7d2KqLx01m")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/concepts/pC7d2KqLx01m"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/concepts/pC7d2KqLx01m");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": {
    "concept_unique": "pC7d2KqLx01m",
    "mode": "postmode",
    "concept_name": "SaaS growth tips",
    "title_count": null,
    "content_count": 1,
    "created_at": "2026-10-02T09:00:00Z",
    "updated_at": "2026-10-02T09:00:00Z",
    "description": "Practical growth tips for SaaS founders",
    "scope": "global",
    "scope_region": null,
    "default_template_unique": null,
    "titles": null,
    "persona": {
      "persona_unique": "aV3kQ9mN2pRt",
      "gender": "female",
      "generation": "30s",
      "region": "JP",
      "keywords": { "occupation": "SaaS marketer", "interest": ["growth", "analytics"] },
      "description": "A B2B SaaS marketer with ten years of experience...",
      "casual_description": null,
      "taboos": ["politics"],
      "timezone_mode": "auto",
      "timezone": "Asia/Tokyo",
      "updated_at": "2026-10-02T09:10:00Z"
    },
    "rule": {
      "rule_unique": "rU5tW7yZ9aBc",
      "language": "ja",
      "formality": "polite",
      "emoji": "sometimes",
      "line_break": "moderate",
      "post_type_share": { "assertion": 40, "knowhow": 30, "experience": 20, "casual": 10 },
      "updated_at": "2026-10-02T09:20:00Z"
    }
  }
}

Content

Get video, blog, carousel, and post content across modes. In the detail, package contains what is posted to each social network or site (body text, images, videos, and captions), so you can bring the content you made in Stepeee into your own systems.

GET › /projects/{project_unique}/contents

Content list

Gets the content in a project (video, blog, carousel, and post) across modes. You can filter by progress, publishing language, whether it has been posted, and more.

Parameters
Name In Type Required Default Description
project_unique Path string Required — The project ID
mode Query string Optional — Filter by mode. You can pass several of moviemode (video), blogmode (blog), carouselmode (carousel), and postmode (post), separated by commas. All modes when omitted
status Query string Optional — Filter by progress. not_started / in_progress / complete
concept_unique Query string Optional — Returns only the content of this concept
title_unique Query string Optional — Returns only the content of this title (postmode is not included because it has no titles)
language Query string Optional — Filter by publishing language (for example, ja or en)
published Query string Optional — true returns only content that has been posted, and false returns only content that has not been posted yet
from Query string Optional — Returns only items created at or after this date and time
to Query string Optional — Returns only items created at or before this date and time
sort Query string Optional -created_at The order. created_at / -created_at / updated_at / -updated_at (- means newest first)
page Query integer Optional 1 The page to return (starting at 1)
per_page Query integer Optional 20 The number of items per page (1–100)
Response fields
FieldTypeDescription
data[].mode string moviemode (video) / blogmode (blog) / carouselmode (carousel) / postmode (post)
data[].variation_unique string The content ID
data[].variation_no integer The number within the title (the concept for postmode)
data[].title string The title (the concept name for postmode)
data[].concept object The concept it belongs to {concept_unique, concept_name}
data[].title_unique string | null The ID of the title it belongs to (null for postmode)
data[].language string | null The publishing language (the language viewers and readers see). null if not set
data[].status string Progress. not_started / in_progress / complete (marked as complete)
data[].progress.step_process integer The completed step (0–3)
data[].progress.is_complete boolean Whether it is marked as complete
data[].is_readonly boolean Whether it is view-only because of a plan downgrade
data[].confirmed_at string | null When the content was marked as reviewed (null for postmode and when not reviewed)
data[].published_platforms array The destinations where it has been posted (success / partial_success)
data[].created_at string Created at
data[].updated_at string Updated at
meta.total integer The total number of matching items
meta.current_page integer The current page
meta.per_page integer The number of items per page
meta.last_page integer The last page
Errors
StatusCodeWhen
404 not_found The project does not exist or is outside what you can access (Project not found.)
422 validation_error The parameters are invalid
Request example
curl "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/contents?mode=blogmode&status=complete&published=false" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/contents?mode=blogmode&status=complete&published=false",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/contents?mode=blogmode&status=complete&published=false", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/contents?mode=blogmode&status=complete&published=false")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/contents?mode=blogmode&status=complete&published=false"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/contents?mode=blogmode&status=complete&published=false");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": [
    {
      "mode": "blogmode",
      "variation_unique": "bmA1b2C3d4E5",
      "variation_no": 1,
      "title": "How to start a blog",
      "concept": { "concept_unique": "bC2x8RtY4u", "concept_name": "Blogging for beginners" },
      "title_unique": "bT9kL3mN5pQr",
      "language": "en",
      "status": "complete",
      "progress": { "step_process": 3, "is_complete": true },
      "is_readonly": false,
      "confirmed_at": "2026-10-03T08:00:00Z",
      "published_platforms": [],
      "created_at": "2026-10-01T03:00:00Z",
      "updated_at": "2026-10-03T08:00:00Z"
    }
  ],
  "meta": { "total": 1, "current_page": 1, "per_page": 20, "last_page": 1 }
}
GET › /projects/{project_unique}/contents/{variation_unique}

Content detail

Gets the details of one content item. package contains what is actually posted to each destination (the same as when posting from Stepeee). For post (postmode) content, get the list of posts with the schedule.

Parameters
Name In Type Required Default Description
project_unique Path string Required — The project ID
variation_unique Path string Required — The content ID
Response fields
FieldTypeDescription
mode / variation_unique / … / updated_at — Same as the content list
publish[] object Posting results by destination
publish[].platform string The destination. youtube / tiktok / instagram / x / wordpress / webhook
publish[].status string The posting result. success (posted) / failed / scheduled (scheduled on the destination) / processing / partial_success (the post succeeded but a related step such as setting the thumbnail failed) / reviewing (the post succeeded and is waiting for the destination's review, so the URL is not yet available)
publish[].post_url string | null The URL of the post (null if not yet available)
publish[].posted_at string | null When it was posted (null if not posted)
content object Generated data by mode. blogmode: body_html (article body) / carouselmode: scenario (slide structure; resolved_header, resolved_footer, and resolved_texts are the text in the publishing language) / moviemode: movie_unique, download_url, thumbnail_url (the video selected in Step3; the URLs are valid for one hour after they are issued) / postmode: posts (post_text, post_type)
package object | null What is posted. null for postmode
package.video (moviemode) object | null The finished video {file: {url, expires_at}, thumbnail: {url, expires_at} | null, duration_ms, width, height, size_bytes}. null if there is none yet
package.youtube.title (moviemode) string | null The YouTube title
package.youtube.description (moviemode) string | null The YouTube description (including the fixed text)
package.youtube.thumbnail (moviemode) object | null The YouTube thumbnail {url, expires_at, width, height}. null for vertical videos (Shorts), which do not set a thumbnail
package.tiktok.caption (moviemode) string | null The TikTok caption
package.instagram.caption (moviemode / carouselmode) string | null The Instagram caption (including the fixed text)
package.instagram.cover (moviemode) object | null The Instagram cover image {url, expires_at, width, height}
package.x.posts (moviemode / blogmode / carouselmode) array The X posts. Two or more are posted in order as a thread
package.title (blogmode) string | null The article title
package.meta_description (blogmode) string | null The meta description
package.body_html (blogmode) string | null The HTML of the article body. Images are in the src of img tags, and affiliate codes and reviews for product introductions are already filled in. You can publish it on your site as is
package.images (blogmode) array Images in the body (in order of appearance) {url, expires_at, width, height}
package.cover_image (blogmode) object | null The cover image {url, expires_at, width, height}
package.audio (blogmode) object | null The article audio {url, expires_at, duration_ms}
package.slides[] (carouselmode) object Slide images (in slide order) {no, image: {url, expires_at, width, height}}
Errors
StatusCodeWhen
404 not_found The project does not exist or is outside what you can access (Project not found.)
404 not_found The content does not exist (Content not found.)
Request example
curl "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/contents/bmA1b2C3d4E5" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/contents/bmA1b2C3d4E5",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/contents/bmA1b2C3d4E5", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/contents/bmA1b2C3d4E5")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/contents/bmA1b2C3d4E5"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/contents/bmA1b2C3d4E5");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": {
    "mode": "blogmode",
    "variation_unique": "bmA1b2C3d4E5",
    "variation_no": 1,
    "title": "How to start a blog",
    "concept": { "concept_unique": "bC2x8RtY4u", "concept_name": "Blogging for beginners" },
    "title_unique": "bT9kL3mN5pQr",
    "language": "en",
    "status": "complete",
    "progress": { "step_process": 3, "is_complete": true },
    "is_readonly": false,
    "confirmed_at": "2026-10-03T08:00:00Z",
    "published_platforms": ["wordpress"],
    "created_at": "2026-10-01T03:00:00Z",
    "updated_at": "2026-10-03T08:00:00Z",
    "publish": [
      {
        "platform": "wordpress",
        "status": "success",
        "post_url": "https://blog.example.com/how-to-start-a-blog/",
        "posted_at": "2026-10-03T09:00:00Z"
      }
    ],
    "content": { "body_html": "<h2>Why start a blog</h2><p>...</p>" },
    "package": {
      "title": "How to Start a Blog in 2026",
      "meta_description": "A step-by-step guide to starting your first blog.",
      "body_html": "<h2>Why start a blog</h2><p>...</p><figure><img src=\"https://...\" alt=\"\" /></figure>",
      "images": [
        { "url": "https://...", "expires_at": "2026-10-21T10:00:00Z", "width": 1536, "height": 1024 }
      ],
      "cover_image": { "url": "https://...", "expires_at": "2026-10-21T10:00:00Z", "width": 1200, "height": 630 },
      "audio": null,
      "x": { "posts": ["New post: How to start a blog in 2026"] }
    }
  }
}
GET › /projects/{project_unique}/contents/{variation_unique}/schedule

Posting schedule

Gets the planned, generated, and posted posts of post (postmode) content in order of scheduled time. It also includes the automation status.

Parameters
Name In Type Required Default Description
project_unique Path string Required — The project ID
variation_unique Path string Required — The content ID
from Query string Optional — The start of the range (scheduled time). 7 days ago when omitted (37 days before to when only to is given)
to Query string Optional — The end of the range (scheduled time). 30 days from now when omitted (37 days after from when only from is given). The range can be up to 92 days
Response fields
FieldTypeDescription
automation.enabled boolean Whether automation is on
automation.is_executing boolean Whether automatic post creation is running (true even after automation is turned off while posts being created remain)
automation.week_limit integer | null The maximum number of weeks of plans created automatically (null means unlimited)
automation.week_count integer The number of weeks of plans created so far
automation.next_refill_at string | null When the next plans will be created (null when automation is off)
automation.timezone string | null The time zone used for the posting schedule
items[].schedule_unique string The schedule ID
items[].post_unique string | null The post ID (null if the text has not been created yet)
items[].scheduled_at string When the post is scheduled
items[].scheduled_at_local string The scheduled time in the local time of timezone (YYYY-MM-DDTHH:MM:SS)
items[].timezone string The time zone when the plan was created
items[].status string The status. pending (waiting for the text to be created) / generating / generated (created, waiting to be posted) / awaiting_image (waiting for the image) / posting / posted / failed / discarded (canceled)
items[].confirm_state string Review before posting. none (no review) / awaiting (not posted until approved) / notice (posted after a notification only) / confirmed (approved)
items[].created_via string How it was created. auto (automation) / manual
items[].post_type string The post type. assertion (opinion) / knowhow / experience / casual (everyday)
items[].text string | null The post text (null if it has not been created yet)
items[].thread[] object The following posts in the thread (in order) {text, post_url, posted_at, image}
items[].image object | null The attached image {url, expires_at} (null if none)
items[].post_url string | null The URL of the post (null if not yet available)
items[].posted_at string | null When it was posted (null if not posted)
items[].fail_reason string | null The reason for failure. token_expired (X disconnected) / api_error (X error) / not_found (the post target does not exist) / insufficient_token (not enough tokens) / no_x_account (no X account set) / node_ws_timeout (text creation timed out) / unconfirmed (not approved)
meta.from / meta.to string The range that was returned
Errors
StatusCodeWhen
404 not_found The project does not exist or is outside what you can access (Project not found.)
404 not_found The content is not post (postmode) content, or the content does not exist (Schedule not found.)
422 validation_error The range is longer than 92 days, to is earlier than from, or a value cannot be read as a date and time
Request example
curl "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/contents/pmV7d2KqLx01/schedule?from=2026-10-20&to=2026-11-20" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/contents/pmV7d2KqLx01/schedule?from=2026-10-20&to=2026-11-20",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/contents/pmV7d2KqLx01/schedule?from=2026-10-20&to=2026-11-20", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/contents/pmV7d2KqLx01/schedule?from=2026-10-20&to=2026-11-20")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/contents/pmV7d2KqLx01/schedule?from=2026-10-20&to=2026-11-20"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/contents/pmV7d2KqLx01/schedule?from=2026-10-20&to=2026-11-20");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": {
    "automation": {
      "enabled": true,
      "is_executing": true,
      "week_limit": 4,
      "week_count": 2,
      "next_refill_at": "2026-10-26T15:00:00Z",
      "timezone": "Asia/Tokyo"
    },
    "items": [
      {
        "schedule_unique": "Sc1aB2cD3eF4",
        "post_unique": "Po1aB2cD3eF4",
        "scheduled_at": "2026-10-20T01:00:00Z",
        "scheduled_at_local": "2026-10-20T10:00:00",
        "timezone": "Asia/Tokyo",
        "status": "posted",
        "confirm_state": "none",
        "created_via": "auto",
        "post_type": "assertion",
        "text": "Most teams overthink onboarding...",
        "thread": [
          {
            "text": "First, cut the steps in half.",
            "post_url": "https://x.com/i/status/1848000000000000002",
            "posted_at": "2026-10-20T01:00:05Z",
            "image": null
          }
        ],
        "image": { "url": "https://...", "expires_at": "2026-10-21T10:00:00Z" },
        "post_url": "https://x.com/i/status/1848000000000000001",
        "posted_at": "2026-10-20T01:00:00Z",
        "fail_reason": null
      },
      {
        "schedule_unique": "Sc9zY8xW7vU6",
        "post_unique": null,
        "scheduled_at": "2026-10-25T09:30:00Z",
        "scheduled_at_local": "2026-10-25T18:30:00",
        "timezone": "Asia/Tokyo",
        "status": "pending",
        "confirm_state": "none",
        "created_via": "auto",
        "post_type": "knowhow",
        "text": null,
        "thread": [],
        "image": null,
        "post_url": null,
        "posted_at": null,
        "fail_reason": null
      }
    ]
  },
  "meta": { "from": "2026-10-20T00:00:00Z", "to": "2026-11-20T23:59:59Z" }
}

Posting results & destinations

Get posting results for social networks, WordPress, and webhooks, and the status of the destinations connected to a project. You can use this to detect disconnected accounts.

GET › /projects/{project_unique}/publications

Posting results

Gets the posting results of a project across all modes and destinations, newest first. For post (postmode), posted and failed posts (the first post of each thread) are included.

Parameters
Name In Type Required Default Description
project_unique Path string Required — The project ID
mode Query string Optional — Filter by mode. You can pass several of moviemode (video), blogmode (blog), carouselmode (carousel), and postmode (post), separated by commas. All modes when omitted
platform Query string Optional — Filter by destination (comma-separated). youtube / tiktok / instagram / x / wordpress / webhook
status Query string Optional — Filter by posting result (comma-separated). success / failed / scheduled / processing / partial_success / reviewing
from Query string Optional — Returns only results whose posting time (the recorded time if not posted) is at or after this date and time
to Query string Optional — Returns only results whose posting time (the recorded time if not posted) is at or before this date and time
page Query integer Optional 1 The page to return (starting at 1)
per_page Query integer Optional 20 The number of items per page (1–100)
Response fields
FieldTypeDescription
data[].publication_unique string The posting result ID
data[].mode string moviemode (video) / blogmode (blog) / carouselmode (carousel) / postmode (post)
data[].content.variation_unique string The content ID
data[].content.title string | null The title (the concept name for postmode)
data[].platform string The destination. youtube / tiktok / instagram / x / wordpress / webhook
data[].account object | null The account used to post {account_unique, name}. name is null for disconnected accounts. null for postmode
data[].status string The posting result. success (posted) / failed / scheduled (scheduled on the destination) / processing / partial_success (the post succeeded but a related step such as setting the thumbnail failed) / reviewing (the post succeeded and is waiting for the destination's review, so the URL is not yet available)
data[].post_url string | null The URL of the post (null if not yet available)
data[].error_code string | null The failure reason code (differs by destination and mode)
data[].is_automation boolean Whether it was posted by an automation
data[].posted_at string | null When it was posted (null if not posted)
data[].created_at string Created at
meta.total integer The total number of matching items
meta.current_page integer The current page
meta.per_page integer The number of items per page
meta.last_page integer The last page
Errors
StatusCodeWhen
404 not_found The project does not exist or is outside what you can access (Project not found.)
422 validation_error The parameters are invalid
Request example
curl "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/publications?platform=x,youtube&status=failed" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/publications?platform=x,youtube&status=failed",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/publications?platform=x,youtube&status=failed", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/publications?platform=x,youtube&status=failed")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/publications?platform=x,youtube&status=failed"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/publications?platform=x,youtube&status=failed");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": [
    {
      "publication_unique": "Xk3p9QmZ2aLw",
      "mode": "postmode",
      "content": { "variation_unique": "pmV7d2KqLx01", "title": "SaaS growth tips" },
      "platform": "x",
      "account": null,
      "status": "success",
      "post_url": "https://x.com/i/status/1848123456789012345",
      "error_code": null,
      "is_automation": true,
      "posted_at": "2026-10-04T10:00:00Z",
      "created_at": "2026-10-03T22:10:00Z"
    },
    {
      "publication_unique": "Pq2wE4rT6yUi",
      "mode": "blogmode",
      "content": { "variation_unique": "bmA1b2C3d4E5", "title": "How to start a blog" },
      "platform": "wordpress",
      "account": { "account_unique": "Wp3xY5zA7bCd", "name": "My WP Site" },
      "status": "failed",
      "post_url": null,
      "error_code": "auth_failed",
      "is_automation": false,
      "posted_at": null,
      "created_at": "2026-10-02T10:00:00Z"
    }
  ],
  "meta": { "total": 2, "current_page": 1, "per_page": 20, "last_page": 1 }
}
GET › /projects/{project_unique}/accounts

Connected destinations

Gets the destinations connected to a project (social network accounts, WordPress, and webhooks) and their status. When status becomes expired or connection_error, you need to reconnect.

Parameters
Name In Type Required Default Description
project_unique Path string Required — The project ID
Response fields
FieldTypeDescription
data[].account_unique string The destination ID
data[].platform string The destination. youtube / tiktok / instagram / x / wordpress / webhook
data[].name string The display name. The account name for social networks, the site name for WordPress, and the domain of the destination URL for webhooks (in the form https://example.com/…, without the path)
data[].image_url string | null The URL of the account's icon image (social networks only)
data[].status string The status. active / expired (the connection expired and you need to reconnect) / connection_error (cannot connect to WordPress)
data[].token_expires_at string | null When the connection expires (social networks only)
data[].connected_at string | null When it was connected
data[].last_used_at string | null When it was last used to post (social networks only)
data[].site_url string | null The WordPress site URL (WordPress only)
Errors
StatusCodeWhen
404 not_found The project does not exist or is outside what you can access (Project not found.)
Request example
curl "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/accounts" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/accounts",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/accounts", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/accounts")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/accounts"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/projects/a1b2c3d4e5/accounts");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": [
    {
      "account_unique": "Wh9kQ2mN4pRt",
      "platform": "webhook",
      "name": "https://hooks.example.com/…",
      "image_url": null,
      "status": "active",
      "token_expires_at": null,
      "connected_at": "2026-07-01T00:00:00Z",
      "last_used_at": null,
      "site_url": null
    },
    {
      "account_unique": "Wp3xY5zA7bCd",
      "platform": "wordpress",
      "name": "Example Blog",
      "image_url": null,
      "status": "connection_error",
      "token_expires_at": null,
      "connected_at": "2026-08-01T00:00:00Z",
      "last_used_at": null,
      "site_url": "https://blog.example.com"
    },
    {
      "account_unique": "Yt4aB6cD8eFg",
      "platform": "youtube",
      "name": "Channel B",
      "image_url": "https://yt3.ggpht.com/abc.png",
      "status": "expired",
      "token_expires_at": "2026-10-01T00:00:00Z",
      "connected_at": "2026-09-01T00:00:00Z",
      "last_used_at": "2026-09-30T00:00:00Z",
      "site_url": null
    }
  ]
}

Automations

Get the progress of video, blog, and carousel automations. For post automation (post mode), use the content schedule.

GET › /automations

Automation runs

Gets video, blog, and carousel automation runs (scheduled, running, waiting for review, and finished), newest first.

Parameters
Name In Type Required Default Description
status Query string Optional — Filter by status (comma-separated). The values are the same as data[].status
mode Query string Optional — Filter by mode (comma-separated). moviemode / blogmode / carouselmode
project_unique Query string Optional — Returns only the runs of this project
from Query string Optional — Returns only items created at or after this date and time
to Query string Optional — Returns only items created at or before this date and time
page Query integer Optional 1 The page to return (starting at 1)
per_page Query integer Optional 20 The number of items per page (1–100)
Response fields
FieldTypeDescription
data[].auto_unique string The automation run ID
data[].mode string moviemode / blogmode / carouselmode
data[].project object The project {project_unique, project_name}
data[].content.variation_unique string The content ID
data[].content.title string | null The title (the concept name for postmode)
data[].run_no integer Which run this is for the content
data[].status string The status. pending (waiting to run; scheduled when reserved_at is in the future) / executing / waiting_confirmation (waiting for content review; continues after review) / completed / failed / stopped / token_shortage (ended because tokens ran out)
data[].execution_mode string sequential (one at a time) / parallel (all at once)
data[].phases object The status of each stage {setup, step1, step2, step3}. not_started / running (running, or the stage where it stopped) / done (including skipped, and, for a run that finished as completed, steps that were out of this run's scope) / error
data[].template.setup_template_name string | null The name of the setup template used at the start
data[].template.automation_template_name string | null The name of the automation template used at the start
data[].reserved_at string | null The scheduled run time
data[].executed_at string | null When the run started
data[].completed_at string | null When the run ended (for completed / failed / stopped / token_shortage)
data[].created_at string When the run was registered
meta.total integer The total number of matching items
meta.current_page integer The current page
meta.per_page integer The number of items per page
meta.last_page integer The last page
Errors
StatusCodeWhen
404 not_found The project does not exist or is outside what you can access (Project not found.)
422 validation_error The parameters are invalid
Request example
curl "https://stepeee.com/public-api/v1/automations?status=executing,waiting_confirmation" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/automations?status=executing,waiting_confirmation",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/automations?status=executing,waiting_confirmation", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/automations?status=executing,waiting_confirmation")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/automations?status=executing,waiting_confirmation"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/automations?status=executing,waiting_confirmation");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": [
    {
      "auto_unique": "aB3dE5fG7h",
      "mode": "blogmode",
      "project": { "project_unique": "Pq7Rs9Tu1V", "project_name": "My Blog" },
      "content": { "variation_unique": "Xy12Zw34Ab56", "title": "Spring travel tips" },
      "run_no": 2,
      "status": "executing",
      "execution_mode": "sequential",
      "phases": { "setup": "done", "step1": "done", "step2": "running", "step3": "not_started" },
      "template": { "setup_template_name": "Default setup", "automation_template_name": null },
      "reserved_at": null,
      "executed_at": "2026-10-19T23:09:27Z",
      "completed_at": null,
      "created_at": "2026-10-19T23:09:20Z"
    }
  ],
  "meta": { "total": 1, "current_page": 1, "per_page": 20, "last_page": 1 }
}
GET › /automations/{auto_unique}

Automation run detail

Gets the details of one automation run and the progress of each step. You can use this to check progress while it is running.

Parameters
Name In Type Required Default Description
auto_unique Path string Required — The automation run ID
Response fields
FieldTypeDescription
auto_unique / mode / … / created_at — Same as the automation run list
steps[].key string The step name (for example, step1_1_text)
steps[].phase string The stage of the step. setup / step1 / step2 / step3
steps[].status string The step status. not_run (not run, not applicable, or running) / success / error / skipped
steps[].started_at string | null When the running step started (null once it ends)
steps[].finished_at string | null When the step ended
steps[].duration_sec number | null How many seconds it took (for success / error)
steps[].error_code string | null The reason for failure. internal_error / automation_record_not_found / automation_stopped_by_admin / insufficient_token (not enough tokens) / token_group_blocked (team quota reached) / automation_auth_error (destination authentication error) / automation_not_found / automation_conflict / automation_rate_limited (destination rate limit) / automation_server_error (destination error) / automation_network_error (network error) / automation_step_failed. null when no reason was recorded
accounts[] object The destinations at the start {platform, account_unique, name}. name is null for webhooks
stop_requested boolean Whether a stop was requested
confirmation.requested_at string | null When it started waiting for review
confirmation.confirmed_at string | null When the content was reviewed (null while waiting for review or when not reviewed)
Errors
StatusCodeWhen
404 not_found The automation does not exist or is outside what you can access (Automation not found.)
Request example
curl "https://stepeee.com/public-api/v1/automations/aB3dE5fG7h" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/automations/aB3dE5fG7h",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/automations/aB3dE5fG7h", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/automations/aB3dE5fG7h")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/automations/aB3dE5fG7h"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/automations/aB3dE5fG7h");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": {
    "auto_unique": "aB3dE5fG7h",
    "mode": "blogmode",
    "project": { "project_unique": "Pq7Rs9Tu1V", "project_name": "My Blog" },
    "content": { "variation_unique": "Xy12Zw34Ab56", "title": "Spring travel tips" },
    "run_no": 2,
    "status": "failed",
    "execution_mode": "sequential",
    "phases": { "setup": "done", "step1": "done", "step2": "done", "step3": "error" },
    "template": { "setup_template_name": "Default setup", "automation_template_name": "Skip voice" },
    "reserved_at": null,
    "executed_at": "2026-10-19T23:09:27Z",
    "completed_at": "2026-10-19T23:17:50Z",
    "created_at": "2026-10-19T23:09:20Z",
    "steps": [
      { "key": "setup", "phase": "setup", "status": "success", "started_at": null, "finished_at": "2026-10-19T23:09:29Z", "duration_sec": 2.3, "error_code": null },
      { "key": "step1_1_text", "phase": "step1", "status": "success", "started_at": null, "finished_at": "2026-10-19T23:11:40Z", "duration_sec": 126.3, "error_code": null },
      { "key": "step2_2", "phase": "step2", "status": "skipped", "started_at": null, "finished_at": "2026-10-19T23:17:39Z", "duration_sec": null, "error_code": null },
      { "key": "step3_4_webhook", "phase": "step3", "status": "error", "started_at": null, "finished_at": "2026-10-19T23:17:46Z", "duration_sec": 1.2, "error_code": "automation_server_error" }
    ],
    "accounts": [
      { "platform": "webhook", "account_unique": "dxuD2gk3NYuA", "name": null }
    ],
    "stop_requested": false,
    "confirmation": { "requested_at": null, "confirmed_at": null }
  }
}

Materials & templates

Get My Materials (images, videos, and BGM) and setup and automation templates.

GET › /materials

Material list

Gets My Materials (images, videos, and BGM), newest first. On a team, it returns the materials shared by the team. Images used only inside a project and files still being created are not included.

Parameters
Name In Type Required Default Description
kind Query string Optional — Filter by material kind. image / video / bgm
type Query string Optional — Filter by material use. The values are the same as data[].type. When used with kind, only uses of that kind can be specified
category_unique Query string Optional — Returns only the materials in this category
page Query integer Optional 1 The page to return (starting at 1)
per_page Query integer Optional 20 The number of items per page (1–100)
Response fields
FieldTypeDescription
data[].material_unique string The material ID
data[].kind string The kind. image / video / bgm
data[].type string The use. image_element (element) / image_vertical (vertical) / image_horizontal (horizontal) / reference_person (reference image: person) / reference_background (reference image: background) / reference_layout (reference image: layout) / reference_cta (reference image: CTA) / reference_pet (reference image: pet) / reference_product (reference image: product or service) / image_rulebook (rulebook) / image_promotion (promotion) / video_vertical (vertical video) / video_horizontal (horizontal video) / bgm
data[].category object | null The category {category_unique, name} (null if uncategorized)
data[].width / data[].height integer | null Width and height (pixels). null for BGM and when not measured
data[].duration_ms integer | null Length (milliseconds). null for images
data[].size_bytes integer File size (bytes). It may be 0 when the material refers to a shared material
data[].file object The file itself {url, expires_at}
data[].thumbnail object | null The thumbnail {url, expires_at}. null for BGM, elements, and while the thumbnail is being created
data[].created_at string Created at
meta.total integer The total number of matching items
meta.current_page integer The current page
meta.per_page integer The number of items per page
meta.last_page integer The last page
Errors
StatusCodeWhen
422 validation_error The parameters are invalid
Request example
curl "https://stepeee.com/public-api/v1/materials?kind=image&type=image_vertical" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/materials?kind=image&type=image_vertical",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/materials?kind=image&type=image_vertical", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/materials?kind=image&type=image_vertical")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/materials?kind=image&type=image_vertical"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/materials?kind=image&type=image_vertical");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": [
    {
      "material_unique": "aB3dE5fG7hJ9kL1mN3pQ",
      "kind": "image",
      "type": "image_vertical",
      "category": { "category_unique": "Xy12Ab34Cd", "name": "Product shots" },
      "width": 768,
      "height": 1344,
      "duration_ms": null,
      "size_bytes": 1234567,
      "file": { "url": "https://...", "expires_at": "2026-10-21T11:00:00Z" },
      "thumbnail": { "url": "https://...", "expires_at": "2026-10-21T11:00:00Z" },
      "created_at": "2026-10-01T10:00:00Z"
    },
    {
      "material_unique": "Zq9Wv8Ut7Sr6Qp5On4Ml",
      "kind": "bgm",
      "type": "bgm",
      "category": null,
      "width": null,
      "height": null,
      "duration_ms": 30000,
      "size_bytes": 482133,
      "file": { "url": "https://...", "expires_at": "2026-10-21T11:00:00Z" },
      "thumbnail": null,
      "created_at": "2026-09-30T08:12:00Z"
    }
  ],
  "meta": { "total": 2, "current_page": 1, "per_page": 20, "last_page": 1 }
}
GET › /materials/{material_unique}

Material detail

Gets one material.

Parameters
Name In Type Required Default Description
material_unique Path string Required — The material ID
Response fields
FieldTypeDescription
material_unique string The material ID
kind string The kind. image / video / bgm
type string The use. image_element (element) / image_vertical (vertical) / image_horizontal (horizontal) / reference_person (reference image: person) / reference_background (reference image: background) / reference_layout (reference image: layout) / reference_cta (reference image: CTA) / reference_pet (reference image: pet) / reference_product (reference image: product or service) / image_rulebook (rulebook) / image_promotion (promotion) / video_vertical (vertical video) / video_horizontal (horizontal video) / bgm
category object | null The category {category_unique, name} (null if uncategorized)
width / height integer | null Width and height (pixels). null for BGM and when not measured
duration_ms integer | null Length (milliseconds). null for images
size_bytes integer File size (bytes). It may be 0 when the material refers to a shared material
file object The file itself {url, expires_at}
thumbnail object | null The thumbnail {url, expires_at}. null for BGM, elements, and while the thumbnail is being created
created_at string Created at
Errors
StatusCodeWhen
404 not_found The material does not exist or is outside what you can access (Material not found.)
Request example
curl "https://stepeee.com/public-api/v1/materials/aB3dE5fG7hJ9kL1mN3pQ" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/materials/aB3dE5fG7hJ9kL1mN3pQ",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/materials/aB3dE5fG7hJ9kL1mN3pQ", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/materials/aB3dE5fG7hJ9kL1mN3pQ")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/materials/aB3dE5fG7hJ9kL1mN3pQ"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/materials/aB3dE5fG7hJ9kL1mN3pQ");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": {
    "material_unique": "aB3dE5fG7hJ9kL1mN3pQ",
    "kind": "image",
    "type": "image_vertical",
    "category": { "category_unique": "Xy12Ab34Cd", "name": "Product shots" },
    "width": 768,
    "height": 1344,
    "duration_ms": null,
    "size_bytes": 1234567,
    "file": { "url": "https://...", "expires_at": "2026-10-21T11:00:00Z" },
    "thumbnail": { "url": "https://...", "expires_at": "2026-10-21T11:00:00Z" },
    "created_at": "2026-10-01T10:00:00Z"
  }
}
GET › /material-categories

Material categories

Gets material categories and the number of materials in each category.

Parameters
Name In Type Required Default Description
kind Query string Optional — Filter by material kind. image / video / bgm
type Query string Optional — Filter by material use. The values are the same as data[].type. When used with kind, only uses of that kind can be specified
Response fields
FieldTypeDescription
data[].category_unique string The category ID
data[].name string The category name
data[].kind string The kind. image / video / bgm
data[].type string The use. image_element (element) / image_vertical (vertical) / image_horizontal (horizontal) / reference_person (reference image: person) / reference_background (reference image: background) / reference_layout (reference image: layout) / reference_cta (reference image: CTA) / reference_pet (reference image: pet) / reference_product (reference image: product or service) / image_rulebook (rulebook) / image_promotion (promotion) / video_vertical (vertical video) / video_horizontal (horizontal video) / bgm
data[].is_system boolean Whether the category was created automatically by Stepeee
data[].material_count integer The number of materials in the category
Errors
StatusCodeWhen
422 validation_error The parameters are invalid
Request example
curl "https://stepeee.com/public-api/v1/material-categories?kind=image" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/material-categories?kind=image",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/material-categories?kind=image", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/material-categories?kind=image")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/material-categories?kind=image"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/material-categories?kind=image");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": [
    {
      "category_unique": "Bg12Ab34Cd",
      "name": "Lo-fi",
      "kind": "bgm",
      "type": "bgm",
      "is_system": false,
      "material_count": 3
    },
    {
      "category_unique": "Xy12Ab34Cd",
      "name": "Product shots",
      "kind": "image",
      "type": "image_vertical",
      "is_system": false,
      "material_count": 12
    }
  ]
}
GET › /templates

Template list

Gets setup templates and automation templates, most recently updated first. On a team, it returns the templates shared by the team.

Parameters
Name In Type Required Default Description
mode Query string Optional — Filter by mode. moviemode / blogmode / carouselmode / postmode
kind Query string Optional — Filter by template kind. setup / automation
page Query integer Optional 1 The page to return (starting at 1)
per_page Query integer Optional 20 The number of items per page (1–100)
Response fields
FieldTypeDescription
data[].template_unique string The template ID
data[].kind string The kind. setup (a setup template that holds the production settings) / automation (an automation template that decides which steps an automation runs)
data[].mode string moviemode (video) / blogmode (blog) / carouselmode (carousel) / postmode (post)
data[].name string The template name
data[].created_at string Created at
data[].updated_at string Updated at
meta.total integer The total number of matching items
meta.current_page integer The current page
meta.per_page integer The number of items per page
meta.last_page integer The last page
Errors
StatusCodeWhen
422 validation_error The parameters are invalid
Request example
curl "https://stepeee.com/public-api/v1/templates?kind=setup&mode=moviemode" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/templates?kind=setup&mode=moviemode",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/templates?kind=setup&mode=moviemode", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/templates?kind=setup&mode=moviemode")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/templates?kind=setup&mode=moviemode"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/templates?kind=setup&mode=moviemode");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": [
    {
      "template_unique": "Ab12Cd34Ef",
      "kind": "automation",
      "mode": "carouselmode",
      "name": "Morning carousel",
      "created_at": "2026-01-01T00:00:00Z",
      "updated_at": "2026-10-05T00:00:00Z"
    },
    {
      "template_unique": "Gh56Ij78Kl",
      "kind": "setup",
      "mode": "moviemode",
      "name": "60-second explainer",
      "created_at": "2026-01-01T00:00:00Z",
      "updated_at": "2026-10-01T00:00:00Z"
    }
  ],
  "meta": { "total": 2, "current_page": 1, "per_page": 20, "last_page": 1 }
}

Titles and content

Add titles to video, blog, and carousel concepts, and delete content. Available on the Pro plan or higher.

POST › /projects/{project_unique}/concepts/{concept_unique}/titles

Add titles

Adds titles in bulk to a video, blog, or carousel concept and creates the first piece of content for each title (the same process as adding titles manually in the app). Titles whose name already exists in the same concept, and names that appear a second time or later in the same request, are not created and are listed in skipped. A concept can have up to 100 titles; if adding would exceed 100, nothing is created (names that are skipped are not counted). If the plan's content limit prevents creating content, only the title is created and variation_unique is null. Blog promotion and affiliate settings cannot be set; titles are created as standard articles. On a team, the titles and content you create are owned by the team owner.

Parameters
Name In Type Required Default Description
project_unique Path string Required — The project ID
concept_unique Path string Required — The concept ID (a video, blog, or carousel concept in the project)
titles Body string[] Required — The titles to add. 1–50 items. Each must be 1–500 characters after trimming leading and trailing spaces (titles that are only spaces, only "0", or contain a line break in the middle are not allowed)
language Body string Required — The publishing language of the content to create (a language code such as ja, en, or fr)
setup_template_unique Body string | null Optional — The ID of the setup template to apply (template_unique from GET /templates, with kind setup and the same mode as the concept). If omitted, the concept's default template is used (or the standard setup if none is set)
Idempotency-Key Header string Optional — A unique string you generate for each action (1–255 printable ASCII characters). Resending the same content with the same key returns the first response as is (see Idempotency-Key in the conventions)
Response fields
FieldTypeDescription
created[] object The titles created (in the order sent)
created[].title_unique string The title ID
created[].title_name string The title name (with leading and trailing spaces removed)
created[].variation_unique string | null The ID of the first content created. null if it could not be created because of the plan's content limit
skipped[] object The titles not created (in the order sent)
skipped[].title_name string The title name (with leading and trailing spaces removed)
skipped[].reason string Why it was not created. duplicate (a title with the same name exists in the same concept, or the name appeared a second time or later in the same request)
Errors
StatusCodeWhen
403 plan_required The API key owner is below the Pro plan
403 team_plan_lost You are a team member and the team owner is no longer on the Team plan
403 readonly The project is read-only because of a plan downgrade
404 not_found The project does not exist or is outside your access, or the concept is not in that project
409 title_limit_reached Adding would make the concept exceed 100 titles (nothing is created). details contains limit (the limit) and total_after_add (the count after adding)
409 idempotency_request_in_progress A request with the same Idempotency-Key is still being processed
422 validation_error The parameters are invalid
422 mode_not_supported A post (post mode) or photo concept
422 template_not_found The template for setup_template_unique does not exist (including templates of another mode)
422 idempotency_key_invalid The Idempotency-Key is not 1–255 printable ASCII characters
422 idempotency_key_reused The same Idempotency-Key was used for a request with different content
Request example
curl -X POST "https://stepeee.com/public-api/v1/projects/Pq7Rs9Tu1V/concepts/Cx2Yz4Wv6Ut8/titles" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90" \
  -d '{
  "titles": ["Easy breakfast for beginners", "Meal prep in 5 minutes", "Easy breakfast for beginners"],
  "language": "en",
  "setup_template_unique": "Gh56Ij78Kl"
}'
import requests

body = """{
  "titles": ["Easy breakfast for beginners", "Meal prep in 5 minutes", "Easy breakfast for beginners"],
  "language": "en",
  "setup_template_unique": "Gh56Ij78Kl"
}"""

response = requests.request(
    "POST",
    "https://stepeee.com/public-api/v1/projects/Pq7Rs9Tu1V/concepts/Cx2Yz4Wv6Ut8/titles",
    headers={
        "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
        "Content-Type": "application/json",
        "Idempotency-Key": "5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90",
    },
    data=body,
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/projects/Pq7Rs9Tu1V/concepts/Cx2Yz4Wv6Ut8/titles", {
  method: "POST",
  headers: {
    "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
    "Content-Type": "application/json",
    "Idempotency-Key": "5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90",
  },
  body: JSON.stringify({
    "titles": ["Easy breakfast for beginners", "Meal prep in 5 minutes", "Easy breakfast for beginners"],
    "language": "en",
    "setup_template_unique": "Gh56Ij78Kl"
  }),
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/projects/Pq7Rs9Tu1V/concepts/Cx2Yz4Wv6Ut8/titles")!)
request.httpMethod = "POST"
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90", forHTTPHeaderField: "Idempotency-Key")
request.httpBody = """
{
  "titles": ["Easy breakfast for beginners", "Meal prep in 5 minutes", "Easy breakfast for beginners"],
  "language": "en",
  "setup_template_unique": "Gh56Ij78Kl"
}
""".data(using: .utf8)

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/projects/Pq7Rs9Tu1V/concepts/Cx2Yz4Wv6Ut8/titles"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .header("Content-Type", "application/json")
    .header("Idempotency-Key", "5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90")
    .method("POST", HttpRequest.BodyPublishers.ofString("""
        {
          "titles": ["Easy breakfast for beginners", "Meal prep in 5 minutes", "Easy breakfast for beginners"],
          "language": "en",
          "setup_template_unique": "Gh56Ij78Kl"
        }
        """))
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$body = <<<'JSON'
{
  "titles": ["Easy breakfast for beginners", "Meal prep in 5 minutes", "Easy breakfast for beginners"],
  "language": "en",
  "setup_template_unique": "Gh56Ij78Kl"
}
JSON;

$ch = curl_init("https://stepeee.com/public-api/v1/projects/Pq7Rs9Tu1V/concepts/Cx2Yz4Wv6Ut8/titles");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
    "Content-Type: application/json",
    "Idempotency-Key: 5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": {
    "created": [
      { "title_unique": "q8Wm2LzR0pXa", "title_name": "Easy breakfast for beginners", "variation_unique": "Hn4kT7sY1cVb" },
      { "title_unique": "Zr5uP9eK3wQd", "title_name": "Meal prep in 5 minutes", "variation_unique": "Jm8oB2fN6xLe" }
    ],
    "skipped": [
      { "title_name": "Easy breakfast for beginners", "reason": "duplicate" }
    ]
  }
}
DELETE › /projects/{project_unique}/contents/{variation_unique}

Delete content

Deletes video, blog, or carousel content (the same process as deleting content in the app). Deleted content disappears from lists and details immediately, and after delete_at (3 days later) it is permanently deleted together with the generated images, audio, and videos. The numbers of later content under the same title are shifted down by one. Content whose automation is running or scheduled cannot be deleted, so stop the automation first. The last remaining content of a title and post (post mode) content cannot be deleted.

Parameters
Name In Type Required Default Description
project_unique Path string Required — The project ID
variation_unique Path string Required — The content ID
Response fields
FieldTypeDescription
variation_unique string The ID of the deleted content
mode string moviemode (video) / blogmode (blog) / carouselmode (carousel)
delete_at string When it will be permanently deleted
Errors
StatusCodeWhen
403 plan_required The API key owner is below the Pro plan
403 team_plan_lost You are a team member and the team owner is no longer on the Team plan
403 readonly The content or project is read-only because of a plan downgrade
404 not_found The project does not exist or is outside your access, the content is not in that project, or it has already been deleted
409 automation_running An automation is running or scheduled (an automation has not finished)
409 last_variation The last remaining content of the title
422 mode_not_supported Post (post mode) content
Request example
curl -X DELETE "https://stepeee.com/public-api/v1/projects/Pq7Rs9Tu1V/contents/Hn4kT7sY1cVb" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.request(
    "DELETE",
    "https://stepeee.com/public-api/v1/projects/Pq7Rs9Tu1V/contents/Hn4kT7sY1cVb",
    headers={
        "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
    },
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/projects/Pq7Rs9Tu1V/contents/Hn4kT7sY1cVb", {
  method: "DELETE",
  headers: {
    "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
  },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/projects/Pq7Rs9Tu1V/contents/Hn4kT7sY1cVb")!)
request.httpMethod = "DELETE"
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/projects/Pq7Rs9Tu1V/contents/Hn4kT7sY1cVb"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .method("DELETE", HttpRequest.BodyPublishers.noBody())
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/projects/Pq7Rs9Tu1V/contents/Hn4kT7sY1cVb");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": {
    "variation_unique": "Hn4kT7sY1cVb",
    "mode": "blogmode",
    "delete_at": "2026-10-24T12:00:00Z"
  }
}

Automation actions

Start, reschedule, and stop video, blog, and carousel automations, and approve automations waiting for review. You can act on the same automations you can get with GET /automations. Stopping is available on any plan; everything else requires the Pro plan or higher.

POST › /automations

Start an automation

Starts automations for content (video, blog, or carousel) using the settings of an automation template. If you specify reserved_at, it is scheduled; if omitted, it runs now. All target content must be in the same project and the same mode. If even one piece of content cannot be started, nothing is registered, and the response says which content cannot be started and why. Steps already created are skipped and the run starts from the first step not yet created; steps set to skip in the automation template are not run. For content whose setup is not complete, the setup is finalized at the start. Running an automation consumes Token. The balance is not checked at the start; if it runs out during the run, the automation ends with token_shortage. With settings that require content review, the automation stops at waiting_confirmation partway through, so approve it with POST /automations/{auto_unique}/confirm. Automatic posting for posts (post mode) is a separate mechanism turned on and off per content in the app and cannot be started with this endpoint.

Parameters
Name In Type Required Default Description
variation_uniques Body string[] Required — The variation_unique of the content to automate. 1–50 items, no duplicates
automation_template_unique Body string Required — The automation template ID (template_unique from GET /templates?kind=automation, with the same mode as the content)
reserved_at Body string Optional — When to run (a future date and time). Specify it in ISO 8601 with a time offset; values without an offset are treated as UTC. If omitted, it runs now
Idempotency-Key Header string Required — A unique string you generate for each action (1–255 printable ASCII characters). Required to prevent starting twice. Resending the same content with the same key returns the first response as is (see Idempotency-Key in the conventions)
Response fields
FieldTypeDescription
data[].auto_unique string The automation run ID
data[].mode string moviemode / blogmode / carouselmode
data[].project object The project {project_unique, project_name}
data[].content.variation_unique string The content ID
data[].content.title string | null The title (the concept name for postmode)
data[].run_no integer Which run this is for the content
data[].status string The status. pending (waiting to run; scheduled when reserved_at is in the future) / executing / waiting_confirmation (waiting for content review; continues after review) / completed / failed / stopped / token_shortage (ended because tokens ran out)
data[].execution_mode string sequential (one at a time) / parallel (all at once)
data[].phases object The status of each stage {setup, step1, step2, step3}. not_started / running (running, or the stage where it stopped) / done (including skipped, and, for a run that finished as completed, steps that were out of this run's scope) / error
data[].template.setup_template_name string | null The name of the setup template used at the start
data[].template.automation_template_name string | null The name of the automation template used at the start
data[].reserved_at string | null The scheduled run time
data[].executed_at string | null When the run started
data[].completed_at string | null When the run ended (for completed / failed / stopped / token_shortage)
data[].created_at string When the run was registered
Errors
StatusCodeWhen
403 plan_required The API key owner is below the Pro plan
403 team_plan_lost You are a team member and the team owner is no longer on the Team plan
404 content_not_found A variation_unique is not among the content you can access (including content scheduled for deletion after the request was accepted; nothing is registered). details.variation_uniques contains the matching ones
404 automation_template_not_found The automation template does not exist
409 contents_not_automatable Some content cannot be started (nothing is registered). details.contents contains an array of {variation_unique, reason}. reason is automation_active (an automation has not finished) / content_complete (already complete) / content_readonly (read-only because of a plan downgrade) / layout_template_required (carousel content in a project with no layout template)
409 idempotency_request_in_progress A request with the same Idempotency-Key is still being processed
422 validation_error The parameters are invalid (0 items or 51 or more items, duplicates, reserved_at in the past, and so on)
422 mode_not_supported Post (post mode) content is included. details.variation_uniques contains the matching ones
422 mixed_modes Content of different modes is mixed
422 mixed_projects Content of different projects is mixed
422 automation_template_mode_mismatch The automation template's mode differs from the content's mode
422 idempotency_key_required The Idempotency-Key header is missing
422 idempotency_key_invalid The Idempotency-Key is not 1–255 printable ASCII characters
422 idempotency_key_reused The same Idempotency-Key was used for a request with different content
503 automation_dispatch_failed Starting the immediate run failed. The registered automations become failed. Resending with the same Idempotency-Key registers them again and starts them
Request example
curl -X POST "https://stepeee.com/public-api/v1/automations" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90" \
  -d '{
  "variation_uniques": ["Ab3dEf6hIj9k", "Lm2nOp5qRs8t"],
  "automation_template_unique": "Tm7pL2xQ9z",
  "reserved_at": "2030-01-02T12:04:05+09:00"
}'
import requests

body = """{
  "variation_uniques": ["Ab3dEf6hIj9k", "Lm2nOp5qRs8t"],
  "automation_template_unique": "Tm7pL2xQ9z",
  "reserved_at": "2030-01-02T12:04:05+09:00"
}"""

response = requests.request(
    "POST",
    "https://stepeee.com/public-api/v1/automations",
    headers={
        "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
        "Content-Type": "application/json",
        "Idempotency-Key": "5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90",
    },
    data=body,
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/automations", {
  method: "POST",
  headers: {
    "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
    "Content-Type": "application/json",
    "Idempotency-Key": "5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90",
  },
  body: JSON.stringify({
    "variation_uniques": ["Ab3dEf6hIj9k", "Lm2nOp5qRs8t"],
    "automation_template_unique": "Tm7pL2xQ9z",
    "reserved_at": "2030-01-02T12:04:05+09:00"
  }),
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/automations")!)
request.httpMethod = "POST"
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90", forHTTPHeaderField: "Idempotency-Key")
request.httpBody = """
{
  "variation_uniques": ["Ab3dEf6hIj9k", "Lm2nOp5qRs8t"],
  "automation_template_unique": "Tm7pL2xQ9z",
  "reserved_at": "2030-01-02T12:04:05+09:00"
}
""".data(using: .utf8)

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/automations"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .header("Content-Type", "application/json")
    .header("Idempotency-Key", "5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90")
    .method("POST", HttpRequest.BodyPublishers.ofString("""
        {
          "variation_uniques": ["Ab3dEf6hIj9k", "Lm2nOp5qRs8t"],
          "automation_template_unique": "Tm7pL2xQ9z",
          "reserved_at": "2030-01-02T12:04:05+09:00"
        }
        """))
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$body = <<<'JSON'
{
  "variation_uniques": ["Ab3dEf6hIj9k", "Lm2nOp5qRs8t"],
  "automation_template_unique": "Tm7pL2xQ9z",
  "reserved_at": "2030-01-02T12:04:05+09:00"
}
JSON;

$ch = curl_init("https://stepeee.com/public-api/v1/automations");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
    "Content-Type: application/json",
    "Idempotency-Key: 5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": [
    {
      "auto_unique": "Qw3eRt6yUi",
      "mode": "blogmode",
      "project": { "project_unique": "Pj8kL3mN2b", "project_name": "My Project" },
      "content": { "variation_unique": "Ab3dEf6hIj9k", "title": "My blog title" },
      "run_no": 1,
      "status": "pending",
      "execution_mode": "sequential",
      "phases": { "setup": "running", "step1": "not_started", "step2": "not_started", "step3": "not_started" },
      "template": { "setup_template_name": null, "automation_template_name": "Morning blog" },
      "reserved_at": "2030-01-02T03:04:05Z",
      "executed_at": null,
      "completed_at": null,
      "created_at": "2026-10-21T10:00:00Z"
    },
    {
      "auto_unique": "As4dFg7hJk",
      "mode": "blogmode",
      "project": { "project_unique": "Pj8kL3mN2b", "project_name": "My Project" },
      "content": { "variation_unique": "Lm2nOp5qRs8t", "title": "My blog title" },
      "run_no": 1,
      "status": "pending",
      "execution_mode": "sequential",
      "phases": { "setup": "running", "step1": "not_started", "step2": "not_started", "step3": "not_started" },
      "template": { "setup_template_name": null, "automation_template_name": "Morning blog" },
      "reserved_at": "2030-01-02T03:04:05Z",
      "executed_at": null,
      "completed_at": null,
      "created_at": "2026-10-21T10:00:00Z"
    }
  ]
}
PATCH › /automations/{auto_unique}

Change the scheduled time

Changes the run time of an automation waiting to run (status pending). Automations that have started or finished cannot be changed.

Parameters
Name In Type Required Default Description
auto_unique Path string Required — The automation run ID
reserved_at Body string Required — The new run time (a future date and time). Specify it in ISO 8601 with a time offset; values without an offset are treated as UTC
Response fields
FieldTypeDescription
auto_unique string The automation run ID
mode string moviemode / blogmode / carouselmode
project object The project {project_unique, project_name}
content.variation_unique string The content ID
content.title string | null The title (the concept name for postmode)
run_no integer Which run this is for the content
status string The status. pending (waiting to run; scheduled when reserved_at is in the future) / executing / waiting_confirmation (waiting for content review; continues after review) / completed / failed / stopped / token_shortage (ended because tokens ran out)
execution_mode string sequential (one at a time) / parallel (all at once)
phases object The status of each stage {setup, step1, step2, step3}. not_started / running (running, or the stage where it stopped) / done (including skipped, and, for a run that finished as completed, steps that were out of this run's scope) / error
template.setup_template_name string | null The name of the setup template used at the start
template.automation_template_name string | null The name of the automation template used at the start
reserved_at string | null The scheduled run time
executed_at string | null When the run started
completed_at string | null When the run ended (for completed / failed / stopped / token_shortage)
created_at string When the run was registered
Errors
StatusCodeWhen
403 plan_required The API key owner is below the Pro plan
403 team_plan_lost You are a team member and the team owner is no longer on the Team plan
404 not_found The automation does not exist or is outside what you can access (Automation not found.)
409 automation_not_pending Not waiting to run (pending); it has started or finished
422 validation_error reserved_at is missing, cannot be read as a date and time, or is in the past
Request example
curl -X PATCH "https://stepeee.com/public-api/v1/automations/Qw3eRt6yUi" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
  "reserved_at": "2031-05-06T07:08:09+09:00"
}'
import requests

body = """{
  "reserved_at": "2031-05-06T07:08:09+09:00"
}"""

response = requests.request(
    "PATCH",
    "https://stepeee.com/public-api/v1/automations/Qw3eRt6yUi",
    headers={
        "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
        "Content-Type": "application/json",
    },
    data=body,
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/automations/Qw3eRt6yUi", {
  method: "PATCH",
  headers: {
    "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "reserved_at": "2031-05-06T07:08:09+09:00"
  }),
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/automations/Qw3eRt6yUi")!)
request.httpMethod = "PATCH"
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = """
{
  "reserved_at": "2031-05-06T07:08:09+09:00"
}
""".data(using: .utf8)

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/automations/Qw3eRt6yUi"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .header("Content-Type", "application/json")
    .method("PATCH", HttpRequest.BodyPublishers.ofString("""
        {
          "reserved_at": "2031-05-06T07:08:09+09:00"
        }
        """))
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$body = <<<'JSON'
{
  "reserved_at": "2031-05-06T07:08:09+09:00"
}
JSON;

$ch = curl_init("https://stepeee.com/public-api/v1/automations/Qw3eRt6yUi");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PATCH");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
    "Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": {
    "auto_unique": "Qw3eRt6yUi",
    "mode": "blogmode",
    "project": { "project_unique": "Pj8kL3mN2b", "project_name": "My Project" },
    "content": { "variation_unique": "Ab3dEf6hIj9k", "title": "My blog title" },
    "run_no": 1,
    "status": "pending",
    "execution_mode": "sequential",
    "phases": { "setup": "running", "step1": "not_started", "step2": "not_started", "step3": "not_started" },
    "template": { "setup_template_name": null, "automation_template_name": "Morning blog" },
    "reserved_at": "2031-05-05T22:08:09Z",
    "executed_at": null,
    "completed_at": null,
    "created_at": "2026-10-21T10:00:00Z"
  }
}
POST › /automations/{auto_unique}/stop

Stop an automation

The same action as stopping in the app. Available on any plan (so you can stop it even after changing your plan). Automations waiting to run (including scheduled ones) or waiting for review become stopped immediately. For a running automation, a stop is requested: at the time of the response it is still executing, and it becomes stopped at the next break between steps (check with GET /automations/{auto_unique}). Finished automations are left as they are and their current state is returned.

Parameters
Name In Type Required Default Description
auto_unique Path string Required — The automation run ID
Response fields
FieldTypeDescription
auto_unique string The automation run ID
mode string moviemode / blogmode / carouselmode
project object The project {project_unique, project_name}
content.variation_unique string The content ID
content.title string | null The title (the concept name for postmode)
run_no integer Which run this is for the content
status string The status. pending (waiting to run; scheduled when reserved_at is in the future) / executing / waiting_confirmation (waiting for content review; continues after review) / completed / failed / stopped / token_shortage (ended because tokens ran out)
execution_mode string sequential (one at a time) / parallel (all at once)
phases object The status of each stage {setup, step1, step2, step3}. not_started / running (running, or the stage where it stopped) / done (including skipped, and, for a run that finished as completed, steps that were out of this run's scope) / error
template.setup_template_name string | null The name of the setup template used at the start
template.automation_template_name string | null The name of the automation template used at the start
reserved_at string | null The scheduled run time
executed_at string | null When the run started
completed_at string | null When the run ended (for completed / failed / stopped / token_shortage)
created_at string When the run was registered
Errors
StatusCodeWhen
404 not_found The automation does not exist or is outside what you can access (Automation not found.)
Request example
curl -X POST "https://stepeee.com/public-api/v1/automations/Qw3eRt6yUi/stop" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.request(
    "POST",
    "https://stepeee.com/public-api/v1/automations/Qw3eRt6yUi/stop",
    headers={
        "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
    },
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/automations/Qw3eRt6yUi/stop", {
  method: "POST",
  headers: {
    "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
  },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/automations/Qw3eRt6yUi/stop")!)
request.httpMethod = "POST"
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/automations/Qw3eRt6yUi/stop"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .method("POST", HttpRequest.BodyPublishers.noBody())
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/automations/Qw3eRt6yUi/stop");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": {
    "auto_unique": "Qw3eRt6yUi",
    "mode": "blogmode",
    "project": { "project_unique": "Pj8kL3mN2b", "project_name": "My Project" },
    "content": { "variation_unique": "Ab3dEf6hIj9k", "title": "My blog title" },
    "run_no": 1,
    "status": "stopped",
    "execution_mode": "sequential",
    "phases": { "setup": "running", "step1": "not_started", "step2": "not_started", "step3": "not_started" },
    "template": { "setup_template_name": null, "automation_template_name": "Morning blog" },
    "reserved_at": "2030-01-02T03:04:05Z",
    "executed_at": null,
    "completed_at": "2026-10-21T10:05:00Z",
    "created_at": "2026-10-21T10:00:00Z"
  }
}
POST › /automations/{auto_unique}/confirm

Approve an automation waiting for review

For an automation stopped waiting for content review (status waiting_confirmation), marks the content as reviewed and runs the rest (creating social media text, posting, and so on). The same action as reviewing the content in the app. On success the automation returns to pending and starts running right away.

Parameters
Name In Type Required Default Description
auto_unique Path string Required — The automation run ID
Idempotency-Key Header string Optional — A unique string you generate for each action (1–255 printable ASCII characters). Resending the same content with the same key returns the first response as is (see Idempotency-Key in the conventions)
Response fields
FieldTypeDescription
auto_unique string The automation run ID
mode string moviemode / blogmode / carouselmode
project object The project {project_unique, project_name}
content.variation_unique string The content ID
content.title string | null The title (the concept name for postmode)
run_no integer Which run this is for the content
status string The status. pending (waiting to run; scheduled when reserved_at is in the future) / executing / waiting_confirmation (waiting for content review; continues after review) / completed / failed / stopped / token_shortage (ended because tokens ran out)
execution_mode string sequential (one at a time) / parallel (all at once)
phases object The status of each stage {setup, step1, step2, step3}. not_started / running (running, or the stage where it stopped) / done (including skipped, and, for a run that finished as completed, steps that were out of this run's scope) / error
template.setup_template_name string | null The name of the setup template used at the start
template.automation_template_name string | null The name of the automation template used at the start
reserved_at string | null The scheduled run time
executed_at string | null When the run started
completed_at string | null When the run ended (for completed / failed / stopped / token_shortage)
created_at string When the run was registered
Errors
StatusCodeWhen
403 plan_required The API key owner is below the Pro plan
403 team_plan_lost You are a team member and the team owner is no longer on the Team plan
404 not_found The automation does not exist or is outside what you can access (Automation not found.)
409 automation_not_waiting_confirmation Not waiting for review (waiting_confirmation)
409 content_readonly The content is read-only because of a plan downgrade
409 idempotency_request_in_progress A request with the same Idempotency-Key is still being processed
422 idempotency_key_invalid The Idempotency-Key is not 1–255 printable ASCII characters
422 idempotency_key_reused The same Idempotency-Key was used for a request with different content
503 automation_dispatch_failed Starting the rest of the run failed. The automation becomes failed (the content stays reviewed, so start a new automation to continue)
Request example
curl -X POST "https://stepeee.com/public-api/v1/automations/Qw3eRt6yUi/confirm" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx" \
  -H "Idempotency-Key: 5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90"
import requests

response = requests.request(
    "POST",
    "https://stepeee.com/public-api/v1/automations/Qw3eRt6yUi/confirm",
    headers={
        "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
        "Idempotency-Key": "5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90",
    },
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/automations/Qw3eRt6yUi/confirm", {
  method: "POST",
  headers: {
    "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
    "Idempotency-Key": "5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90",
  },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/automations/Qw3eRt6yUi/confirm")!)
request.httpMethod = "POST"
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")
request.setValue("5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90", forHTTPHeaderField: "Idempotency-Key")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/automations/Qw3eRt6yUi/confirm"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .header("Idempotency-Key", "5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90")
    .method("POST", HttpRequest.BodyPublishers.noBody())
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/automations/Qw3eRt6yUi/confirm");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
    "Idempotency-Key: 5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": {
    "auto_unique": "Qw3eRt6yUi",
    "mode": "carouselmode",
    "project": { "project_unique": "Pj8kL3mN2b", "project_name": "My Project" },
    "content": { "variation_unique": "Ab3dEf6hIj9k", "title": "My carousel title" },
    "run_no": 1,
    "status": "pending",
    "execution_mode": "parallel",
    "phases": { "setup": "done", "step1": "done", "step2": "done", "step3": "running" },
    "template": { "setup_template_name": null, "automation_template_name": "Weekly carousel" },
    "reserved_at": null,
    "executed_at": "2026-10-21T09:00:00Z",
    "completed_at": null,
    "created_at": "2026-10-21T09:00:00Z"
  }
}

Post review

For post (post mode) automation content with pre-posting review set to required or optional, approve, post now, reschedule, or cancel posts. The targets are the same posts as in the app's review list and the links in review emails, and the same rules apply. The list is available on any plan; actions require the Pro plan or higher.

GET › /post-confirms

Posts awaiting review

Gets posts for which a review email was sent that are before their scheduled time (upcoming) or expired (expired), in order of scheduled time. upcoming posts are those awaiting approval (awaiting; not posted unless approved) and notice-only posts (notice; posted as is). expired posts are posts that passed their scheduled time without approval and were not posted; they are returned for only 30 days after the scheduled time. A thread is returned as one item for its first post, with the second and later posts in thread. Available on any plan.

Parameters
Name In Type Required Default Description
state Query string Optional — Filter by upcoming (before the scheduled time) or expired. Both if omitted
project_unique Query string Optional — Return only posts of this project
page Query integer Optional 1 The page to return (starting at 1)
per_page Query integer Optional 20 The number of items per page (1–100)
Response fields
FieldTypeDescription
data[].schedule_unique string The ID of the scheduled post. Used in the action paths
data[].state string | null upcoming (before the scheduled time) / expired. null for posts no longer under review after approval, rescheduling, and so on (only in action responses)
data[].confirm_state string awaiting (awaiting approval; not posted until approved) / notice (notice only; posted as is) / confirmed (approved) / none (no review)
data[].confirm_mode string The content's review setting. required (not posted until approved) / optional (posted even without approval) / none (no review)
data[].can_approve boolean Whether it can be approved (true when upcoming and awaiting)
data[].can_post_now boolean Whether it can be posted now (true when expired)
data[].can_reschedule boolean Whether it can be rescheduled (true when expired)
data[].scheduled_at string When the post is scheduled
data[].scheduled_at_local string The scheduled time in the local time of timezone (YYYY-MM-DDTHH:MM:SS)
data[].timezone string The time zone when the plan was created
data[].deadline_at string | null The deadline for actions. For upcoming, the scheduled time (after which it becomes expired); for expired, 30 days after the scheduled time (after which it disappears from the list and cannot be acted on). null if state is null
data[].project object The project {project_unique, project_name}
data[].concept object The concept {concept_unique, concept_name}
data[].variation_unique string The content ID
data[].post_unique string The post ID (the same as items[].post_unique in the posting schedule)
data[].post_type string The post type. assertion (opinion) / knowhow / experience / casual (everyday)
data[].text string The post text (the first post of a thread)
data[].thread[] object The second and later posts of a thread {text, image} (in order; an empty array if not a thread)
data[].image object | null The attached image {url, expires_at} (a signed URL that expires in 1 hour). null if none
meta.total integer The total number of matching items
meta.current_page integer The current page
meta.per_page integer The number of items per page
meta.last_page integer The last page
Errors
StatusCodeWhen
404 not_found The project does not exist or is outside what you can access (Project not found.)
422 validation_error The parameters are invalid
Request example
curl "https://stepeee.com/public-api/v1/post-confirms?state=upcoming" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/post-confirms?state=upcoming",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/post-confirms?state=upcoming", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/post-confirms?state=upcoming")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/post-confirms?state=upcoming"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/post-confirms?state=upcoming");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": [
    {
      "schedule_unique": "aB3dE5fG7hJ9",
      "state": "upcoming",
      "confirm_state": "awaiting",
      "confirm_mode": "required",
      "can_approve": true,
      "can_post_now": false,
      "can_reschedule": false,
      "scheduled_at": "2026-10-19T13:00:00Z",
      "scheduled_at_local": "2026-10-19T22:00:00",
      "timezone": "Asia/Tokyo",
      "deadline_at": "2026-10-19T13:00:00Z",
      "project": { "project_unique": "Pq7Rs9Tu1V", "project_name": "My brand" },
      "concept": { "concept_unique": "Cx2Yz4Wv6Ut8", "concept_name": "Morning tips" },
      "variation_unique": "Vr5Ws7Xt9Yu1",
      "post_unique": "Ps3Qt5Ru7Sv9",
      "post_type": "knowhow",
      "text": "First post of the thread",
      "thread": [
        { "text": "Second post", "image": null },
        { "text": "Third post", "image": { "url": "https://...", "expires_at": "2026-10-19T13:00:00Z" } }
      ],
      "image": null
    }
  ],
  "meta": { "total": 1, "current_page": 1, "per_page": 20, "last_page": 1 }
}
POST › /post-schedules/{schedule_unique}/approve

Approve a post

Approves a post awaiting approval (upcoming with confirm_state awaiting). Approved posts are posted at their scheduled time. For posts past their scheduled time, use post now or reschedule.

Parameters
Name In Type Required Default Description
schedule_unique Path string Required — The ID of the scheduled post (schedule_unique from GET /post-confirms)
Idempotency-Key Header string Optional — A unique string you generate for each action (1–255 printable ASCII characters). Resending the same content with the same key returns the first response as is (see Idempotency-Key in the conventions)
Response fields
FieldTypeDescription
schedule_unique / state / … / image — The same as one item in the list of posts awaiting review (the state after the action)
Errors
StatusCodeWhen
403 plan_required The API key owner is below the Pro plan
403 team_plan_lost You are a team member and the team owner is no longer on the Team plan
404 post_schedule_not_found A post that does not appear in GET /post-confirms (does not exist, outside your access, not under review, or already approved)
409 confirm_expired Past the scheduled time (use post now or reschedule)
409 confirm_not_pending Not awaiting approval (for example, a notice-only post)
409 idempotency_request_in_progress A request with the same Idempotency-Key is still being processed
422 idempotency_key_invalid The Idempotency-Key is not 1–255 printable ASCII characters
422 idempotency_key_reused The same Idempotency-Key was used for a request with different content
Request example
curl -X POST "https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9/approve" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx" \
  -H "Idempotency-Key: 5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90"
import requests

response = requests.request(
    "POST",
    "https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9/approve",
    headers={
        "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
        "Idempotency-Key": "5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90",
    },
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9/approve", {
  method: "POST",
  headers: {
    "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
    "Idempotency-Key": "5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90",
  },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9/approve")!)
request.httpMethod = "POST"
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")
request.setValue("5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90", forHTTPHeaderField: "Idempotency-Key")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9/approve"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .header("Idempotency-Key", "5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90")
    .method("POST", HttpRequest.BodyPublishers.noBody())
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9/approve");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
    "Idempotency-Key: 5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": {
    "schedule_unique": "aB3dE5fG7hJ9",
    "state": null,
    "confirm_state": "confirmed",
    "confirm_mode": "required",
    "can_approve": false,
    "can_post_now": false,
    "can_reschedule": false,
    "scheduled_at": "2026-10-19T13:00:00Z",
    "scheduled_at_local": "2026-10-19T22:00:00",
    "timezone": "Asia/Tokyo",
    "deadline_at": null,
    "project": { "project_unique": "Pq7Rs9Tu1V", "project_name": "My brand" },
    "concept": { "concept_unique": "Cx2Yz4Wv6Ut8", "concept_name": "Morning tips" },
    "variation_unique": "Vr5Ws7Xt9Yu1",
    "post_unique": "Ps3Qt5Ru7Sv9",
    "post_type": "knowhow",
    "text": "First post of the thread",
    "thread": [
      { "text": "Second post", "image": null },
      { "text": "Third post", "image": { "url": "https://...", "expires_at": "2026-10-19T13:00:00Z" } }
    ],
    "image": null
  }
}
POST › /post-schedules/{schedule_unique}/post-now

Post an expired post now

Sets an expired post to be posted 1 minute from now and marks it approved (it will be posted shortly). The second and later posts of a thread are shifted by the same amount. Can only be used for expired posts.

Parameters
Name In Type Required Default Description
schedule_unique Path string Required — The ID of the scheduled post (schedule_unique from GET /post-confirms)
allow_close_slot Body boolean Optional false true to proceed even if the gap to other scheduled posts of the same content becomes shorter than the minimum gap (90 minutes)
Idempotency-Key Header string Optional — A unique string you generate for each action (1–255 printable ASCII characters). Resending the same content with the same key returns the first response as is (see Idempotency-Key in the conventions)
Response fields
FieldTypeDescription
schedule_unique / state / … / image — The same as one item in the list of posts awaiting review (the state after the action)
Errors
StatusCodeWhen
403 plan_required The API key owner is below the Pro plan
403 team_plan_lost You are a team member and the team owner is no longer on the Team plan
403 resource_readonly The content is read-only because of a plan downgrade
404 post_schedule_not_found A post that does not appear in GET /post-confirms (does not exist, outside your access, not under review, or already approved)
409 confirm_not_expired Not expired (use approve before the scheduled time)
409 schedule_close_to_other_slot allow_close_slot is false and the gap to another scheduled post of the same content would be shorter than 90 minutes. details contains min_gap_minutes (the minimum gap in minutes), timezone, and conflicts (an array of nearby scheduled posts {scheduled_at_local, minutes_apart})
409 schedule_locked Posting or posted, or within 2 minutes of the scheduled time where it overlaps with posting
409 schedule_processing The action happened at the same time as posting and could not be applied
409 idempotency_request_in_progress A request with the same Idempotency-Key is still being processed
422 validation_error allow_close_slot is not true or false
422 idempotency_key_invalid The Idempotency-Key is not 1–255 printable ASCII characters
422 idempotency_key_reused The same Idempotency-Key was used for a request with different content
Request example
curl -X POST "https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9/post-now" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90" \
  -d '{
  "allow_close_slot": true
}'
import requests

body = """{
  "allow_close_slot": true
}"""

response = requests.request(
    "POST",
    "https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9/post-now",
    headers={
        "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
        "Content-Type": "application/json",
        "Idempotency-Key": "5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90",
    },
    data=body,
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9/post-now", {
  method: "POST",
  headers: {
    "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
    "Content-Type": "application/json",
    "Idempotency-Key": "5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90",
  },
  body: JSON.stringify({
    "allow_close_slot": true
  }),
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9/post-now")!)
request.httpMethod = "POST"
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90", forHTTPHeaderField: "Idempotency-Key")
request.httpBody = """
{
  "allow_close_slot": true
}
""".data(using: .utf8)

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9/post-now"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .header("Content-Type", "application/json")
    .header("Idempotency-Key", "5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90")
    .method("POST", HttpRequest.BodyPublishers.ofString("""
        {
          "allow_close_slot": true
        }
        """))
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$body = <<<'JSON'
{
  "allow_close_slot": true
}
JSON;

$ch = curl_init("https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9/post-now");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
    "Content-Type: application/json",
    "Idempotency-Key: 5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": {
    "schedule_unique": "aB3dE5fG7hJ9",
    "state": null,
    "confirm_state": "confirmed",
    "confirm_mode": "required",
    "can_approve": false,
    "can_post_now": false,
    "can_reschedule": false,
    "scheduled_at": "2026-10-21T12:01:00Z",
    "scheduled_at_local": "2026-10-21T21:01:00",
    "timezone": "Asia/Tokyo",
    "deadline_at": null,
    "project": { "project_unique": "Pq7Rs9Tu1V", "project_name": "My brand" },
    "concept": { "concept_unique": "Cx2Yz4Wv6Ut8", "concept_name": "Morning tips" },
    "variation_unique": "Vr5Ws7Xt9Yu1",
    "post_unique": "Ps3Qt5Ru7Sv9",
    "post_type": "knowhow",
    "text": "First post of the thread",
    "thread": [
      { "text": "Second post", "image": null },
      { "text": "Third post", "image": { "url": "https://...", "expires_at": "2026-10-19T13:00:00Z" } }
    ],
    "image": null
  }
}
PATCH › /post-schedules/{schedule_unique}

Reschedule an expired post

Changes the scheduled time of an expired post and marks it approved (it will be posted at the specified time). The second and later posts of a thread are shifted by the same amount. Only expired posts can be rescheduled.

Parameters
Name In Type Required Default Description
schedule_unique Path string Required — The ID of the scheduled post (schedule_unique from GET /post-confirms)
scheduled_at Body string Required — The new scheduled time (a future date and time). Specify it in ISO 8601 with a time offset (such as 2026-10-21T09:30:00+09:00 or 2026-10-21T00:30:00Z; seconds can be omitted and fractions of a second are truncated). It is converted to the time in the post's timezone and saved
allow_close_slot Body boolean Optional false true to proceed even if the gap to other scheduled posts of the same content becomes shorter than the minimum gap (90 minutes)
Response fields
FieldTypeDescription
schedule_unique / state / … / image — The same as one item in the list of posts awaiting review (the state after the action)
Errors
StatusCodeWhen
403 plan_required The API key owner is below the Pro plan
403 team_plan_lost You are a team member and the team owner is no longer on the Team plan
403 resource_readonly The content is read-only because of a plan downgrade
404 post_schedule_not_found A post that does not appear in GET /post-confirms (does not exist, outside your access, not under review, or already approved)
409 confirm_not_expired Not expired (use approve before the scheduled time)
409 schedule_close_to_other_slot allow_close_slot is false and the gap to another scheduled post of the same content would be shorter than 90 minutes. details contains min_gap_minutes (the minimum gap in minutes), timezone, and conflicts (an array of nearby scheduled posts {scheduled_at_local, minutes_apart})
409 schedule_locked Posting or posted, or within 2 minutes of the scheduled time where it overlaps with posting
409 schedule_processing The action happened at the same time as posting and could not be applied
422 confirm_reschedule_past scheduled_at is at or before the current time
422 validation_error scheduled_at is missing, has no time offset, or cannot be read as a date and time, or allow_close_slot is not true or false
Request example
curl -X PATCH "https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
  "scheduled_at": "2026-10-22T09:30:00-05:00",
  "allow_close_slot": false
}'
import requests

body = """{
  "scheduled_at": "2026-10-22T09:30:00-05:00",
  "allow_close_slot": false
}"""

response = requests.request(
    "PATCH",
    "https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9",
    headers={
        "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
        "Content-Type": "application/json",
    },
    data=body,
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9", {
  method: "PATCH",
  headers: {
    "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "scheduled_at": "2026-10-22T09:30:00-05:00",
    "allow_close_slot": false
  }),
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9")!)
request.httpMethod = "PATCH"
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = """
{
  "scheduled_at": "2026-10-22T09:30:00-05:00",
  "allow_close_slot": false
}
""".data(using: .utf8)

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .header("Content-Type", "application/json")
    .method("PATCH", HttpRequest.BodyPublishers.ofString("""
        {
          "scheduled_at": "2026-10-22T09:30:00-05:00",
          "allow_close_slot": false
        }
        """))
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$body = <<<'JSON'
{
  "scheduled_at": "2026-10-22T09:30:00-05:00",
  "allow_close_slot": false
}
JSON;

$ch = curl_init("https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PATCH");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
    "Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": {
    "schedule_unique": "aB3dE5fG7hJ9",
    "state": null,
    "confirm_state": "confirmed",
    "confirm_mode": "required",
    "can_approve": false,
    "can_post_now": false,
    "can_reschedule": false,
    "scheduled_at": "2026-10-22T14:30:00Z",
    "scheduled_at_local": "2026-10-22T23:30:00",
    "timezone": "Asia/Tokyo",
    "deadline_at": null,
    "project": { "project_unique": "Pq7Rs9Tu1V", "project_name": "My brand" },
    "concept": { "concept_unique": "Cx2Yz4Wv6Ut8", "concept_name": "Morning tips" },
    "variation_unique": "Vr5Ws7Xt9Yu1",
    "post_unique": "Ps3Qt5Ru7Sv9",
    "post_type": "knowhow",
    "text": "First post of the thread",
    "thread": [
      { "text": "Second post", "image": null },
      { "text": "Third post", "image": { "url": "https://...", "expires_at": "2026-10-19T13:00:00Z" } }
    ],
    "image": null
  }
}
DELETE › /post-schedules/{schedule_unique}

Cancel a post

Cancels a post (whether before its scheduled time or expired). The post, the second and later posts of the thread, and the schedule are all deleted. This cannot be undone.

Parameters
Name In Type Required Default Description
schedule_unique Path string Required — The ID of the scheduled post (schedule_unique from GET /post-confirms)
Response fields
FieldTypeDescription
schedule_unique string The ID of the canceled scheduled post
deleted boolean Always true
Errors
StatusCodeWhen
403 plan_required The API key owner is below the Pro plan
403 team_plan_lost You are a team member and the team owner is no longer on the Team plan
403 resource_readonly The content is read-only because of a plan downgrade
404 post_schedule_not_found A post that does not appear in GET /post-confirms (does not exist, outside your access, not under review, or already approved)
409 post_posting_locked Being posted (including the second and later posts of the thread) and cannot be canceled
Request example
curl -X DELETE "https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.request(
    "DELETE",
    "https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9",
    headers={
        "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
    },
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9", {
  method: "DELETE",
  headers: {
    "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
  },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9")!)
request.httpMethod = "DELETE"
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .method("DELETE", HttpRequest.BodyPublishers.noBody())
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/post-schedules/aB3dE5fG7hJ9");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": {
    "schedule_unique": "aB3dE5fG7hJ9",
    "deleted": true
  }
}

Webhooks

Sends automation and posting events as JSON to the URLs you register. Use it to learn about completions, failures, and reviews without calling the API repeatedly. Registering and test sends require the Pro plan or higher; listing and deleting are available on any plan.

GET › /webhooks

List webhook endpoints

Gets your registered endpoints in the order they were registered (up to 5; no paging). The signing secret is not included. Available on any plan.

Response fields
FieldTypeDescription
data[].webhook_unique string The endpoint ID
data[].url string The destination URL
data[].events string[] The event names to receive
data[].description string | null A note
data[].is_active boolean Whether events are currently sent. true when the owner is on the Pro plan or higher (for team members, when the team owner is on the Team plan). Events that happen while false are not sent
data[].last_delivery_at string | null When the most recent send was attempted (test sends are not included)
data[].last_delivery_status string | null The result of the most recent send. succeeded / failed. null if nothing has been sent yet
data[].last_response_status integer | null The HTTP status received in the most recent send (null if there was no response)
data[].last_error string | null Why the most recent send failed. The values are the same as error of the test send
data[].created_at string Created at
Request example
curl "https://stepeee.com/public-api/v1/webhooks" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.get(
    "https://stepeee.com/public-api/v1/webhooks",
    headers={"Authorization": "Bearer sk_xxxxxxxxxxxxxxxx"},
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/webhooks", {
  headers: { Authorization: "Bearer sk_xxxxxxxxxxxxxxxx" },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/webhooks")!)
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/webhooks"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/webhooks");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": [
    {
      "webhook_unique": "Xk3pQ9vLm2Ta",
      "url": "https://example.com/hooks/stepeee",
      "events": ["automation.completed", "post.published"],
      "description": "production hook",
      "is_active": true,
      "last_delivery_at": "2026-10-21T12:01:00Z",
      "last_delivery_status": "failed",
      "last_response_status": 503,
      "last_error": "non_2xx_response",
      "created_at": "2026-10-20T09:00:00Z"
    }
  ]
}
POST › /webhooks

Register a webhook endpoint

Registers an endpoint. The secret (signing secret) in the response is returned only in this response, and there is no way to show it later. If you lose it, delete the endpoint and register it again. You can register up to 5 endpoints. Events that happened before registration are not sent.

Parameters
Name In Type Required Default Description
url Body string Required — The destination URL (https only, port 443 only (can be omitted), up to 2048 characters). URLs containing a user name or password cannot be registered. URLs that resolve to internal or reserved addresses (private IPs, loopback, link-local, localhost, and so on) and URLs whose name cannot be resolved cannot be registered
events Body string[] Required — The event names to receive (one or more from the event list, no duplicates)
description Body string | null Optional — A note (up to 100 characters)
Idempotency-Key Header string Optional — A unique string you generate for each action (1–255 printable ASCII characters). Resending the same content with the same key returns the first response as is (see Idempotency-Key in the conventions)
Response fields
FieldTypeDescription
webhook_unique string The endpoint ID
url string The destination URL
events string[] The event names to receive
description string | null A note
is_active boolean Whether events are currently sent. true when the owner is on the Pro plan or higher (for team members, when the team owner is on the Team plan). Events that happen while false are not sent
last_delivery_at string | null When the most recent send was attempted (test sends are not included)
last_delivery_status string | null The result of the most recent send. succeeded / failed. null if nothing has been sent yet
last_response_status integer | null The HTTP status received in the most recent send (null if there was no response)
last_error string | null Why the most recent send failed. The values are the same as error of the test send
created_at string Created at
secret string The signing secret (starts with whsec_). Returned only in this response
Errors
StatusCodeWhen
403 plan_required The API key owner is below the Pro plan
403 team_plan_lost You are a team member and the team owner is no longer on the Team plan
409 webhook_limit_reached You have already registered 5
409 idempotency_request_in_progress A request with the same Idempotency-Key is still being processed
422 validation_error The input is invalid (a URL that is not https, a port other than 443, a URL containing a user name or password, a URL that resolves to an internal address or whose name cannot be resolved, an unknown event name, duplicate event names, and so on)
422 idempotency_key_invalid The Idempotency-Key is not 1–255 printable ASCII characters
422 idempotency_key_reused The same Idempotency-Key was used for a request with different content
Request example
curl -X POST "https://stepeee.com/public-api/v1/webhooks" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90" \
  -d '{
  "url": "https://example.com/hooks/stepeee",
  "events": ["automation.completed", "post.published"],
  "description": "production hook"
}'
import requests

body = """{
  "url": "https://example.com/hooks/stepeee",
  "events": ["automation.completed", "post.published"],
  "description": "production hook"
}"""

response = requests.request(
    "POST",
    "https://stepeee.com/public-api/v1/webhooks",
    headers={
        "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
        "Content-Type": "application/json",
        "Idempotency-Key": "5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90",
    },
    data=body,
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/webhooks", {
  method: "POST",
  headers: {
    "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
    "Content-Type": "application/json",
    "Idempotency-Key": "5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90",
  },
  body: JSON.stringify({
    "url": "https://example.com/hooks/stepeee",
    "events": ["automation.completed", "post.published"],
    "description": "production hook"
  }),
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/webhooks")!)
request.httpMethod = "POST"
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90", forHTTPHeaderField: "Idempotency-Key")
request.httpBody = """
{
  "url": "https://example.com/hooks/stepeee",
  "events": ["automation.completed", "post.published"],
  "description": "production hook"
}
""".data(using: .utf8)

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/webhooks"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .header("Content-Type", "application/json")
    .header("Idempotency-Key", "5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90")
    .method("POST", HttpRequest.BodyPublishers.ofString("""
        {
          "url": "https://example.com/hooks/stepeee",
          "events": ["automation.completed", "post.published"],
          "description": "production hook"
        }
        """))
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$body = <<<'JSON'
{
  "url": "https://example.com/hooks/stepeee",
  "events": ["automation.completed", "post.published"],
  "description": "production hook"
}
JSON;

$ch = curl_init("https://stepeee.com/public-api/v1/webhooks");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
    "Content-Type: application/json",
    "Idempotency-Key: 5f0c2d8e-7a41-4c1b-9d3e-2b8f6a1c4e90",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": {
    "webhook_unique": "Xk3pQ9vLm2Ta",
    "url": "https://example.com/hooks/stepeee",
    "events": ["automation.completed", "post.published"],
    "description": "production hook",
    "is_active": true,
    "last_delivery_at": null,
    "last_delivery_status": null,
    "last_response_status": null,
    "last_error": null,
    "created_at": "2026-10-21T12:00:00Z",
    "secret": "whsec_5f0c2a9e8b7d4c3a1f6e9d8c7b6a5f4e3d2c1b0a9f8e7d6c"
  }
}
DELETE › /webhooks/{webhook_unique}

Delete a webhook endpoint

Deletes an endpoint. Events not yet sent and events waiting to be retried are not sent either. Available on any plan.

Parameters
Name In Type Required Default Description
webhook_unique Path string Required — The endpoint ID
Response fields
FieldTypeDescription
webhook_unique string The ID of the deleted endpoint
deleted boolean Always true
Errors
StatusCodeWhen
404 not_found The endpoint does not exist or belongs to another user
Request example
curl -X DELETE "https://stepeee.com/public-api/v1/webhooks/Xk3pQ9vLm2Ta" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.request(
    "DELETE",
    "https://stepeee.com/public-api/v1/webhooks/Xk3pQ9vLm2Ta",
    headers={
        "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
    },
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/webhooks/Xk3pQ9vLm2Ta", {
  method: "DELETE",
  headers: {
    "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
  },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/webhooks/Xk3pQ9vLm2Ta")!)
request.httpMethod = "DELETE"
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/webhooks/Xk3pQ9vLm2Ta"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .method("DELETE", HttpRequest.BodyPublishers.noBody())
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/webhooks/Xk3pQ9vLm2Ta");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": {
    "webhook_unique": "Xk3pQ9vLm2Ta",
    "deleted": true
  }
}
POST › /webhooks/{webhook_unique}/test

Test a send

Sends a webhook.test event once on the spot and returns the result. It is sent with the same signature, headers, and destination checks as real events, so you can use it to check signature verification on the receiving side. It is not retried and is not recorded in last_delivery_at and similar fields.

Parameters
Name In Type Required Default Description
webhook_unique Path string Required — The endpoint ID
Response fields
FieldTypeDescription
delivered boolean Whether a 2xx response was received
response_status integer | null The HTTP status received (null if there was no response)
error string | null Why it failed. url_blocked (not https, a port other than 443, contains a user name and password, or just before sending it resolved to an internal address or could not be resolved) / timeout (no response within 5 seconds) / connection_failed (the connection or TLS failed) / non_2xx_response (a response other than 2xx; redirects are also treated as failures and are not followed). null on success
Errors
StatusCodeWhen
403 plan_required The API key owner is below the Pro plan
403 team_plan_lost You are a team member and the team owner is no longer on the Team plan
404 not_found The endpoint does not exist or belongs to another user
Request example
curl -X POST "https://stepeee.com/public-api/v1/webhooks/Xk3pQ9vLm2Ta/test" \
  -H "Authorization: Bearer sk_xxxxxxxxxxxxxxxx"
import requests

response = requests.request(
    "POST",
    "https://stepeee.com/public-api/v1/webhooks/Xk3pQ9vLm2Ta/test",
    headers={
        "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
    },
)
print(response.json())
const response = await fetch("https://stepeee.com/public-api/v1/webhooks/Xk3pQ9vLm2Ta/test", {
  method: "POST",
  headers: {
    "Authorization": "Bearer sk_xxxxxxxxxxxxxxxx",
  },
});
const data = await response.json();
console.log(data);
var request = URLRequest(url: URL(string: "https://stepeee.com/public-api/v1/webhooks/Xk3pQ9vLm2Ta/test")!)
request.httpMethod = "POST"
request.setValue("Bearer sk_xxxxxxxxxxxxxxxx", forHTTPHeaderField: "Authorization")

let (data, _) = try await URLSession.shared.data(for: request)
print(String(data: data, encoding: .utf8)!)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://stepeee.com/public-api/v1/webhooks/Xk3pQ9vLm2Ta/test"))
    .header("Authorization", "Bearer sk_xxxxxxxxxxxxxxxx")
    .method("POST", HttpRequest.BodyPublishers.noBody())
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
$ch = curl_init("https://stepeee.com/public-api/v1/webhooks/Xk3pQ9vLm2Ta/test");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Authorization: Bearer sk_xxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$response = curl_exec($ch);
curl_close($ch);

echo $response;
Response example
{
  "data": {
    "delivered": false,
    "response_status": 500,
    "error": "non_2xx_response"
  }
}

Events

When an event you registered for occurs, a POST is sent to the registered URL. Only events in projects the endpoint owner can access at that time are sent. Events are checked every minute, so delivery can take a few minutes. Events that happened before the endpoint was registered, and events that happened while the owner was below the Pro plan (for team members, while the team owner was not on the Team plan), are not sent.

EventMeaning
automation.waiting_confirmationAn automation (video, blog, or carousel) stopped waiting for content review
automation.completedAn automation completed to the end
automation.failedAn automation ended with failed, stopped, or token_shortage (ran out of Token)
post.publishedPosted to a social network (a video, blog, or carousel posting result became success, or a post (post mode) was posted to X). Both manual and automated posts are included
post.failedA post failed (a video, blog, or carousel posting result became failed or partial_success, or a post (post mode) was skipped because posting or text creation failed)
post.confirm_requiredA post (post mode) awaiting approval appeared in GET /post-confirms (it is not posted until approved)
webhook.testA test send (POST /webhooks/{webhook_unique}/test). It cannot be used as an event to register for

Request body

The body is JSON (UTF-8) of {id, type, created_at, data}. id is the event ID (starts with evt_), type is the event name, and created_at is when the event was recorded. When the same event is resent, the body is the same and only t in the signature changes. The shape of data for each event is as follows.

EventShape of data
automation.*The same shape as one item of data[] in GET /automations. status is waiting_confirmation for automation.waiting_confirmation, completed for automation.completed, and failed / stopped / token_shortage for automation.failed
post.published
post.failed
Video, blog, and carousel: one item of data[] in GET /projects/{project_unique}/publications plus project. For post.failed, status is failed or partial_success, and error_code contains the same failure code as the posting result. Posts (post mode): the same shape plus schedule_unique. publication_unique is the post ID (null for a schedule whose text creation failed), content.title is the concept name, account is null, and error_code is token_expired / api_error / not_found / insufficient_token / no_x_account / node_ws_timeout. Use is_automation to tell whether it was an automated post
post.confirm_requiredThe same shape as one item of data[] in GET /post-confirms, with the values at the time the event was recorded. Not sent for notice-only (notice) posts. The signed image URL expires 1 hour after recording, so if it arrives late, get it again with GET /post-confirms. If a post for the same schedule is recreated, it arrives again with a new post_unique
webhook.test{"webhook_unique": "…"}

Body examples

automation.failed
{
  "id": "evt_Q2x8Lr0aZk9MvB3pT7cYw1Nd",
  "type": "automation.failed",
  "created_at": "2026-10-21T12:01:03Z",
  "data": {
    "auto_unique": "a8Kd02LmQz",
    "mode": "blogmode",
    "project": { "project_unique": "Pj7Xc2Lq9W", "project_name": "My Project" },
    "content": { "variation_unique": "Vb3Nq8Ls1Kd0", "title": "How to start" },
    "run_no": 2,
    "status": "token_shortage",
    "execution_mode": "sequential",
    "phases": { "setup": "done", "step1": "done", "step2": "running", "step3": "not_started" },
    "template": { "setup_template_name": "Default", "automation_template_name": null },
    "reserved_at": null,
    "executed_at": "2026-10-21T11:40:00Z",
    "completed_at": "2026-10-21T12:00:41Z",
    "created_at": "2026-10-21T11:39:58Z"
  }
}
post.published
{
  "id": "evt_7hT2mWq9Zp3LxR8cVb1Nk5Ds",
  "type": "post.published",
  "created_at": "2026-10-21T12:02:10Z",
  "data": {
    "publication_unique": "Lg4Hs9Pq2Wx7",
    "mode": "moviemode",
    "project": { "project_unique": "Pj7Xc2Lq9W", "project_name": "My Project" },
    "content": { "variation_unique": "Mv8Kq2Zt5Rb1", "title": "5 tips" },
    "platform": "youtube",
    "account": { "account_unique": "Ac1Bd2Ef3Gh4", "name": "My Channel" },
    "status": "success",
    "post_url": "https://www.youtube.com/watch?v=xxxxxxxx",
    "error_code": null,
    "is_automation": true,
    "posted_at": "2026-10-21T12:01:55Z",
    "created_at": "2026-10-21T12:01:20Z"
  }
}
post.failed
{
  "id": "evt_9Zq1Lm4Xc7Vb2Nk8Ds3Pt6Wr",
  "type": "post.failed",
  "created_at": "2026-10-21T09:00:40Z",
  "data": {
    "publication_unique": "Pp5Kd8Lw2Qz9",
    "mode": "postmode",
    "project": { "project_unique": "Pj7Xc2Lq9W", "project_name": "My Project" },
    "content": { "variation_unique": "Pv4Lk9Qz2Wx6", "title": "My X concept" },
    "platform": "x",
    "account": null,
    "status": "failed",
    "post_url": null,
    "error_code": "token_expired",
    "is_automation": true,
    "posted_at": null,
    "created_at": "2026-10-20T21:00:12Z",
    "schedule_unique": "Sc6Hj1Kl8Mn3"
  }
}
post.confirm_required
{
  "id": "evt_3Kd8Lq2Wz7Xc1Vb9Nm4Pt6Rs",
  "type": "post.confirm_required",
  "created_at": "2026-10-19T08:15:02Z",
  "data": {
    "schedule_unique": "aB3dE5fG7hJ9",
    "state": "upcoming",
    "confirm_state": "awaiting",
    "confirm_mode": "required",
    "can_approve": true,
    "can_post_now": false,
    "can_reschedule": false,
    "scheduled_at": "2026-10-19T13:00:00Z",
    "scheduled_at_local": "2026-10-19T22:00:00",
    "timezone": "Asia/Tokyo",
    "deadline_at": "2026-10-19T13:00:00Z",
    "project": { "project_unique": "Pq7Rs9Tu1V", "project_name": "My brand" },
    "concept": { "concept_unique": "Cx2Yz4Wv6Ut8", "concept_name": "Morning tips" },
    "variation_unique": "Vr5Ws7Xt9Yu1",
    "post_unique": "Ps3Qt5Ru7Sv9",
    "post_type": "knowhow",
    "text": "First post of the thread",
    "thread": [
      { "text": "Second post", "image": null }
    ],
    "image": { "url": "https://...", "expires_at": "2026-10-19T09:15:02Z" }
  }
}
webhook.test
{
  "id": "evt_Q2x8Lr0aZk9MvB3pT7cYw1Nd",
  "type": "webhook.test",
  "created_at": "2026-10-21T12:00:00Z",
  "data": { "webhook_unique": "Xk3pQ9vLm2Ta" }
}

Request headers

HeaderDescription
Content-Type: application/jsonThe body format
Stepeee-Signature: t=1792584000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdThe signature. t is the time it was sent (UNIX seconds) and v1 is the signature (HMAC-SHA256 in hexadecimal). See signature verification below for how to verify it
Stepeee-Event-Id: evt_Q2x8Lr0aZk9MvB3pT7cYw1NdThe event ID (the same as id in the body). It does not change on resends, so use it to remove duplicates
Stepeee-Event-Type: automation.failedThe event name (the same as type in the body)
User-Agent: Stepeee-Webhooks/1.0The sender

Signature verification

To confirm that the body you received was sent by Stepeee and has not been altered, verify the signature as follows.

  1. Split the Stepeee-Signature header on "," and take the value of t= (UNIX seconds) and the value of v1= (64 hexadecimal characters).
  2. Using the received body as raw bytes (do not parse and rebuild the JSON), build the string of the t value, ".", and the body joined together.
  3. Compute HMAC-SHA256 with the secret you received at registration (the whole value including whsec_) as the key, and encode it as lowercase hexadecimal.
  4. Compare the computed value with v1 using a comparison whose time does not depend on the content (such as hmac.compare_digest in Python or hash_equals in PHP). If they do not match, return 400 or similar and do not process it.
  5. To prevent replays of old requests, we recommend rejecting requests whose t differs from the current time by 5 minutes or more.
  6. Use id in the body to skip events you have already processed.

Verification code examples

import hmac, hashlib, time

def verify(body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, v1 = parts.get("t"), parts.get("v1")
    if not t or not v1 or abs(time.time() - int(t)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

# Flask: verify(request.get_data(), request.headers["Stepeee-Signature"], SECRET)
const crypto = require("crypto");

function verify(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
  const t = parts.t, v1 = parts.v1;
  if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest("hex");
  return v1.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

// Express: receive the body as raw bytes with express.raw()
app.post("/hooks/stepeee", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.body, req.get("Stepeee-Signature"), process.env.STEPEEE_WEBHOOK_SECRET)) return res.sendStatus(400);
  const event = JSON.parse(req.body.toString("utf8"));
  res.sendStatus(200);
});
function verifyStepeeeSignature(string $body, string $header, string $secret, int $tolerance = 300): bool
{
    $parts = [];
    foreach (explode(',', $header) as $part) {
        [$k, $v] = array_pad(explode('=', $part, 2), 2, '');
        $parts[$k] = $v;
    }
    if (! isset($parts['t'], $parts['v1']) || abs(time() - (int) $parts['t']) > $tolerance) {
        return false;
    }
    $expected = hash_hmac('sha256', $parts['t'].'.'.$body, $secret);

    return hash_equals($expected, $parts['v1']);
}

$ok = verifyStepeeeSignature(file_get_contents('php://input'), $_SERVER['HTTP_STEPEEE_SIGNATURE'] ?? '', $secret);
# BODY: raw request body, T: t from Stepeee-Signature, SECRET: whsec_...
printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}'
# The signature is valid if the output equals v1

Success conditions and retries

If you return 2xx within 5 seconds, the event is treated as delivered. The response body is not read and redirects are not followed. Responses other than 2xx, timeouts, and connection failures are retried, with up to 8 sends including the first. The intervals before the next send are 1 minute → 5 minutes → 30 minutes → 2 hours → 4 hours → 6 hours → 10 hours, and if all 8 fail, the event is not sent. If the owner is below the Pro plan at the time of sending (for team members, if the team owner is not on the Team plan), the event is not sent. It is also not sent if the owner can no longer access the project at the time of sending (left the team, had permission removed, or the project was deleted).

Because the same event is resent until a 2xx is received, the same event may arrive two or more times. Remove duplicates with Stepeee-Event-Id (id in the body). The order of delivery is not guaranteed. Do heavy processing after returning 2xx.

Destination requirements

Destinations must be https URLs, and the port is limited to 443. URLs containing a user name or password cannot be registered either. URLs that resolve to internal or reserved addresses (private IPs, loopback, link-local, localhost, and so on) and URLs whose name cannot be resolved cannot be registered. The name is also resolved and checked just before sending, and if it resolves to an internal address, the send fails as url_blocked without being sent.