API Reference
REST API for creating cancellation sessions, retrieving outcomes, and handling webhooks. Authentication, errors, and signatures — all covered here.
Base URL
All API endpoints are served from a single base URL. Append the path to call any endpoint.
https://mrrx.app/api/v1
Authentication
All API requests require a Bearer token in the Authorization header. Generate keys in Settings > API Keys.
Authorization: Bearer mrrx_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Two key prefixes are issued:
Rate Limits
Limits apply per tenant or per session, depending on the endpoint.
| Endpoint | Limit |
|---|---|
| Session creation | 100/hour per tenant |
| Session execution | 10/minute per session |
| Status checks | 1000/hour per tenant |
Every response includes rate limit headers so you can throttle client-side:
X-RateLimit-Limit: 100 X-RateLimit-Remaining: 95 X-RateLimit-Reset: 1706540400
Endpoints
Create Session
/api/v1/sessionsCreates a new cancellation session for a customer.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
customer_id | string | Yes | Stripe customer ID (e.g., cus_xxxxx) |
subscription_id | string | Yes | Stripe subscription ID (e.g., sub_xxxxx) |
return_url | string | No | URL to redirect after completion |
metadata | object | No | Custom key-value pairs (max 10 keys) |
Example Request
curl -X POST https://mrrx.app/api/v1/sessions \
-H "Authorization: Bearer mrrx_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"customer_id": "cus_ABC123",
"subscription_id": "sub_XYZ789",
"return_url": "https://yourapp.com/account",
"metadata": {
"user_id": "usr_123",
"plan": "pro"
}
}'Example Response (201 Created)
{
"id": "ses_abc123xyz",
"token": "eyJhbGciOiJIUzI1NiIs...",
"url": "https://mrrx.app/c/eyJhbGciOiJIUzI1NiIs...",
"expires_at": "2026-01-30T12:00:00.000Z",
"status": "created"
}Response Fields
| Field | Type | Description |
|---|---|---|
id | string | Unique session identifier |
token | string | Secure token for hosted page access |
url | string | Full URL to redirect the customer to |
expires_at | string | ISO 8601 timestamp when session expires |
status | string | Current session status |
Get Session
/api/v1/sessions/:idRetrieves the current status and details of a session.
Example Request
curl https://mrrx.app/api/v1/sessions/ses_abc123xyz \ -H "Authorization: Bearer mrrx_live_xxxxx"
Example Response (200 OK)
{
"id": "ses_abc123xyz",
"status": "completed",
"subscription": {
"id": "sub_XYZ789",
"product_name": "Pro Plan",
"price": {
"amount": 2900,
"currency": "usd",
"interval": "month"
},
"current_period_end": "2026-02-15T00:00:00.000Z"
},
"outcome": {
"action": "pause",
"executed_at": "2026-01-29T15:30:00.000Z",
"details": {
"pause_duration_days": 30,
"resume_date": "2026-03-17T00:00:00.000Z"
}
},
"survey_response": {
"reason": "not_using_enough",
"reason_other": null,
"feedback": "Taking a break to focus on other projects"
},
"created_at": "2026-01-29T14:00:00.000Z",
"expires_at": "2026-01-30T14:00:00.000Z"
}Action-Specific Outcome Details
The shape of outcome.details varies by action. Each form is shown below.
Pause action
"details": {
"pause_duration_days": 30,
"resume_date": "2026-03-17T00:00:00.000Z"
}Discount action
"details": {
"discount_type": "percent",
"discount_amount": 25,
"duration_months": 3,
"coupon_id": "coup_abc123"
}Downgrade action
"details": {
"previous_price_id": "price_old123",
"new_price_id": "price_new456",
"new_amount": 1900
}Extend trial action
"details": {
"extension_days": 14,
"new_trial_end": "2026-02-12T00:00:00.000Z",
"original_trial_end": "2026-01-29T00:00:00.000Z"
}Cancel action
"details": {
"canceled_at": "2026-01-29T15:30:00.000Z",
"cancel_at_period_end": false
}Health Check
/api/v1/healthCheck API availability and current version.
{
"status": "healthy",
"timestamp": "2026-01-29T15:30:00.000Z",
"version": "1.0.0"
}Error Responses
All errors share the same envelope. Inspect error.code for programmatic handling.
{
"error": {
"code": "invalid_subscription",
"message": "Subscription not found or not active",
"details": {
"subscription_id": "sub_invalid"
}
}
}Error Codes
| Code | HTTP Status | Description |
|---|---|---|
invalid_api_key | 401 | API key not found or inactive |
expired_api_key | 401 | API key has expired |
insufficient_permissions | 403 | API key lacks required permission |
rate_limited | 429 | Too many requests |
invalid_customer | 400 | Customer not found in connected Stripe |
subscription_not_found | 404 | Subscription not found in connected Stripe |
subscription_already_canceled | 400 | Cannot create session for canceled subscription |
session_already_exists | 409 | Active session exists for this subscription |
session_not_found | 404 | Session ID not found |
session_expired | 410 | Session has expired |
session_already_completed | 409 | Session action already executed |
action_not_available | 400 | Requested action not configured for this session |
invalid_token | 401 | Session token invalid or expired |
invalid_request | 400 | Malformed request body |
validation_error | 400 | Request validation failed |
stripe_error | 502 | Error communicating with Stripe |
internal_error | 500 | Internal server error |
Session Statuses
The status field on a session reflects its current state in the cancellation flow.
| Status | Description |
|---|---|
created | Session created, customer hasn't viewed page yet |
viewed | Customer opened the cancellation page |
offer_shown | Retention offers displayed to customer |
action_selected | Customer selected an action |
survey_completed | Customer completed the feedback survey |
executing | Action is being processed on Stripe |
completed | Action successfully executed |
failed | Action execution failed |
expired | Session expired (24-hour limit) |
abandoned | Customer left without completing (inferred) |
Actions
Five retention actions can resolve a session.
| Action | Description | Use case |
|---|---|---|
pause | Pause subscription for configured duration | Customer needs temporary break (1-3 months) |
discount | Apply discount coupon to subscription | Price sensitivity, retain with reduced cost |
downgrade | Switch to a lower-tier plan | Customer needs fewer features/usage |
extend_trial | Extend trial period by configured days | Trial users need more time to evaluate |
cancel | Cancel the subscription | Customer confirms cancellation |
Survey Reasons
Standardized exit reason codes returned in survey_response.reason.
| Reason | Display text |
|---|---|
too_expensive | Too expensive |
not_using_enough | Not using it enough |
missing_features | Missing features I need |
switching_competitor | Switching to a competitor |
technical_issues | Technical issues |
business_closing | Business closing / project ended |
other | Other |
Pagination
List endpoints use cursor-based pagination. Pass limit and the cursor from the previous response.
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 20 | Results per page (max 100) |
cursor | string | — | Cursor from previous response |
curl "https://mrrx.app/api/v1/sessions?limit=50" \ -H "Authorization: Bearer mrrx_live_xxxxx"
Paginated response shape
{
"data": [ ... ],
"pagination": {
"has_more": true,
"next_cursor": "ses_abc123xyz"
}
}Fetching the next page
curl "https://mrrx.app/api/v1/sessions?limit=50&cursor=ses_abc123xyz" \ -H "Authorization: Bearer mrrx_live_xxxxx"
Idempotency
Safely retry POST requests by including an Idempotency-Key header. Identical requests with the same key return the original response. Keys are valid for 24 hours.
curl -X POST https://mrrx.app/api/v1/sessions \
-H "Authorization: Bearer mrrx_live_xxxxx" \
-H "Idempotency-Key: unique-request-id-123" \
-H "Content-Type: application/json" \
-d '{ ... }'TypeScript SDK Example
Official SDKs are coming. In the meantime, this minimal client covers most integration needs. For framework-specific examples, see the examples page.
class MrrxClient {
private apiKey: string;
private baseUrl = 'https://mrrx.app/api/v1';
constructor(apiKey: string) {
this.apiKey = apiKey;
}
async createSession(params: {
customer_id: string;
subscription_id: string;
return_url?: string;
metadata?: Record<string, string>;
}) {
const response = await fetch(`${this.baseUrl}/sessions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(params),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error.message);
}
return response.json();
}
async getSession(sessionId: string) {
const response = await fetch(`${this.baseUrl}/sessions/${sessionId}`, {
headers: {
'Authorization': `Bearer ${this.apiKey}`,
},
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error.message);
}
return response.json();
}
}
// Usage
const mrrx = new MrrxClient(process.env.MRRX_API_KEY!);
const session = await mrrx.createSession({
customer_id: 'cus_xxx',
subscription_id: 'sub_xxx',
return_url: 'https://yourapp.com/account'
});
// Redirect user to session.url
window.location.href = session.url;Webhooks
Configure outbound webhooks in Settings > Webhooks to receive real-time events. Every delivery is signed with HMAC-SHA256 so you can verify authenticity.
Event Types
| Event | Trigger |
|---|---|
session.created | New session created via API |
session.completed | Customer completed an action |
session.expired | Session expired without completion |
subscription.paused | Subscription paused via MRRX |
subscription.discount_applied | Discount applied to subscription |
subscription.trial_extended | Trial period extended |
subscription.canceled | Subscription canceled via MRRX |
Payload Shape
{
"id": "evt_abc123",
"type": "session.completed",
"created_at": "2026-01-29T15:30:00.000Z",
"data": {
"session_id": "ses_abc123xyz",
"subscription_id": "sub_XYZ789",
"customer_id": "cus_ABC123",
"action": "pause",
"outcome": {
"action": "pause",
"details": {
"pause_duration_days": 30,
"resume_date": "2026-03-17T00:00:00.000Z"
}
},
"survey_response": {
"reason": "not_using_enough"
}
}
}Webhook Headers
| Header | Description |
|---|---|
X-MRRX-Signature | Signature for verification (see below) |
X-MRRX-Event-Type | Event type (e.g., session.completed) |
X-MRRX-Delivery-ID | Unique delivery identifier |
X-MRRX-Retry-Attempt | Retry attempt number (only on retries) |
Content-Type | application/json |
User-Agent | MRRX-Webhook/1.0 |
Signature Verification
The X-MRRX-Signature header uses the format t=<timestamp>,v1=<signature>:
X-MRRX-Signature: t=1706540400,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Verify using HMAC-SHA256 and constant-time comparison:
import crypto from 'crypto';
function verifyWebhook(
payload: string,
signatureHeader: string,
secret: string
): boolean {
// Parse the signature header
const parts = signatureHeader.split(',');
const timestamp = parts.find(p => p.startsWith('t='))?.slice(2);
const signature = parts.find(p => p.startsWith('v1='))?.slice(3);
if (!timestamp || !signature) return false;
// Verify the signature against the payload
const expected = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express example
app.post('/webhooks/mrrx', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-mrrx-signature'] as string;
const payload = req.body.toString();
if (!verifyWebhook(payload, signature, process.env.MRRX_WEBHOOK_SECRET!)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(payload);
// Process event...
res.json({ received: true });
});Delivery Logs
Every delivery is recorded and visible at Settings > Webhooks > View Details > Delivery Log. Each entry includes:
Retry Behavior
API Versioning
The API uses URL-based versioning: /api/v1/...
Best Practices
Session Flow Integration
POST /api/v1/sessionsserver-side with the subscriber's Stripe IDs.return_url. Show a confirmation matching their outcome.session.completed and the relevant subscription.* events.Error Handling
try {
const session = await mrrx.createSession({
customer_id: user.stripeCustomerId,
subscription_id: user.stripeSubscriptionId,
});
window.location.href = session.url;
} catch (error) {
if (error.message.includes('already_exists')) {
// Session already active, redirect to existing one
} else if (error.message.includes('already_canceled')) {
// Show message that subscription is already canceled
} else {
// Show generic error, log for investigation
}
}Testing
Use Stripe test mode customers and subscriptions before going live: