Product

How it works Widgets Compare HAPI score

Who it's for

Professionals Companies Services

More

Developers Pricing Journal
Claim your pageSign in

Quickstart

Link a signed-in user to their Moat page, show their card, and receive a verified webhook. Five steps, about an hour of work.

Before you start#

You'll need three values from us, a server that can make HTTPS requests, and a public HTTPS endpoint for webhooks. Request partner access if you don't have them yet.

MOAT_BASE_URL=https://moat.page
MOAT_XSITE_API_KEY=your_partner_key          # server only, never in a browser
MOAT_XSITE_WEBHOOK_SECRET=your_webhook_secret # used to verify signatures
Naming trap. MOAT_BASE_URL is the public origin you call. Moat's own code also defines MOAT_ROOT_URL, which it computes for its own subdirectory installs. Don't copy that one into your config.

1. Check your key#

Ask whether an email already has a Moat page. This confirms your key works without changing anything.

curl -X POST "$MOAT_BASE_URL/profile/api/has_profile" \
  -H "X-API-Key: $MOAT_XSITE_API_KEY" \
  -d "[email protected]"

Expected response:

{
  "success": true,
  "has_profile": false
}

A 401 with {"success": false, "error": "Unauthorized"} means the key is missing or wrong.

2. Link the user after sign-in#

Once your server has verified the person's sign-in (see Sign-in), call create_profile. It returns the existing page if there is one, or creates a starter page. It's safe to call on every sign-in.

Include your site details so the page lists your app as connected. The user_update_path is where Moat will send webhooks.

const res = await fetch(`${process.env.MOAT_BASE_URL}/profile/api/create_profile`, {
  method: "POST",
  headers: { "X-API-Key": process.env.MOAT_XSITE_API_KEY },
  body: new URLSearchParams({
    email: user.email,
    first_name: user.firstName,
    last_name: user.lastName,
    profile_type: "professional",
    site_url: "https://yourapp.example",
    site_name: "Your App",
    callback_site_param: "yourapp",
    user_update_path: "https://yourapp.example/hooks/moat",
  }),
});
const moat = await res.json();
// store moat.public_key (safe for browsers) and moat.private_key (server only)

Expected response:

{
  "success": true,
  "created": true,
  "profile_key": "7qzwrjebfpaw",
  "profile_url": "https://moat.page/profile/d/amara-okafor-7qzwrjebfpaw",
  "public_key": "c6424007df52eee0ee82483f7c464dc3",
  "private_key": "prv_..."
}
Keep the private key on your server. It unlocks contact details and edits for that one person. The public key is the only one that belongs in HTML.

3. Show their card#

Add the widget script once per page, then a div wherever the card should appear.

<script src="https://moat.page/profile/widget.js" defer></script>

<div class="moat-widget"
     data-public-key="c6424007df52eee0ee82483f7c464dc3"
     data-level="public"
     data-theme="light"></div>

You should see a card like this. If you see a network error instead, your domain isn't on the allowlist yet.

Amara Okafor
Staff Product Designer at Loomwell
Toronto, Canada

Designs payment flows people finish. Previously built checkout for two marketplaces.

Open to workCan mentorFigmaDesign systems

Experience

Staff Product Designer, Loomwell, 2023 to now

Senior Designer, Parcelry, 2019 to 2023

Education

BDes, OCAD University

Sample profileFollow

4. Verify webhooks#

When the person saves their page, Moat sends a JSON event to your user_update_path. Check the signature against the raw body before trusting it, then reply with a 2xx quickly.

import crypto from "node:crypto";

app.post("/hooks/moat", express.raw({ type: "application/json" }), (req, res) => {
  const expected = "sha256=" + crypto
    .createHmac("sha256", process.env.MOAT_XSITE_WEBHOOK_SECRET)
    .update(req.body)                     // raw bytes, not re-serialised JSON
    .digest("hex");
  const given = req.get("X-Moat-Signature") || "";
  const ok = given.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
  if (!ok) return res.sendStatus(401);

  const event = JSON.parse(req.body);
  queue.add(event);                       // do the work after responding
  res.sendStatus(200);
});

5. Try it end to end#

  1. Sign in to your app as a test user, which runs step 2.
  2. Open that person's Moat page and change their headline.
  3. Watch your endpoint receive profile.updated with a valid signature.
  4. Reload the page showing their card and press its refresh icon to see the new headline.

What's next#