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
| Plan | Requests per key |
|---|---|
| Free / trial | 60 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
| Method | Path | Scope | What it does |
|---|---|---|---|
GET | /me | read | Key, agency and your current rate limit |
GET | /assets | read | List 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} | read | One asset with its last 30 checks |
GET | /assets/{id}/history?limit=50 | read | Check history for one asset, newest first (max 500) |
PATCH | /assets/{id} | write | Update 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/import | write | Bulk 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=30 | read | Assets expiring within N days, soonest first, including already expired |
POST | /assets | write | Track 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} | write | Stop tracking an asset (204) |
POST | /assets/{id}/check | write | Re-check expiry now and return the updated asset |
GET | /clients | read | List clients |
GET | /clients/{id} | read | One client |
POST | /clients | write | Create a client: name, optional contactEmails, contactName, phone, notes, billable, remindersOptIn |
PATCH | /clients/{id} | write | Update any of the client fields |
DELETE | /clients/{id} | write | Delete a client; its assets become unassigned |
GET | /reminders | read | Reminder schedule (days before expiry) for domains and SSL |
PUT | /reminders | write | Set 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} | read | Live RDAP/WHOIS registration expiry of any domain (not saved) |
GET | /lookup/cert/{host}?port=443 | read | Live TLS check of a public host's served certificate (not saved) |
GET | /lookup/ct/{host} | read | Latest certificate for a hostname from Certificate Transparency logs (does not contact the host) |
GET | /alert-channels | read | Email, Slack, Discord and webhook alert channels with plan usage. Slack/Discord/webhook URLs are masked |
POST | /alert-channels | write | Add a channel: {"type":"email|slack|discord|webhook","target":"…"}. Plan channel limits apply (402) |
PATCH | /alert-channels/{id} | write | Change target/type, or pause with {"enabled":false} |
DELETE | /alert-channels/{id} | write | Delete a channel |
POST | /alert-channels/{id}/test | write | Send a real sample alert to the channel (5/hour per agency) |
GET | /agency | read | Agency name, plan, usage vs limits (assets, alert channels, webhooks, API keys) and API rate limit |
GET | /members | read | Team members and roles (read-only) |
GET | /reports | read | Available monthly report periods with download links |
GET | /reports/{YYYY-MM}.json|csv|pdf | read | Monthly renewal report: what renews, who pays, totals |
GET | /webhooks | read | List webhooks and their last delivery |
GET | /webhooks/{id} | read | One webhook (the secret is never returned) |
PATCH | /webhooks/{id} | write | Change url or events, or pause with {"enabled":false}. The secret is kept |
GET | /webhooks/{id}/deliveries?limit=50 | read | Delivery log of real events: event, status, attempts, HTTP code, error (test deliveries are not logged) |
POST | /webhooks | write | Create a webhook: {"url":"https://…","events":[…]}. Returns the signing secret once |
DELETE | /webhooks/{id} | write | Delete a webhook |
POST | /webhooks/{id}/test | write | Send 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:
| Event | When |
|---|---|
asset.expiring | An asset reaches one of your reminder thresholds (same schedule as alerts) |
asset.expired | An asset's expiry date has passed (once per expiry date) |
asset.renewed | A check finds the expiry date moved later |
check.failed | A 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.