/ Docs
API reference
HTTP + JSON. Bearer auth with an API key from /dashboard/settings. Every endpoint here is callable with the curl example as-is — just swap rkidx_… for your real key.
Base URL: https://rapidseoindexer.com/api/v1
Authentication
All requests authenticate with a bearer token in the Authorization header. We accept two token types:
- API keys (prefix
rkidx_) — long-lived, scoped to your account. Best for servers, CI, and integrations. Generate in /dashboard/settings. - Supabase JWT— issued by our auth flow. Used by the web app; you usually don't want this for server-to-server.
API keys never expire on their own. Rotate by deleting + regenerating.
curl https://rapidseoindexer.com/api/v1/credits/balance \
-H "Authorization: Bearer rkidx_YOUR_API_KEY"Errors
Errors return a JSON body with a detail string. The HTTP status is the source of truth — body fields are for humans, not branching logic.
HTTP/1.1 401 Unauthorized
X-Request-ID: 8f9a3c4d2b1e
Content-Type: application/json
{"detail": "Invalid API key"}Common statuses:
400— invalid input401— missing or invalid token403— disabled user404— resource not found / not yours409— concurrent request with the same Idempotency-Key422— body validation failed (or same Idempotency-Key with different body)429— rate-limited500— our fault; the response body + every 5xx response header includesX-Request-ID— quote it in support tickets and we can pull the full trace.
Idempotency
POST endpoints that change state accept an Idempotency-Key header (opaque, 8–128 chars of [A-Za-z0-9_-]). The first call processes normally; subsequent calls with the same key + same body within 24h return the cached response verbatim and add Idempotent-Replayed: true.
Reusing the same key with a different body returns 422— that's almost always a client bug worth surfacing loud. Concurrent retries get 409 until the first finishes.
Recommended pattern: one UUID per request, generated by the client.
curl -X POST https://rapidseoindexer.com/api/v1/urls \
-H "Authorization: Bearer rkidx_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"urls":["https://example.com/page"],"mode":"standard"}'Submit URLs
POST/v1/urls
Submit one or more URLs to the indexing pipeline. Returns a per-URL breakdown of which were accepted, which were rejected (and why), and how many credits were charged.
Body:
urls— array of absolute URLs (1–10000). Each URL is normalized + validated before charging.mode—"standard"(1 credit) or"apex"(3 credits; adds satellite + Google Indexing API fan-out).project_id— optional UUID to group submissions.
curl -X POST https://rapidseoindexer.com/api/v1/urls \
-H "Authorization: Bearer rkidx_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7f23-batch-2026-05-19" \
-d '{
"urls": [
"https://www.etsy.com/listing/12345/my-product",
"https://www.etsy.com/listing/67890/another-product"
],
"mode": "apex"
}'{
"accepted": 2,
"rejected": 0,
"credits_charged": 6,
"items": [
{
"url": "https://www.etsy.com/listing/12345/my-product",
"accepted": true,
"submission_id": "a4f1...-...-...-b9c0",
"reason": null,
"detail": null
},
{
"url": "https://www.etsy.com/listing/67890/another-product",
"accepted": true,
"submission_id": "b5e2...-...-...-c0d1",
"reason": null,
"detail": null
}
]
}Rejection reasons include duplicate_indexed (same URL was indexed in the last 30 days), duplicate_in_progress, blacklisted, insufficient_credits. No credit is charged for rejected URLs.
List URLs
GET/v1/urls
Lists your URL submissions, newest first. Defaults to your customer URLs only — internal fan-out (gist, telegraph, pastebin) is hidden unless you opt in with include_satellites=true.
Query parameters:
limit(default 100, max soft-bounded by server)offsetstatus_filter— one ofpending,submitting,submitted,indexed,failed,refundedproject_id— UUID
curl "https://rapidseoindexer.com/api/v1/urls?status_filter=indexed&limit=50" \
-H "Authorization: Bearer rkidx_YOUR_API_KEY"Get URL detail
GET/v1/urls/{id}
Per-URL detail including computed pipeline stages (Discovery → Backlinks → Search engines → Verification). The stages are aggregated from internal logs — we deliberately don't expose the exact backlink destinations.
curl https://rapidseoindexer.com/api/v1/urls/a4f1abcd-1234-5678-90ab-cdef01234567 \
-H "Authorization: Bearer rkidx_YOUR_API_KEY"{
"id": "a4f1abcd-...",
"url": "https://www.etsy.com/listing/12345/my-product",
"domain": "www.etsy.com",
"status": "indexed",
"mode": "apex",
"credits_used": 3,
"credits_refunded": 0,
"refunded": false,
"submitted_at": "2026-05-18T14:23:01Z",
"indexed_at": "2026-05-18T15:47:33Z",
"last_checked_at": "2026-05-18T15:47:33Z",
"next_check_at": null,
"created_at": "2026-05-18T14:22:55Z",
"stages": [
{"key":"discovery","label":"Indexing pings sent","status":"done","attempts":5,"successes":4},
{"key":"backlinks","label":"Backlinks built","status":"done","attempts":12,"successes":12},
{"key":"search_engines","label":"Search engines notified","status":"done","attempts":4,"successes":3},
{"key":"verification","label":"Indexed by Google","status":"done","attempts":1,"successes":1}
]
}Credit balance
GET/v1/credits/balance
curl https://rapidseoindexer.com/api/v1/credits/balance \
-H "Authorization: Bearer rkidx_YOUR_API_KEY"{
"credits": 1180,
"lifetime_purchased": 2500,
"lifetime_spent": 1320,
"lifetime_refunded": 0
}Credit transactions
GET/v1/credits/transactions
Returns the most recent debits, refunds, purchases, and admin adjustments. Each row includes the related URL ID when applicable — useful for reconciling your invoicing against actual submissions.
curl https://rapidseoindexer.com/api/v1/credits/transactions \
-H "Authorization: Bearer rkidx_YOUR_API_KEY"List credit packs
GET/v1/credits/packs
Public — no auth required. Returns the credit packs currently for sale plus whether each payment provider is live.
curl https://rapidseoindexer.com/api/v1/credits/packsProjects
Optional grouping: tag URLs with a project_id so you can filter them in the dashboard + reports.
POST/v1/projects
curl -X POST https://rapidseoindexer.com/api/v1/projects \
-H "Authorization: Bearer rkidx_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Etsy listings — Q2 2026"}'GET/v1/projects
DELETE/v1/projects/{id}
Webhooks
Subscribe to terminal-state events on your URLs. url.indexed, url.failed, url.refunded. Bodies are HMAC-SHA256 signed with the webhook's secret.
POST/v1/webhooks
curl -X POST https://rapidseoindexer.com/api/v1/webhooks \
-H "Authorization: Bearer rkidx_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"label": "Slack inbox",
"url": "https://hooks.slack.com/services/T0/B0/secret",
"events": ["url.indexed", "url.failed"]
}'The response includes a one-time secret — store it now, it's never returned again. Use /rotate-secret to mint a new one if you lose it.
Signature verification
Every delivery includes X-Webhook-Signature: sha256=<hex>, X-Webhook-Event, and X-Webhook-Id. Verify the signature against the raw body using your stored secret:
import hmac, hashlib
from fastapi import Request, HTTPException
SECRET = "wh_secret_from_creation"
async def verify(request: Request):
body = await request.body()
expected = "sha256=" + hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
got = request.headers.get("X-Webhook-Signature", "")
if not hmac.compare_digest(expected, got):
raise HTTPException(401, "bad signature")
return bodyimport crypto from "node:crypto";
const SECRET = "wh_secret_from_creation";
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const expected = "sha256=" + crypto
.createHmac("sha256", SECRET)
.update(req.body)
.digest("hex");
const got = req.headers["x-webhook-signature"] ?? "";
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got))) {
return res.status(401).send("bad signature");
}
const payload = JSON.parse(req.body.toString());
// handle event ...
res.json({ ok: true });
});Event body shape
{
"event": "url.indexed",
"delivered_at": "2026-05-19T22:00:00Z",
"data": {
"submission_id": "a4f1...-...-...-b9c0",
"url": "https://www.etsy.com/listing/12345/my-product",
"indexed_at": "2026-05-19T21:58:42Z",
"minutes_to_index": 87.3
}
}Auto-disable
After 10 consecutive failures (non-2xx response, timeout, or network error), a webhook is automatically disabled to stop it burning worker time. Re-enable via PATCH (sets is_active: true, resets the counter) or rotate the URL if your receiver moved.
Management
GET/v1/webhooks
PATCH/v1/webhooks/{id}
POST/v1/webhooks/{id}/test
POST/v1/webhooks/{id}/rotate-secret
DELETE/v1/webhooks/{id}
The /test endpoint fires a synthetic webhook.test event so you can verify your receiver before any real URL flips through.