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

All requests go to:

https://www.getranklify.com/api/v1

Always 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_here

Two token types

API key (scope: api)Store connection (scope: plugin)
Created byYou, at /api-keysThe connect flow, from an official plugin
PlansAgency onlyFree, Pro, and Agency
Bound toYour accountOne store origin
EndpointsAllAll
  • 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
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

HeaderValue
AuthorizationBearer lfy_... (required)
Content-Typeapplication/json (required)

Request body

FieldTypeRequiredNotes
product_namestringYesMax 200 chars.
featuresstringYesKey features / selling points. Max 600 chars.
platformstringNoOne of amazon, shopify, woocommerce. Default amazon. Unknown values fall back to amazon.
tonestringNoOne of Professional, Energetic, Friendly, Luxury, Technical, Playful, Bold, Minimalist. Default Professional.
categorystringNoMax 100 chars.
pricestringNoFree-text price context (e.g. "$24.99 mid-range"), not a number. Max 100 chars.
customerstringNoTarget customer & use cases. Max 500 chars.
competitorsstringNoCompetitor URLs or market context. Max 300 chars.
brand_idstringNoTarget 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:

FieldTypeNotes
external_refstringYour 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-KeyheaderSee Idempotency. Strongly recommended.

Job lifecycle

queuedprocessingcomplete or failed. Only the last two are terminal; stop polling when you see one.

statusWhat you get
queuedpoll_after_ms — wait that long, then poll again.
processingSame. Generation is running.
completelisting (same shape as the sync endpoint) and listing_id.
failederror.code and error.message. The reserved credit has been refunded.
Job envelopes
// 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} every poll_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 queued rather than failed, 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.

curl
# 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 usage object.

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.

200 OK
{
  "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.remaining is the number of generations left this month. Zero means the next generate call returns usage_limit_reached.
  • account.is_member is true when the token belongs to a team member; the plan and pool reported are the workspace owner's.
  • capabilities tells you which surfaces this plan can use, so you can hide UI instead of surfacing a 403 after the user clicks.
  • brands[].id is what you pass as brand_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

  1. Your plugin sends the store owner's browser to https://www.getranklify.com/connect with site, platform, return_url, and a random state nonce you generated.
  2. They sign in (or sign up) and approve the connection. We redirect back to return_url with ?ranklify_code=...&state=....
  3. Verify the state matches the nonce you issued, then have your server POST the code to /api/v1/connect/exchange along with the same site.
  4. You get back a token. It is shown once — store it, and use it as the Bearer token on every subsequent call.
Connect flow
# 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_url must be on the same origin as site. This is what stops /connect?site=goodstore.com&return_url=evil.com from 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_code error so a prober learns nothing.
  • The site in your exchange request must match the one the code was issued for, or you get site_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.

200 OK
{
  "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

FieldAmazonShopifyWooCommerce
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." } }
StatuscodeMeaning
401missing_authorizationNo / malformed Authorization header.
401invalid_api_keyWrong format, unknown, or revoked token.
403plan_requiredAn api-scope key whose owner is not on the Agency plan. Plugin tokens never return this.
403scope_forbiddenThis token type can't reach this endpoint.
403brand_forbiddenbrand_id is not on this account.
410brand_archivedbrand_id refers to an archived brand.
400invalid_jsonBody is not valid JSON.
400validation_errorMissing required field or a field exceeds its limit.
403usage_limit_reachedMonthly generation limit hit.
404job_not_foundUnknown job_id, or it belongs to another account.
generation_failedTerminal job failure, returned inside a failed job envelope (not as an HTTP error). Credit refunded.
job_expiredA job nobody polled, swept after 20 minutes. Also a job-envelope code. Credit refunded.
400invalid_codeConnect code is invalid, expired, or already used.
400site_mismatchsite doesn't match the origin the connect code was issued for.
429rate_limitedOver 10 requests/min for this token. Retry after the Retry-After header.
500internal_errorUnexpected 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 429 with a Retry-After: 60 header.
  • Exempt from the rate limit: GET /api/v1/jobs/{id}, GET /api/v1/jobs, and GET /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 usage object reports used, limit, and plan.
  • 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 Deprecation and Sunset response headers on the retiring version.

Code samples

Synchronous

cURL
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"
  }'
JavaScript (Node 18+ / fetch)
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);
Python (requests)
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 (cURL)
<?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"];
Java (java.net.http)
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)

cURL
# 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 (cURL) — the pattern our WooCommerce plugin uses
<?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:

Changelog

v1 · 2026-08-06
  • Added POST /api/v1/jobs and GET /api/v1/jobs/{id} — async generation for clients with execution caps.
  • Added Idempotency-Key on 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/me are exempt from the rate limit.
  • New error codes: scope_forbidden, job_not_found, job_expired, invalid_code, site_mismatch.
v1 · 2026-07-14
  • Errors now return a machine-readable error.code.
  • listing is a stable superset — every field is always present (empty when not applicable to the platform).
  • Responses include api_version, and echo the resolved tone and brand_id.
  • Added optional brand_id to file a listing under a specific workspace.
v1 · initial
  • POST /api/v1/generate — Bearer auth, Agency plan, 10 req/min per key.

Questions or need a higher rate limit? Get in touch.