Purlock

API documentation

Everything takes a key, answers in JSON, and uses plain HTTP, so it works from any language.

Get your API key

Your key

Send your key on every call. Keep it on your server: anyone with it can use your allowance.

curl "https://www.purlock.com/v1/tools/email-deliverability-checker?target=yourshop.com" \
  -H "Authorization: Bearer pk_live_..."

A key can read, or read and write. To use facts, scans or prechecks, tie the key to a site when you create it.

Limits

PlanCredits a monthCalls a minute
Free10020
Individual5,000120
Business25,000300

Most calls cost 1 credit, a precheck 5 and a full scan 20. Each answer has x-credits-used and x-credits-limit headers.

Errors

Errors always look the same, so your code can check the code and show the message.

{ "error": { "code": "out_of_credits", "message": "You've used this month's API calls." } }

Tools

GET /v1/tools/{tool}?target=...

Runs one tool. status is good, attention or bad. Each check has a level, title, detail and fix. data has the same answer in full.
{
  "tool": "email-deliverability",
  "target": "yourshop.com",
  "status": "attention",
  "headline": "yourshop.com works, with 1 thing worth fixing.",
  "checks": [{ "id": "spf-near-limit", "level": "warn", "title": "SPF uses 8 of 10 DNS lookups: close to the limit", "fix": "..." }],
  "facts": [{ "label": "SPF lookups", "value": "8 of 10" }],
  "data": { "spf": { "lookups": 8, "costs": [] } },
  "checkedAt": "2026-11-05T09:00:00.000Z"
}

GET /v1/favicon?target=example.com&redirect=1

The icon's address, or with redirect=1 a redirect to it.

Facts

The values behind Live Facts. Change one, and every page using its tag shows the new value within seconds.

GET /v1/facts

Every fact on the key's site.

GET /v1/facts/{key}

One fact.

PUT /v1/facts/{key}

Needs a key that can write, and a verified site. It also tells you how many pages still type the old value by hand.
curl -X PUT "https://www.purlock.com/v1/facts/pro-price" \
  -H "Authorization: Bearer pk_live_..." \
  -H "content-type: application/json" \
  -d '{ "value": "$29/month" }'

Prices

Link a fact to a Stripe lookup key on your Live facts page, and Stripe decides what it shows. With Stripe connected, changes arrive straight away. With a pasted key, we check every day and after every sale.

GET /v1/prices/{lookup_key}

The current price, ready to show, from our records, so it's fast and doesn't use your Stripe rate limit. Includes the lowest price of the last 30 days, once we have 30 days of history for it.
{ "lookupKey": "pro_monthly", "amount": 2900, "currency": "usd", "interval": "month", "formatted": "$29/month", "lowest30Days": 1900, "lowest30DaysFormatted": "$19/month" }

Plan status

What a customer is on right now, kept current by Stripe's webhooks, so it's fast enough to check on every request. Connect Stripe on your site's Live facts page first.

GET /v1/customers/{stripe_customer_id}/plan

Set limits in Stripe, in your product's metadata: limit_seats = 5 comes back as seats: 5. Someone with no subscription gets active: false, not an error. Cache the answer, and decide what your app does if we can't be reached.
{
  "customer": "cus_123",
  "active": true,
  "status": "active",
  "plan": { "name": "Pro", "lookupKey": "pro_monthly", "priceId": "price_123" },
  "limits": { "seats": 5, "sso": true },
  "renewsAt": "2026-12-05T00:00:00.000Z",
  "endsAt": null,
  "trialEndsAt": null
}

Scans and prechecks

These come with the Individual and Business plans, on a verified site.

POST /v1/scan

Starts a full check of the key's site, and returns its id. Run it after each deploy.

GET /v1/scan/{id}

Its progress, and its findings once it's done.

POST /v1/precheck

Checks a draft against your Fact Sheet before it goes live. Send { "text": "..." } or { "url": "https://preview..." }.

Webhooks

Add one on your API keys page. We send finding.created, finding.resolved and fact.changed for a site, and monitor.changed for monitors. Check each one is really from us:

import { createHmac, timingSafeEqual } from "node:crypto";

function isFromPurlock(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // Too old: a replay.
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}

Live Facts in React

Live Facts needs no key: its values are public. Keep the current value inside the tag, so visitors still see it if the script can't run.

<script src="https://www.purlock.com/live/v1.js" data-site="YOUR_SITE_ID" async></script>

<span data-purlock="pro-price" suppressHydrationWarning>$19/month</span>