The Peerseal developer reference.
Peersealis a portable Trust Score built from peer vouches, verified purchases, eyewitness confirmations, and identity-verified ownership. This page covers the three integration surfaces any partner needs — the unauthed browser badge, the partner-call Verify API, and the in-app sign-in flow — grounded in the actual route handlers that ship in this repo.
01 — Getting started
What Peerseal is.
A portable, deterministic Trust Score a partner can resolve to a single integer in the 0–100 range — the same scored bundle the public /u/<handle> page and the unauthed browser badge read.
A Trust Score is a 0–100 integer computed from four attestation kinds: verified purchase receipts, named eyewitness vouches, identity-verified ownership, and continuous-recording corroboration. The full weights live on /methodology; for partners, the only contract that matters is that the score is a single integer accompanied by a four-item breakdown and a list of earned labels.
The same scored bundle powers three different surfaces depending on who is asking. Third-party pages run the unauthed 3-kilobyte badge snippet below — no key, no SDK. Marketplace back ends call /api/lookup with a partner key for a slim server-to-server payload. The full public profile (display name, post list, sparkline) is reachable unauthed at /api/users/<handle> for any page that wants to render its own card.
<!-- Mount point: where the badge renders -->
<div data-peerseal-handle="demo0001"></div>
<!-- Script: idempotent — multiple mount divs each get their own badge -->
<script src="https://getpeerseal.com/embed/badge/demo0001.js"></script>02 — Authentication
Sign-in is the in-app form.
The /verify and /mvp surfaces read a session cookie set by the sign-in form below. Anonymous reads are fine; an authenticated viewer unlocks a Subscribe panel on any profile that isn't their own.
Peerseal ships an in-app sign-in form on /login and the matching /signup. Both pages talk to the framework-owned /api/auth/**routes — a partner integrating against the Verify API does NOT need to sign in; the sign-in form is for users of Peerseal itself.
New here? Create an account
The session cookie is the contract for /api/users/<handle>: when the cookie is present and the viewer is not the profile owner, the response sets viewerCanSubscribe: true and the page renders a Subscribe pane. When the cookie is missing or the viewer is the owner, the field is false and the pane is hidden. The server computes this; the client never has to compare creator ids.
The /api/lookupverify endpoint is independent of session state — it reads a partner key from x-api-key, not a cookie, so back-end integrations work cleanly without any user signed in.
03 — Verify api
Resolve a handle’s score from your back end.
The server-to-server verify endpoint. One handle, one x-api-keyheader, one slim JSON response — that is the whole surface.
GET https://getpeerseal.com/api/lookupaccepts a single required query parameter, handle, and a single required header, x-api-key: $PEERSEAL_API_KEY. The handle is the first 8 characters of the user’s User.id (a cuid prefix), validated server-side as ^[a-z0-9]+$ up to 64 characters; an invalid or empty handle returns 400.
- missing or revoked key401 Unauthorized
- handle empty / > 64 / wrong shape400 Bad Request
- prefix hits 0 or > 1 user404 Not Found
- exactly one resolved user200 OK + LookupResponse
The endpoint is force-dynamic and ships no Cache-Controlheader — every request is a fresh database read, so the partner can call it on whatever cadence it needs and trust the result. Cache the response on your side if you want to throttle outbound traffic.
curl -sS "https://getpeerseal.com/api/lookup" \
-G \
--data-urlencode "handle=demo0001" \
-H "x-api-key: $PEERSEAL_API_KEY"// Resolve a handle to its Trust Score server-side and render the
// result into your own listing UI. The partner key MUST stay on the
// server — never expose it to the browser.
const siteUrl = process.env.PEERSEAL_SITE_URL; // e.g. https://getpeerseal.com
const apiKey = process.env.PEERSEAL_API_KEY;
const handle = "demo0001";
const url = new URL("/api/lookup", siteUrl);
url.searchParams.set("handle", handle);
const res = await fetch(url, {
headers: { "x-api-key": apiKey, accept: "application/json" },
});
if (!res.ok) throw new Error(`lookup ${res.status}`);
const { score, breakdown, badges, totals } = await res.json();
// Render in your markup:
// <span class="trust-score">Trust Score {score}/100</span>
// ...iterate {breakdown} for the per-kind contribution chips...
// ...iterate {badges} for the earned-badge chip strip...{
"handle": "demo0001",
"score": 78,
"topBadge": "Vouched by witnesses",
"attestationCount": 4,
"url": "https://getpeerseal.com/u/demo0001"
}The LookupResponseslims the data to the scored bundle only — never the display name, image, joinedAt, post list, or sparkline. The slim posture minimizes disclosure even when the caller’s API key is valid. If the partner needs the full public profile (display name, post list, sparkline, viewer flags) it can also call the unauthed /api/users/<handle>endpoint directly — the handle is the auth surface there.
{
"header": {
"handle": "demo0001",
"displayName": "Ada Mun",
"image": null,
"joinedAt": "2024-04-12T14:02:00.000Z"
},
"score": {
"score": 78,
"badges": [
"Vouched by witnesses",
"Purchased by author"
],
"breakdown": {
"receipt": true,
"eyewitness": true,
"identityVerified": false,
"continuousRecording": true,
"receiptPoints": 55,
"eyewitnessPoints": 40,
"identityVerifiedPoints": 0,
"continuousRecordingPoints": 20
}
},
"totals": {
"receipts": 3,
"eyewitness": 5,
"identityVerified": 0,
"continuousRecording": 2
},
"sparkline": [
[
40,
"2024-05-01T00:00:00.000Z"
],
[
62,
"2024-06-14T00:00:00.000Z"
],
[
78,
"2024-08-04T12:34:56.000Z"
]
],
"posts": [],
"viewer": null,
"viewerCanSubscribe": false,
"witnesses": {
"incoming": {
"count": 0,
"handles": []
},
"outgoing": {
"count": 0,
"handles": []
},
"reciprocal": {
"count": 0,
"handles": []
}
}
}04 — Webhooks
No push endpoint yet — poll on demand.
The Verify API has no push webhook. Both /api/lookup and /api/users/[handle] are force-dynamic with no Cache-Control, so every call is a fresh read.
Today the Verify API has no push webhook endpoint. Both /api/lookup and /api/users/<handle> are force-dynamic— every call is a fresh read with no client-cache, so the canonical integration pattern is to poll on demand from your back end. Treat /api/lookup as your source-of-truth and cache the result yourself if you want to throttle outbound traffic. The closest existing precedent inside the codebase is /api/payment/poll, which polls Stripe payment events — the same shape applies.
05 — Badge embeds
Drop an unauthed badge into any third-party page.
No key, no SDK, no build step. A 3-kilobyte script reads /api/users/<handle> cross-origin and renders a Trust Score ribbon next to a byline. Unknown handles degrade to a “Not verified” pill.
The widget is intentionally narrow: a mount div carrying data-peerseal-handle="<8>" paired with one script tag loaded from the Peersealorigin. The script is idempotent — multiple mount divs each get their own badge, and the stylesheet is injected once.
<!-- Mount point: where the badge renders -->
<div data-peerseal-handle="demo0001"></div>
<!-- Script: idempotent — multiple mount divs each get their own badge -->
<script src="https://getpeerseal.com/embed/badge/demo0001.js"></script>import { useEffect } from "react";
const SCRIPT_SRC = "https://getpeerseal.com/embed/badge/demo0001.js";
function loadRuntime() {
if (document.querySelector('script[data-peerseal-runtime]')) return;
const s = document.createElement("script");
s.src = SCRIPT_SRC;
s.dataset.peersealRuntime = "1";
document.body.appendChild(s);
}
export function PeersealBadge({ handle, className }) {
useEffect(loadRuntime, []);
return (
<div data-peerseal-handle={handle} className={className} />
);
}
// Use: <PeersealBadge handle="demo0001" />// In your theme's functions.php:
add_shortcode("peerseal", function ($atts) {
$atts = shortcode_atts(["handle" => ""], $atts);
if (!$atts["handle"]) return "";
static $loaded = false;
$src = esc_url("https://getpeerseal.com/embed/badge/" . rawurlencode($atts["handle"]) . ".js");
$inline = !$loaded ? '<script src="' . $src . '"></script>' : "";
$loaded = true;
return '<div data-peerseal-handle="' . esc_attr($atts["handle"]) . '"></div>' . $inline;
});
// Usage in a post or page: [peerseal handle="demo0001"]Under the hood: GET /embed/badge/<handle>.js returns the same script body on every path (the URL segment is vanity) with Cache-Control: public, max-age=60 so edge caches coalesce. The script reads the API origin from its own src attribute at runtime, fetches /api/users/<handle> with access-control-allow-origin: *, defensively re-checks the handle against /^[a-z0-9]+$/so a tampered data attribute cannot route a request to the wrong origin, and maps the score to a band (high ≥ 70, mid ≥ 40, else low).
On 404 or network failure the badge degrades to a dashed “Not verified on Peerseal” pill linking to the same profile URL — never a flash of empty markup, never a thrown error.
Quick reference
- Script bodydemo0001.js
- Cache-Controlpublic, max-age=60
- CORSaccess-control-allow-origin: *
- Handle regex/^[a-z0-9]+$/
- Bandshigh ≥ 70 · mid ≥ 40 · else low
- Fallback“Not verified on Peerseal” pill