RenewRoster REST API

REST APIMCP serverOpenAPI 3.1 spec

Every RenewRoster account, free or paid, can use the API to read and manage the domains and SSL certificates it tracks. Base URL: https://app.renewroster.com/api/v1. Requests and responses are JSON.

Authentication

An agency owner creates keys in the dashboard under API. A key belongs to one agency and can only see that agency's data. Pick Read-only or Read + write when you create it. The key is shown once; we store only a hash. Revoke a key at any time from the same page.

curl -H "Authorization: Bearer rr_live_xxxxxxxxxxxx_yourSecret" https://app.renewroster.com/api/v1/me

Missing, invalid or revoked keys get 401. Using a read-only key on a write endpoint gets 403.

Rate limits

PlanRequests per key
Free / trial60 per minute
Paid (Starter, Agency, Scale)300 per minute

Responses include X-RateLimit-Limit. Over the limit you get 429 with Retry-After (seconds). POST /assets/{id}/check is also limited to 30 per hour per key and once a minute per asset.

Endpoints

MethodPathScopeWhat it does
GET/mereadKey, agency and your current rate limit
GET/assetsreadList domains and certificates. Filters: type=domain|ssl, status=ok|expiring|expired|unknown|error, client=<id>|none, expiring_within=<days>, limit (max 1000), offset
GET/assets/{id}readOne asset with its last 30 checks
GET/assets/{id}/history?limit=50readCheck history for one asset, newest first (max 500)
PATCH/assets/{id}writeUpdate clientId/clientName (or clientId:null to unassign), payer (agency|client), autoRenew, renewalCost, currency, per-asset reminderDays (null = agency schedule), or rename with name/port
POST/assets/importwriteBulk add up to 1000: {"assets":[{"type":"domain","name":"a.com","clientName":"Acme"}]}, or the dashboard CSV (client,domain,cert_host,port) as {"csv":"…"} or a text/csv body. Duplicates are skipped; the whole import is refused with 402 if it would pass your plan's asset limit. 30 imports/hour
GET/expiring?days=30readAssets expiring within N days, soonest first, including already expired
POST/assetswriteTrack a domain or SSL host: {"type":"domain","name":"example.com","clientName":"Acme"} or {"type":"ssl","name":"www.example.com","port":443}. Plan asset limits apply (402 when full); duplicates get 409
DELETE/assets/{id}writeStop tracking an asset (204)
POST/assets/{id}/checkwriteRe-check expiry now and return the updated asset
GET/clientsreadList clients
GET/clients/{id}readOne client
POST/clientswriteCreate a client: name, optional contactEmails, contactName, phone, notes, billable, remindersOptIn
PATCH/clients/{id}writeUpdate any of the client fields
DELETE/clients/{id}writeDelete a client; its assets become unassigned
GET/remindersreadReminder schedule (days before expiry) for domains and SSL
PUT/reminderswriteSet domainDays / sslDays to arrays from 90, 60, 45, 30, 21, 14, 7, 3, 1, or null for the default (60, 30, 14, 3)
GET/lookup/domain/{domain}readLive RDAP/WHOIS registration expiry of any domain (not saved)
GET/lookup/cert/{host}?port=443readLive TLS check of a public host's served certificate (not saved)
GET/lookup/ct/{host}readLatest certificate for a hostname from Certificate Transparency logs (does not contact the host)
GET/alert-channelsreadEmail, Slack, Discord and webhook alert channels with plan usage. Slack/Discord/webhook URLs are masked
POST/alert-channelswriteAdd a channel: {"type":"email|slack|discord|webhook","target":"…"}. Plan channel limits apply (402)
PATCH/alert-channels/{id}writeChange target/type, or pause with {"enabled":false}
DELETE/alert-channels/{id}writeDelete a channel
POST/alert-channels/{id}/testwriteSend a real sample alert to the channel (5/hour per agency)
GET/agencyreadAgency name, plan, usage vs limits (assets, alert channels, webhooks, API keys) and API rate limit
GET/membersreadTeam members and roles (read-only)
GET/reportsreadAvailable monthly report periods with download links
GET/reports/{YYYY-MM}.json|csv|pdfreadMonthly renewal report: what renews, who pays, totals
GET/webhooksreadList webhooks and their last delivery
GET/webhooks/{id}readOne webhook (the secret is never returned)
PATCH/webhooks/{id}writeChange url or events, or pause with {"enabled":false}. The secret is kept
GET/webhooks/{id}/deliveries?limit=50readDelivery log of real events: event, status, attempts, HTTP code, error (test deliveries are not logged)
POST/webhookswriteCreate a webhook: {"url":"https://…","events":[…]}. Returns the signing secret once
DELETE/webhooks/{id}writeDelete a webhook
POST/webhooks/{id}/testwriteSend a signed webhook.test delivery now and report the HTTP status

