Platform tools

RippleCore API reference

Everything the ten demos are built on: a key-authenticated REST API and signed webhooks. The base URL below is this running server — every curl on this page works if you paste it into a terminal while pnpm dev is up.

https://developer.ripplecore.co

Authentication

Send your key as a bearer token on every request:Authorization: Bearer <key>. A missing or unknown key is a 401; a key used outside its scope is a 403. These demo keys are mock values seeded by the simulator — in production, treat live keys like passwords and never ship one to a browser. You can create and revoke keys live at/manage; a revoked key returns a distinct 401 "API key has been revoked".

KeyValueNotes
Org-wide live keyrc_live_all_8f2c9a7b4e1d6c3fScope * — reads and writes any circle. Server-side only in a real integration.
FGCU circle keyrc_live_fgcu_a1b2c3d4e5f6a7b8Scoped to the fgcu circle — 403 on any other circle.
Public read-only key (acme)rc_pub_acme_11aa22bb33cc44ddSafe to ship in a browser: it can ONLY call /stats, and only for acme.

Live explorer

Send real requests to this server. Try the public key on a data endpoint to see a live 403, or POST the same act twice with the same idempotency key.

REST endpoints

GET/api/v1/acts/feed

List acts (feed)

The circle's activity feed, newest first. Quick acts are excluded — only story acts are shared. Pass since to pull incrementally: you get acts with createdSec >= since plus a nextCursor to use on the next poll (inclusive boundary — dedupe by act id client-side).

ParamInTypeRequiredNotes
circlequerystringnoDefaults to fgcu.
sincequeryunix secondsnoIncremental cursor. Omit for the full feed.
curl -s "https://developer.ripplecore.co/api/v1/acts/feed?circle=fgcu" \
  -H "Authorization: Bearer rc_live_fgcu_a1b2c3d4e5f6a7b8"
Example response
{
  "circle": "fgcu",
  "acts": [
    {
      "id": "a1",
      "circle": "fgcu",
      "memberId": "m1",
      "kind": "story",
      "category": "Environment",
      "caption": "Beach cleanup at Bunche Beach before class.",
      "peopleHelped": 8,
      "reactions": 12,
      "createdSec": 1752499400
    }
  ],
  "nextCursor": 1752499400
}
  • Poll with since=nextCursor every N seconds for an incremental sync — see the analytics demo.

Used by: Ripple Wall · ESG analytics · Mobile companion

POST/api/v1/acts

Log an act (write)

Create a story act. Validation: category and caption are required non-empty strings; peopleHelped is an integer 0–99 (422 otherwise). Send an Idempotency-Key header to make retries safe: replaying the same key returns the ORIGINAL act with idempotent: true instead of creating a duplicate. On success the platform emits an act.logged webhook to every subscribed endpoint.

ParamInTypeRequiredNotes
circlebodystringnoDefaults to fgcu; must match the key's scope.
categorybodystringyes
captionbodystringyes
peopleHelpedbodyinteger 0–99yes
curl -s -X POST "https://developer.ripplecore.co/api/v1/acts" \
  -H "Authorization: Bearer rc_live_fgcu_a1b2c3d4e5f6a7b8" \
  -H "content-type: application/json" \
  -H "Idempotency-Key: replace-with-a-uuid" \
  -d '{"category":"Environment","caption":"Beach cleanup with the crew","peopleHelped":3}'
Example response
{
  "act": {
    "id": "a_1f2e3d4c",
    "circle": "fgcu",
    "memberId": "m1",
    "kind": "story",
    "category": "Environment",
    "caption": "Beach cleanup with the crew",
    "peopleHelped": 3,
    "reactions": 0,
    "createdSec": 1752500000
  },
  "idempotent": false
}
  • Returns 201. Replays with the same Idempotency-Key return the stored act and idempotent: true.
  • Demo shortcut: the act is attributed to the circle's first member — a real API derives the author from a user-bound token.

Used by: Mobile companion

GET/api/v1/leaderboard

Leaderboard

Members of a circle ranked by ripples (kindness points), highest first.

ParamInTypeRequiredNotes
circlequerystringnoDefaults to fgcu.
curl -s "https://developer.ripplecore.co/api/v1/leaderboard?circle=fgcu" \
  -H "Authorization: Bearer rc_live_fgcu_a1b2c3d4e5f6a7b8"
