Skip to content

The Peerbeta API, for your scripts.

Create a closed test from your release pipeline, follow it day by day and fetch its report as JSON, Markdown or PDF. The same actions work through webhooks and an MCP server.

Getting started

A key belongs to an organization. Its owner or an admin creates it in Settings › API keys, with the scopes the script needs; it is shown once. Pass it in the Authorization header of every request.

First request
curl https://api.peerbeta.com/api/v1/organization \
  -H "Authorization: Bearer pbk_…"
ScopeAllows
workspaces.readRead the organization
tests.readRead tests and their status
tests.writeCreate tests
reports.readRead reports (JSON, Markdown, PDF)
mcp.readMCP server: read tools
mcp.writeMCP server: write tools

Base address: https://api.peerbeta.com/api/v1. The organization and the member always come from the key, never from the request.

Limits

  • 60 requests a minute per key. Every answer carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; past it, the answer is a 429 with Retry-After.
  • 50 tests created a day per organization.
  • A request body of 64 KB at most.
  • Each call is written to the key’s journal, shown in Settings › API keys.

Create a test

POST /tests creates the app (or reuses the organization’s app with this package) and a draft test. With funding, the test is paid at once: a paid order not attached yet, the credits queue (your credits or the organization’s), or the circle. Without funding, the test waits for its payment on the web.

Request body
{
  "app": {
    "name": "Carnet",
    "packageName": "com.example.carnet",
    "category": "productivity",
    "countries": ["FR", "BE"],
    "playConsoleAccountId": "<id from GET /organization>"
  },
  "features": [
    { "title": "Search", "instruction": "Search a note with two words." }
  ],
  "optInUrl": "https://play.google.com/apps/testing/com.example.carnet",
  "funding": { "type": "credits", "wallet": "organization" }
}

Send an Idempotency-Key header (1 to 100 characters): the same key with the same body returns the same test for 24 hours, without creating a second one; with another body, the answer is a 409. The answer is a 201 with a Location header, or a 200 when the idempotency key was already used.

The Play Console steps (closed track, testers’ group, countries, release) are still confirmed by the developer on the test’s page: its address is in links.dashboard.

Follow a test

GET /tests/{testId}/status tells where the test stands: the day out of 14, the testers (installed, active today, at risk, lost), the day 1, day 7 and day 14 feedback, and whether the report is ready. Rather than polling the API, subscribe to the webhooks.

A test’s state
curl https://api.peerbeta.com/api/v1/tests/<testId>/status \
  -H "Authorization: Bearer pbk_…"

Fetch the report

GET /tests/{testId}/report returns the report as JSON: numbers, synthesis, recommendations, the organization’s brand, and a signed PDF link valid for 15 minutes, without a key. With format=markdown, the answer is the report’s Markdown.

An organization’s report carries its name and logo, with a discreet mention of Peerbeta. Peerbeta prepares the production access request; Google alone decides.

Webhooks

Add an HTTPS destination in Settings › Webhooks and pick its events. Each send is signed with the destination’s secret. An answer outside 2xx is tried up to 7 times in all (after 1 min, 5 min, 30 min, 2 h, 8 h, then 24 h); the attempt journal is in the settings.

EventSent when
test.startedDay 1 starts: enough testers have a verified install.
test.pausedThe test is paused (by you, by Peerbeta, or a broken link).
tester.installedA tester installed the app, verified by the tester app.
checkpoint.completedA tester sent their day 1, day 7 or day 14 feedback.
test.completedThe 14 days are over.
report.readyThe report is written: JSON, Markdown and PDF.

The body is a JSON envelope: id, type, created (milliseconds since 1970) and data, which always carries the test. Testers are numbers there, never a name or an address.

Example: tester.installed
{
  "id": "evt_3f9a2c7d1e0b4a5c6d7e8f90",
  "type": "tester.installed",
  "created": 1762700000000,
  "data": {
    "test": {
      "id": "<testId>",
      "status": "recruiting",
      "offer": "pro",
      "mode": "offer",
      "app": { "id": "<appId>", "name": "Carnet", "packageName": "com.example.carnet" },
      "day1At": null,
      "endsAt": null,
      "eligibleAt": null
    },
    "tester": { "number": 7, "installedAt": "2026-11-09T14:02:11.000Z", "proof": "open" },
    "installs": 9
  }
}

Check the signature before reading the body: an HMAC SHA-256 of “timestamp.body” with the secret, compared with x-peerbeta-signature; refuse a timestamp older than five minutes. x-peerbeta-event-id lets you skip a send already handled.

Verify the signature (Node.js)
import { createHmac, timingSafeEqual } from "node:crypto";

// body: the raw request body, as a string, before any JSON.parse.
export function isFromPeerbeta(body, headers, secret) {
  const timestamp = headers["x-peerbeta-timestamp"];
  const signature = headers["x-peerbeta-signature"] ?? "";
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const expected = "sha256=" + createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
  return expected.length === signature.length && timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

MCP server

An AI agent (Claude, Cursor…) does the same actions through the MCP server, over HTTP transport, with an organization key. Read tools need mcp.read; create_test needs mcp.write and tests.write.

Server address
https://api.peerbeta.com/mcp

Tools: list_workspaces, get_organization, list_tests, create_test, get_test_status, get_test_report.

Errors

An error returns JSON with a stable code in error, a message and, for a validation, the field at fault.

HTTPCodeWhen
400VALIDATIONA field is missing or malformed; `field` names it.
401UNAUTHENTICATEDNo key in the Authorization header.
401API_KEY_INVALIDUnknown, expired or revoked key.
403FORBIDDENThe key lacks the scope, or the API is not turned on.
404NOT_FOUNDThis test or report is not in the key’s organization.
409CONFLICTA test is already under way for this app, or the idempotency key was used with another body.
429RATE_LIMITEDToo many requests; `Retry-After` says when to try again.

Reference

RequestScopeReturns
GET /organizationworkspaces.readThe key’s organization: billing identity, Play Console accounts, shared credits.
GET /teststests.readThe organization’s tests, newest first (`status`, `limit`, `cursor`).
POST /teststests.writeCreate a test (and its app when the package is new).
GET /tests/{testId}tests.readOne test: mode, offer, setup, dates, links.
GET /tests/{testId}/statustests.readThe test’s state now: day, testers, feedback, report.
GET /tests/{testId}/reportreports.readThe report as JSON (with a signed PDF link) or Markdown (`format=markdown`).
GET /reports/{reportId}/pdfnoneThe report’s PDF, by signed link, without a key.

The full OpenAPI 3.1 specification, with the schemas and the webhooks: https://api.peerbeta.com/api/v1/openapi.json.