Dashboard-only

For security these stay in the dashboard and are not available through the API or MCP: billing, plan changes, checkout and invoices; inviting members and changing roles; deleting the agency; creating or revoking API keys; agency branding; and admin operations. Webhook secrets are shown only once, when the webhook is created.

Errors look like {"error":"message"} with a 4xx status. The full schema is in the OpenAPI 3.1 spec.

Examples

# What expires in the next 30 days?
curl -s -H "Authorization: Bearer $RR_KEY" "https://app.renewroster.com/api/v1/expiring?days=30"

# Only SSL certificates that already expired
curl -s -H "Authorization: Bearer $RR_KEY" "https://app.renewroster.com/api/v1/assets?type=ssl&status=expired"

# Track a domain for a client (read + write key)
curl -s -X POST -H "Authorization: Bearer $RR_KEY" -H "content-type: application/json" \
  -d '{"type":"domain","name":"example.com","clientName":"Acme"}' https://app.renewroster.com/api/v1/assets

# Remind 30 and 7 days before domain expiry
curl -s -X PUT -H "Authorization: Bearer $RR_KEY" -H "content-type: application/json" \
  -d '{"domainDays":[30,7]}' https://app.renewroster.com/api/v1/reminders

Each asset includes type, name, expiresAt, daysLeft, status (expiring means 30 days or less), registrar or issuer, clientId and the last check time and error.

Webhooks

Add webhooks with the API or in the dashboard. Endpoints must be public https:// URLs on port 443. Events:

EventWhen
asset.expiringAn asset reaches one of your reminder thresholds (same schedule as alerts)
asset.expiredAn asset's expiry date has passed (once per expiry date)
asset.renewedA check finds the expiry date moved later
check.failedA check could not read the expiry (at most once per asset per day)
POST /your/endpoint
Content-Type: application/json
X-RenewRoster-Event: asset.expiring
X-RenewRoster-Delivery: 2f6c…
X-RenewRoster-Signature: t=1767225600,v1=5b1f…

{"id":"2f6c…","event":"asset.expiring","createdAt":"2027-01-01T02:17:00.000Z","agencyId":"…",
 "data":{"asset":{"id":"…","type":"domain","name":"example.com","expiresAt":"2027-01-31T00:00:00.000Z","daysLeft":30},"threshold":30}}

Return any 2xx within 10 seconds. Otherwise we retry after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, then mark the delivery failed. Use X-RenewRoster-Delivery to ignore duplicates.

Verifying signatures

The signature is HMAC-SHA256(secret, t + "." + rawBody) in hex. Compute it over the raw request body, compare in constant time, and reject timestamps older than 5 minutes.

// Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  return expected.length === parts.v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
# Python
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])
<?php // PHP
function rr_verify(string $raw, string $header, string $secret): bool {
  parse_str(str_replace(',', '&', $header), $p);
  if (abs(time() - (int)$p['t']) > 300) return false;
  return hash_equals(hash_hmac('sha256', $p['t'] . '.' . $raw, $secret), $p['v1']);
}

Use it from AI assistants

The same keys work with our remote MCP server for Claude, Cursor, VS Code and Windsurf.