Zephrify

Zephrify API

Everything the engine does is available over HTTP, and this page is the contract. There is also a website: sign up at /signup, create and revoke API keys at /app/account, and manage your plan at /app/billing. It does the same things as the calls below.

Authentication

Every endpoint requires a bearer token — including reads. The entity store is the product; an unauthenticated read endpoint would be a free bulk export of it.

Authorization: Bearer zephrify_xxxxxxxxxxxxxxxxxxxx

Get a token by calling POST /register (below) or by creating a key at /app/account. Tokens are shown once, stored hashed, and expire after 30 days. Send Accept: application/json so errors come back as JSON rather than a redirect.

Base URL

https://app.zephrify.com/api/v1

Signing up

The one call that needs no token. An account is not access: it can start a subscription and read or erase its own data, and everything else answers 402 until Stripe confirms payment.

POST /register
{ "name": "Ada", "email": "ada@example.com", "password": "at least 12 characters" }

201
{
  "user":  { "id": 7, "name": "Ada", "email": "ada@example.com" },
  "token": "zephrify_...",          // shown once; only its hash is stored
  "next":  { "subscribe": "/api/v1/billing/checkout", "note": "..." }
}
CodeWhen
403Sign-up is closed.
422Invalid input, or the email is already registered. Name up to 120 characters, password 12 to 200.
429Limited per IP, per email address and per day.

Status codes you will meet

CodeMeaning
200Fine.
201Created: a site, an account, or a bookmark saved for the first time.
204Done, no body: an unsaved bookmark, an erased account.
202Accepted and queued. A publish or a build runs on a worker, not in the request.
401No token, or a token that is wrong or expired.
402Authenticated, but no active subscription. Deliberately not 403 — a 403 tells a client library to re-authenticate forever.
404Not found, or not yours. Somebody else's site returns 404 rather than 403 so ids cannot be enumerated.
409Nothing malformed; the account is simply in a state that forbids it — at its site limit, or the site is archived or not yet provisioned.
422Validation failed. The response names the field.
429Rate limited. 120 requests a minute per token.
502Account erasure only: the subscription could not be cancelled just now, so nothing was erased. Try again.
503Billing is not enabled, or no plan is configured.

Entities

The verified store: real-world things with typed attributes and a provenance record. Shared across all customers.

GET /entities            list, filterable by type, near, radius, q, limit, cursor
GET /entities/{id}       one entity, with its facts and where each came from

An entity is id, type, name, slug, lat, lon and facts: an object keyed by fact key, each with value, source (the adapter), licence and verified_at. Stale facts are withheld. Lists carry meta.count, next_cursor, has_more and attribution: whoever renders the data owes those credits. Pass next_cursor back as cursor.

Sites

A site is a row: a domain, a market, a scope and a theme. It is not an installation.

GET    /sites                 yours only
POST   /sites                 create one
GET    /sites/{site}          one site, plus what state its pages are in
PATCH  /sites/{site}          rename, re-scope, archive or un-archive
DELETE /sites/{site}          ARCHIVES. It does not destroy anything.
POST   /sites/{site}/publish  queue a publish
POST   /sites/{site}/build    plan, write what is missing, then publish

Creating a site takes a domain, a name, a market and a scope:

POST /sites
{
  "domain": "lisbon.example.com",
  "name": "Lisbon Cafes",
  "market": "cafes",
  "scope": { "types": ["poi"], "radius_km": 8 }
}

The scope takes types, record_types, root_entity_id, radius_km, descendants, all_of_type, path_style (slug or name) and up to ten filters, each { "fact", "op", "value", "types" } with op one of gte gt lte lt eq ne in exists. market, theme and type names are letters, digits, dashes and underscores only. types: [] is refused.

The domain must point at us and be verified before it can publish; the response carries the exact DNS record to add. The storage and pull zones are created by an operator, so a new site reports awaiting_provisioning: true until that has happened. At the account's site limit, creating answers 409. A site carries routes, archived, stale_urls, domain_verified and, until verified, domain_verification (the DNS record).

POST /sites/{site}/build takes an optional topic and answers 202 with routes_created, routes_retired, path_collisions and articles_queued; a scope it cannot plan is a 422.

Archiving is not deleting. A published URL that vanishes is a 404 for everyone who linked to it, so paths are retired rather than removed. Infrastructure is torn down by an operator command that asks them to type the domain.

Bookmarks

GET    /bookmarks           your saved places
POST   /bookmarks           save one
DELETE /bookmarks/{entity}  unsave (204, or 404 if not saved)

Saving twice is the same bookmark: 201 the first time, 200 after. The list carries meta.returned, total, next_cursor and attribution.

Usage

GET /usage?days=30

Your own figures only: what you hold now, and what work your account caused in the period. stored counts rows; metered reads the ledger. They disagree on purpose — regenerating an article replaces its row but is two pieces of work.

Account

GET    /account         what is held about you
GET    /account/export  everything, as JSON
DELETE /account         erase it

Erasing needs a body of { "confirm": "your@email" } matching the account's own address exactly; otherwise 422.

These are never withheld for non-payment. Access to your own data, and the ability to delete it, is not a feature of a subscription.

Billing

GET  /billing           current subscription state
POST /billing/checkout  returns a Stripe Checkout URL
GET  /billing/portal    returns a Stripe billing portal URL

How a subscription actually starts. Call POST /billing/checkout, send the customer's browser to the URL it returns, and wait. Reaching the success page proves nothing — anyone can type a URL. The subscription becomes real when Stripe's signed webhook reaches us, which is a separate request and usually lands within seconds.

So: poll GET /billing until subscribed is true. It returns billing_enabled, subscribed, status, cancelled and ends_at. Checkout and portal reply { "data": { "url": "..." } }; the portal is 404 if you have never subscribed. Cards, invoices, VAT and cancellation all live in Stripe's portal, because those are the places it is worth not reimplementing.

Rate limits

120 requests a minute, counted per token rather than per IP, so one customer behind a shared office connection cannot exhaust another's budget. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining.

What this API will not do

What the product does, and what it costs, is at zephrify.com.