Ranklify API
Version v1 · Base URL https://www.getranklify.com
The Ranklify API generates platform-optimized product listings for Amazon, Shopify, and WooCommerce. Send product details, get back a titled, keyword-rich, scored listing.
There are two ways in. API keys are created in your dashboard and require the Agency plan. Store connections are minted by the connect flow for our official plugins and work on every plan, including Free — they draw on the same monthly credit pool. Both authenticate the same way and reach the same endpoints.
- Base URL
- Authentication
- Quickstart
- Generate a listing (sync)
- Generate a listing (async jobs)
- Idempotency
- Account & usage
- Store connect flow
- Response shape
- Errors
- Rate limits & usage
- Versioning & breaking changes
- Code samples
- OpenAPI & Postman
- Changelog
Base URL
All requests go to:
https://www.getranklify.com/api/v1Always use the www host. The apex domain (https://getranklify.com) issues a 308 redirect, and many HTTP clients drop the Authorization header when following a redirect, which would fail your request. Requests must be https.
Authentication
Every endpoint authenticates the same way: a secret token in the Authorization header as a Bearer token.
Authorization: Bearer lfy_your_token_hereTwo token types
API key (scope: api) | Store connection (scope: plugin) | |
|---|---|---|
| Created by | You, at /api-keys | The connect flow, from an official plugin |
| Plans | Agency only | Free, Pro, and Agency |
| Bound to | Your account | One store origin |
| Endpoints | All | All |
- Both start with
lfy_and are shown once. Store them securely — we keep only a hash. - A token is a full credential. Keep it server-side; never ship it in client-side code or a public repo.
- Both draw on the same monthly credit pool as the dashboard. A Free plan gets 3 generations/month whether they come from the web app or a connected store.
- An API key used by a non-Agency account is rejected with
plan_required. Plugin tokens are never plan-gated — the credit pool is the limit. - Revoke either one from /api-keys. Revoking a connection disconnects that store immediately.
Quickstart
Replace lfy_YOUR_KEY with a real key and run:
curl -X POST https://www.getranklify.com/api/v1/generate \
-H "Authorization: Bearer lfy_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"product_name": "Stainless Steel Pour-Over Kettle",
"features": "Gooseneck spout, 1L, food-grade steel, keeps heat",
"platform": "amazon",
"tone": "Professional"
}'A 200 response returns a listing object plus metadata (see below).
POST /api/v1/generate (synchronous)
Generates a single optimized listing and saves it to your account history. The request stays open for the whole generation (typically 15-35s), so your client needs a timeout of at least 60 seconds.
Running inside PHP, WordPress, or anything else with a hard execution cap? Use the async jobs API instead — every request there is sub-second.
Headers
| Header | Value |
|---|---|
| Authorization | Bearer lfy_... (required) |
| Content-Type | application/json (required) |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| product_name | string | Yes | Max 200 chars. |
| features | string | Yes | Key features / selling points. Max 600 chars. |
| platform | string | No | One of amazon, shopify, woocommerce. Default amazon. Unknown values fall back to amazon. |
| tone | string | No | One of Professional, Energetic, Friendly, Luxury, Technical, Playful, Bold, Minimalist. Default Professional. |
| category | string | No | Max 100 chars. |
| price | string | No | Free-text price context (e.g. "$24.99 mid-range"), not a number. Max 100 chars. |
| customer | string | No | Target customer & use cases. Max 500 chars. |
| competitors | string | No | Competitor URLs or market context. Max 300 chars. |
| brand_id | string | No | Target workspace/brand the listing is filed under. Defaults to your active brand. Must be one you own or you get brand_forbidden. |
Unknown fields are ignored, so new optional fields can be added without breaking your integration.
POST /api/v1/jobs (asynchronous)
Queues a generation and returns 202 immediately with a job_id. You poll GET /api/v1/jobs/{id} until the status is terminal. No request stays open longer than a second, which is what makes this safe inside WordPress, cron workers, serverless functions, and anywhere else with an execution cap you don't control.
The request body is identical to /api/v1/generate, plus two optional extras:
| Field | Type | Notes |
|---|---|---|
| external_ref | string | Your own identifier (e.g. a WooCommerce product ID). Echoed back on every poll so you can match a job to a row in your database. Max 200 chars. |
| Idempotency-Key | header | See Idempotency. Strongly recommended. |
Job lifecycle
queued → processing → complete or failed. Only the last two are terminal; stop polling when you see one.
| status | What you get |
|---|---|
queued | poll_after_ms — wait that long, then poll again. |
processing | Same. Generation is running. |
complete | listing (same shape as the sync endpoint) and listing_id. |
failed | error.code and error.message. The reserved credit has been refunded. |
// 202 Accepted — job queued
{
"api_version": "v1",
"job_id": "9f3c1a2b-...",
"status": "queued",
"platform": "woocommerce",
"external_ref": "4471",
"brand_id": "a9d4...",
"poll_url": "/api/v1/jobs/9f3c1a2b-...",
"poll_after_ms": 3000,
"usage": { "used": 12, "limit": 100, "plan": "pro" }
}
// 200 OK — a later poll, once generation finished
{
"api_version": "v1",
"job_id": "9f3c1a2b-...",
"status": "complete",
"platform": "woocommerce",
"external_ref": "4471",
"listing": { "title": "...", "bullets": ["..."], "focus_keyphrase": "...", "...": "..." },
"listing_id": "b1c2..."
}Polling
- Poll
GET /api/v1/jobs/{id}everypoll_after_ms(currently 3000ms). Polling is exempt from the rate limit, so a 3-second loop is fine. - Most jobs finish in 15-35 seconds. Give up after ~2 minutes and treat it as failed on your side; the job itself will be expired and refunded automatically.
- A transient failure is retried up to 3 times without you doing anything — the job returns to
queuedrather thanfailed, and the credit stays reserved. - A credit is reserved when the job is created and refunded exactly once if it ends up
failed. You are never charged for a generation you didn't receive.
GET /api/v1/jobs
Returns your 20 most recent jobs (?limit= up to 50), newest first, in the same envelope. Useful for rebuilding a queue view after a page reload without tracking every id client-side.
# 1. Queue the job — returns immediately
curl -X POST https://www.getranklify.com/api/v1/jobs \
-H "Authorization: Bearer lfy_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: sku-4471-run1" \
-d '{
"product_name": "Stainless Steel Pour-Over Kettle",
"features": "Gooseneck spout, 1L, food-grade steel, keeps heat",
"platform": "woocommerce",
"external_ref": "4471"
}'
# → 202 { "job_id": "9f3c…", "status": "queued", "poll_after_ms": 3000 }
# 2. Poll until status is complete or failed
curl https://www.getranklify.com/api/v1/jobs/9f3c… \
-H "Authorization: Bearer lfy_YOUR_KEY"Idempotency
Send an Idempotency-Key header on every POST /api/v1/jobs. A retry carrying the same key returns the original job instead of queueing a second generation.
Idempotency-Key: sku-4471-run1- Scoped to your account and remembered for 24 hours. Max 200 chars.
- Use something stable and specific — a product ID plus a run identifier works well. Reusing a key across genuinely different products will return the wrong job.
- Without it, a network timeout on your side is indistinguishable from a failure, and the retry burns a second credit. On a Free plan that's a third of the month.
- A deduped replay does not charge a credit and does not include a
usageobject.
GET /api/v1/me
A cheap account snapshot: plan, remaining credits, what this token is, and which brands it can file listings under. Rate-limit exempt, so it's safe to call on every settings-screen render.
{
"api_version": "v1",
"account": { "email": "owner@example.com", "plan": "pro", "is_member": false },
"usage": { "used": 12, "limit": 100, "plan": "pro", "remaining": 88 },
"token": {
"scope": "plugin",
"name": "shop.example.com",
"site_url": "https://shop.example.com",
"platform": "woocommerce"
},
"brands": [{ "id": "a9d4...", "name": "Hydrosteel", "locked": false }],
"capabilities": { "generate": true, "async_jobs": true, "bulk": true },
"upgrade_url": "https://www.getranklify.com/pricing"
}usage.remainingis the number of generations left this month. Zero means the next generate call returnsusage_limit_reached.account.is_memberis true when the token belongs to a team member; the plan and pool reported are the workspace owner's.capabilitiestells you which surfaces this plan can use, so you can hide UI instead of surfacing a 403 after the user clicks.brands[].idis what you pass asbrand_id. Locked brands exist but can't be written to on the current plan.
Store connect flow
Store connections are how our official plugins get a token without the store owner ever copying and pasting a secret. It's an OAuth-style authorization-code flow: the browser only ever carries a short-lived, single-use code, and the token itself is delivered over a server-to-server request.
This works on every plan, including Free. Connections are not Agency-gated the way raw API keys are — they draw on the same monthly credit pool as the dashboard.
The flow
- Your plugin sends the store owner's browser to
https://www.getranklify.com/connectwithsite,platform,return_url, and a randomstatenonce you generated. - They sign in (or sign up) and approve the connection. We redirect back to
return_urlwith?ranklify_code=...&state=.... - Verify the
statematches the nonce you issued, then have your server POST the code to/api/v1/connect/exchangealong with the samesite. - You get back a
token. It is shown once — store it, and use it as the Bearer token on every subsequent call.
# 1. Send the store owner's browser here (from your plugin's settings screen)
https://www.getranklify.com/connect
?site=https%3A%2F%2Fshop.example.com
&platform=woocommerce
&return_url=https%3A%2F%2Fshop.example.com%2Fwp-admin%2Fadmin.php%3Fpage%3Dranklify
&state=RANDOM_NONCE
# 2. They approve, and we redirect back to return_url with:
# ?ranklify_code=ONE_TIME_CODE&state=RANDOM_NONCE
# 3. Your server (never the browser) trades the code for a token
curl -X POST https://www.getranklify.com/api/v1/connect/exchange \
-H "Content-Type: application/json" \
-d '{
"code": "ONE_TIME_CODE",
"site": "https://shop.example.com"
}'
# → 200 { "token": "lfy_...", "token_prefix": "lfy_a1b2c3d4",
# "site_url": "https://shop.example.com", "platform": "woocommerce",
# "account": { "plan": "free" },
# "usage": { "used": 0, "limit": 3, "plan": "free", "remaining": 3 } }Rules the server enforces
return_urlmust be on the same origin assite. This is what stops/connect?site=goodstore.com&return_url=evil.comfrom being an open redirect that leaks codes.- Codes expire in 10 minutes and are single-use. Expired, already-used, and never-existed all return the same
invalid_codeerror so a prober learns nothing. - The
sitein your exchange request must match the one the code was issued for, or you getsite_mismatch. - Connecting a store that's already connected revokes the previous token for that origin. One live token per store.
- Tokens are stored hashed. We cannot show one to you again — reconnect to issue a new one.
Response shape
A successful call returns 200 with the envelope below. The listing object is a stable superset: every key is always present. Fields that don't apply to the chosen platform come back empty ("" or []), so you can write one parser for all platforms.
{
"api_version": "v1",
"listing": {
"title": "HYDROSTEEL Stainless Steel Pour-Over Kettle ...",
"bullets": ["PRECISE POUR — ...", "BUILT TO LAST — ...", "..."],
"description": "<p>...</p>",
"keywords": ["pour over kettle", "gooseneck kettle", "..."],
"backend_terms": "coffee kettle, drip kettle, ...",
"focus_keyphrase": "",
"meta_description": "",
"product_tags": [],
"seo_scores": {
"keyword_density": 89,
"readability": 92,
"conversion_potential": 91,
"platform_compliance": 95
},
"pro_tip": "Add a lifestyle image showing the spout mid-pour."
},
"listing_id": "b1c2...",
"platform": "amazon",
"tone": "Professional",
"brand_id": "a9d4...",
"usage": { "used": 12, "limit": 1000, "plan": "agency" }
}Which listing fields are populated per platform
| Field | Amazon | Shopify | WooCommerce |
|---|---|---|---|
| title, description, keywords, seo_scores, pro_tip | ✓ | ✓ | ✓ |
| bullets (up to 5) | ✓ | — | ✓ |
| backend_terms (string) | ✓ | — | — |
| meta_description | — | ✓ | ✓ |
| focus_keyphrase | — | — | ✓ |
| product_tags (up to 8) | — | — | ✓ |
seo_scores holds four integers 0–100: keyword_density, readability, conversion_potential, platform_compliance.
Errors
Errors return a non-2xx status with a stable envelope. Branch on error.code, never on the human message (messages may change, codes will not).
{ "error": { "code": "invalid_api_key", "message": "Invalid or revoked API key." } }| Status | code | Meaning |
|---|---|---|
| 401 | missing_authorization | No / malformed Authorization header. |
| 401 | invalid_api_key | Wrong format, unknown, or revoked token. |
| 403 | plan_required | An api-scope key whose owner is not on the Agency plan. Plugin tokens never return this. |
| 403 | scope_forbidden | This token type can't reach this endpoint. |
| 403 | brand_forbidden | brand_id is not on this account. |
| 410 | brand_archived | brand_id refers to an archived brand. |
| 400 | invalid_json | Body is not valid JSON. |
| 400 | validation_error | Missing required field or a field exceeds its limit. |
| 403 | usage_limit_reached | Monthly generation limit hit. |
| 404 | job_not_found | Unknown job_id, or it belongs to another account. |
| — | generation_failed | Terminal job failure, returned inside a failed job envelope (not as an HTTP error). Credit refunded. |
| — | job_expired | A job nobody polled, swept after 20 minutes. Also a job-envelope code. Credit refunded. |
| 400 | invalid_code | Connect code is invalid, expired, or already used. |
| 400 | site_mismatch | site doesn't match the origin the connect code was issued for. |
| 429 | rate_limited | Over 10 requests/min for this token. Retry after the Retry-After header. |
| 500 | internal_error | Unexpected server error. Safe to retry. |
A failed job is not an HTTP error — the poll returns 200 with status: "failed" and an errorobject. Check status before assuming a 200 means success.
Rate limits & usage
- Rate limit: 10 requests per minute per token. Exceeding returns
429with aRetry-After: 60header. - Exempt from the rate limit:
GET /api/v1/jobs/{id},GET /api/v1/jobs, andGET /api/v1/me. Polling every 3 seconds would otherwise exhaust the budget in half a minute. - Usage: every successful generation counts against your plan's monthly quota, shared with the dashboard and with every connected store. The response's
usageobject reportsused,limit, andplan. - Async credits: a job reserves its credit at creation and refunds it exactly once if it ends up
failed. A deduped idempotent replay costs nothing. - Latency: generation is AI-backed and typically takes 15-35s. On
/api/v1/generate, set a client timeout of at least 60 seconds. On/api/v1/jobs, 15 seconds is plenty — every request there is sub-second.
Versioning & breaking changes
The API is versioned in the URL path (/api/v1/...). Our commitments:
- Additive changes ship without a version bump. We may add new optional request fields, new response fields, new endpoints, and new accepted enum values within
v1. Build clients to tolerate unknown fields. - Breaking changes ship as a new major version (
/v2). Breaking means removing or renaming a field, changing a field's type or meaning, making an optional field required, tightening validation, or changing the error shape. - Deprecation window: when a new major version ships, the previous version keeps working for at least 6 months.
- Notice: we announce breaking changes in advance by email to API key owners, in the changelog below, and via
DeprecationandSunsetresponse headers on the retiring version.
Code samples
Synchronous
curl -X POST https://www.getranklify.com/api/v1/generate \
-H "Authorization: Bearer lfy_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"product_name": "Stainless Steel Pour-Over Kettle",
"features": "Gooseneck spout, 1L, food-grade steel, keeps heat",
"platform": "amazon",
"tone": "Professional"
}'const res = await fetch("https://www.getranklify.com/api/v1/generate", {
method: "POST",
headers: {
"Authorization": "Bearer " + process.env.RANKLIFY_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
product_name: "Stainless Steel Pour-Over Kettle",
features: "Gooseneck spout, 1L, food-grade steel, keeps heat",
platform: "amazon",
tone: "Professional",
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(error.code + ": " + error.message);
}
const data = await res.json();
console.log(data.listing.title);import os, requests
resp = requests.post(
"https://www.getranklify.com/api/v1/generate",
headers={
"Authorization": f"Bearer {os.environ['RANKLIFY_API_KEY']}",
"Content-Type": "application/json",
},
json={
"product_name": "Stainless Steel Pour-Over Kettle",
"features": "Gooseneck spout, 1L, food-grade steel, keeps heat",
"platform": "amazon",
"tone": "Professional",
},
timeout=60,
)
if resp.status_code != 200:
err = resp.json()["error"]
raise RuntimeError(f"{err['code']}: {err['message']}")
print(resp.json()["listing"]["title"])<?php
$ch = curl_init("https://www.getranklify.com/api/v1/generate");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("RANKLIFY_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"product_name" => "Stainless Steel Pour-Over Kettle",
"features" => "Gooseneck spout, 1L, food-grade steel, keeps heat",
"platform" => "amazon",
"tone" => "Professional",
]),
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($response, true);
if ($status !== 200) {
throw new Exception($data["error"]["code"] . ": " . $data["error"]["message"]);
}
echo $data["listing"]["title"];import java.net.URI;
import java.net.http.*;
HttpClient client = HttpClient.newHttpClient();
String body = """
{
"product_name": "Stainless Steel Pour-Over Kettle",
"features": "Gooseneck spout, 1L, food-grade steel, keeps heat",
"platform": "amazon",
"tone": "Professional"
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://www.getranklify.com/api/v1/generate"))
.header("Authorization", "Bearer " + System.getenv("RANKLIFY_API_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException("Request failed: " + response.body());
}
System.out.println(response.body());Asynchronous (queue then poll)
# 1. Queue the job — returns immediately
curl -X POST https://www.getranklify.com/api/v1/jobs \
-H "Authorization: Bearer lfy_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: sku-4471-run1" \
-d '{
"product_name": "Stainless Steel Pour-Over Kettle",
"features": "Gooseneck spout, 1L, food-grade steel, keeps heat",
"platform": "woocommerce",
"external_ref": "4471"
}'
# → 202 { "job_id": "9f3c…", "status": "queued", "poll_after_ms": 3000 }
# 2. Poll until status is complete or failed
curl https://www.getranklify.com/api/v1/jobs/9f3c… \
-H "Authorization: Bearer lfy_YOUR_KEY"<?php
// Queue, then poll. No single request stays open long enough to hit a
// shared host's PHP execution limit.
function ranklify_post($path, $body, $extraHeaders = []) {
$ch = curl_init("https://www.getranklify.com" . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => array_merge([
"Authorization: Bearer " . getenv("RANKLIFY_TOKEN"),
"Content-Type: application/json",
], $extraHeaders),
CURLOPT_POSTFIELDS => json_encode($body),
]);
$res = json_decode(curl_exec($ch), true);
curl_close($ch);
return $res;
}
// Idempotency-Key makes a retried POST return the ORIGINAL job instead of
// queueing a second generation — without it a network timeout burns a credit.
$job = ranklify_post("/api/v1/jobs", [
"product_name" => "Stainless Steel Pour-Over Kettle",
"features" => "Gooseneck spout, 1L, food-grade steel, keeps heat",
"platform" => "woocommerce",
"external_ref" => "4471",
], ["Idempotency-Key: sku-4471-run1"]);
$jobId = $job["job_id"];
for ($i = 0; $i < 40; $i++) {
sleep(3);
$ch = curl_init("https://www.getranklify.com/api/v1/jobs/" . $jobId);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("RANKLIFY_TOKEN")],
]);
$poll = json_decode(curl_exec($ch), true);
curl_close($ch);
if ($poll["status"] === "complete") { echo $poll["listing"]["title"]; break; }
if ($poll["status"] === "failed") { throw new Exception($poll["error"]["code"]); }
}OpenAPI & Postman
Import the API into your tooling:
- OpenAPI 3.1 spec: /openapi.json — import into Swagger, Stoplight, or code generators.
- Postman collection: ranklify.postman_collection.json — set the
baseUrlandapiKeyvariables and send.
Changelog
- Added
POST /api/v1/jobsandGET /api/v1/jobs/{id}— async generation for clients with execution caps. - Added
Idempotency-Keyon job creation, deduped for 24 hours. - Added
GET /api/v1/me— plan, remaining credits, token info, brands. - Added the store connect flow (
/connect+/api/v1/connect/exchange) for official plugins. - Store connections work on every plan, including Free. Raw API keys remain Agency-only.
- Polling and
/api/v1/meare exempt from the rate limit. - New error codes:
scope_forbidden,job_not_found,job_expired,invalid_code,site_mismatch.
- Errors now return a machine-readable
error.code. listingis a stable superset — every field is always present (empty when not applicable to the platform).- Responses include
api_version, and echo the resolvedtoneandbrand_id. - Added optional
brand_idto file a listing under a specific workspace.
POST /api/v1/generate— Bearer auth, Agency plan, 10 req/min per key.
Questions or need a higher rate limit? Get in touch.