Example response
{
  "circle": "fgcu",
  "leaderboard": [
    { "id": "m1", "circle": "fgcu", "name": "Maya Chen", "avatarHex": "#6C3BF4", "ripples": 128 },
    { "id": "m2", "circle": "fgcu", "name": "Devon Ross", "avatarHex": "#EC4899", "ripples": 96 }
  ]
}

Used by: Slack digest

GET/api/v1/circles/{slug}/members

List members

All members of a circle. The roster demo uses this as its reconcile pull — the source of truth to diff a webhook-built directory against.

ParamInTypeRequiredNotes
slugpathstringyes
curl -s "https://developer.ripplecore.co/api/v1/circles/fgcu/members" \
  -H "Authorization: Bearer rc_live_fgcu_a1b2c3d4e5f6a7b8"
Example response
{
  "circle": "fgcu",
  "members": [
    { "id": "m1", "circle": "fgcu", "name": "Maya Chen", "avatarHex": "#6C3BF4", "ripples": 128 }
  ]
}

Used by: Roster sync (reconcile)

GET/api/v1/circles/{slug}/stats

Circle stats

Aggregate counts for a circle. This is the ONE endpoint the public read-only key may call — which is why it's the only key safe to embed in a browser widget.

ParamInTypeRequiredNotes
slugpathstringyes
curl -s "https://developer.ripplecore.co/api/v1/circles/acme/stats" \
  -H "Authorization: Bearer rc_pub_acme_11aa22bb33cc44dd"
Example response
{
  "circle": "acme",
  "stats": { "acts": 12, "peopleHelped": 41, "ripples": 40 }
}

Used by: Embed widget

Webhooks

The platform POSTs a JSON envelope { id, type, createdSec, circle, data }to every subscribed endpoint, retrying up to 3 times. Watch them land, header by header, in the webhook inspector.

Event types

Typedata payloadNotes
act.logged{ "actId", "memberId", "memberName", "category", "caption", "peopleHelped" }A member logged a kind act. The highest-volume event.
act.reacted{ "actId", "memberName" }Someone reacted to an act.
member.joined{ "memberId", "name" }A member joined the circle.
member.removed{ "memberId", "name" }A member left or was removed.
reward.redeemed{ "rewardId", "title", "memberId", "memberName", "costRipples" }A member spent ripples on a reward — the money-touching event.

Delivery headers

x-ripplecore-signaturesha256=<hex> — HMAC-SHA256 of "<timestamp>.<rawBody>" with your endpoint secret
x-ripplecore-timestampUnix seconds when the event was created. Reject if outside ±300s (replay defense).
x-ripplecore-deliveryUUID, stable across retries — your idempotency key. Dedupe on it.

Verifying a delivery (Node)

Recompute the HMAC over the timestamp + raw body, compare in constant time, and enforce the ±300s window. Dedupe on x-ripplecore-delivery. The demo endpoints all share the secret whsec_demo_5f4d3c2b1a09f8e7d6c5b4a3.

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

export function verifyWebhook(req: { headers: Headers; rawBody: string }, secret: string) {
  const signature = req.headers.get("x-ripplecore-signature") ?? "";
  const timestamp = Number(req.headers.get("x-ripplecore-timestamp") ?? "0");

  // 1. Reject stale timestamps (replay defense).
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;

  // 2. Recompute the digest over "<timestamp>.<rawBody>".
  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${req.rawBody}`)
    .digest("hex");

  // 3. Constant-time compare against the header.
  const provided = signature.replace("sha256=", "");
  if (provided.length !== expected.length) return false;
  try {
    return timingSafeEqual(Buffer.from(provided, "hex"), Buffer.from(expected, "hex"));
  } catch {
    return false;
  }
}

Errors

StatusWhenExample body
401Missing or unknown API key{ "error": "Missing API key" }
403Public key on a data endpoint, or a circle-scoped key used cross-circle{ "error": "Key not scoped to circle \"acme\"" }
422Write validation failed (empty caption, peopleHelped out of 0–99, …){ "error": "peopleHelped must be an integer between 0 and 99" }