MRRX
Documentation

API Reference

REST API for creating cancellation sessions, retrieving outcomes, and handling webhooks. Authentication, errors, and signatures all covered here.

REST + JSONBearer authHMAC-SHA256 webhooks
On this page
Base URL
Authentication
Rate limits
Endpoints
Error responses
Session statuses
Actions
Survey reasons
Pagination
Idempotency
TypeScript SDK example
Webhooks
API versioning
Best practices
Section 1

Base URL

All API endpoints are served from a single base URL. Append the path to call any endpoint.

https://mrrx.app/api/v1
Section 2

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:

mrrx_live_ - production keys, scoped to your live Stripe account
mrrx_test_ - test/development keys, scoped to your test Stripe account
Key storage
API keys are SHA-256 hashed before storage. Only the prefix is stored in cleartext for identification. Revoke and regenerate instantly from the dashboard if a key is compromised.
Section 3

Rate Limits

Limits apply per tenant or per session, depending on the endpoint.

EndpointLimit
Session creation100/hour per tenant
Session execution10/minute per session
Status checks1000/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
Section 4

Endpoints

Create Session

POST/api/v1/sessions

Creates a new cancellation session for a customer.

Request Body

FieldTypeRequiredDescription
customer_idstringYesStripe customer ID (e.g., cus_xxxxx)
subscription_idstringYesStripe subscription ID (e.g., sub_xxxxx)
return_urlstringNoURL to redirect after completion
metadataobjectNoCustom 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

FieldTypeDescription
idstringUnique session identifier
tokenstringSecure token for hosted page access
urlstringFull URL to redirect the customer to
expires_atstringISO 8601 timestamp when session expires
statusstringCurrent session status

Get Session

GET/api/v1/sessions/:id

Retrieves 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

GET/api/v1/health

Check API availability and current version.

{
  "status": "healthy",
  "timestamp": "2026-01-29T15:30:00.000Z",
  "version": "1.0.0"
}
Section 5

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

CodeHTTP StatusDescription
invalid_api_key401API key not found or inactive
expired_api_key401API key has expired
insufficient_permissions403API key lacks required permission
rate_limited429Too many requests
invalid_customer400Customer not found in connected Stripe
subscription_not_found404Subscription not found in connected Stripe
subscription_already_canceled400Cannot create session for canceled subscription
session_already_exists409Active session exists for this subscription
session_not_found404Session ID not found
session_expired410Session has expired
session_already_completed409Session action already executed
action_not_available400Requested action not configured for this session
invalid_token401Session token invalid or expired
invalid_request400Malformed request body
validation_error400Request validation failed
stripe_error502Error communicating with Stripe
internal_error500Internal server error
Section 6

Session Statuses

The status field on a session reflects its current state in the cancellation flow.

StatusDescription
createdSession created, customer hasn't viewed page yet
viewedCustomer opened the cancellation page
offer_shownRetention offers displayed to customer
action_selectedCustomer selected an action
survey_completedCustomer completed the feedback survey
executingAction is being processed on Stripe
completedAction successfully executed
failedAction execution failed
expiredSession expired (24-hour limit)
abandonedCustomer left without completing (inferred)
Section 7

Actions

Five retention actions can resolve a session.

ActionDescriptionUse case
pausePause subscription for configured durationCustomer needs temporary break (1-3 months)
discountApply discount coupon to subscriptionPrice sensitivity, retain with reduced cost
downgradeSwitch to a lower-tier planCustomer needs fewer features/usage
extend_trialExtend trial period by configured daysTrial users need more time to evaluate
cancelCancel the subscriptionCustomer confirms cancellation
Section 8

Survey Reasons

Standardized exit reason codes returned in survey_response.reason.

ReasonDisplay text
too_expensiveToo expensive
not_using_enoughNot using it enough
missing_featuresMissing features I need
switching_competitorSwitching to a competitor
technical_issuesTechnical issues
business_closingBusiness closing / project ended
otherOther
Section 9

Pagination

List endpoints use cursor-based pagination. Pass limit and the cursor from the previous response.

ParameterTypeDefaultDescription
limitinteger20Results per page (max 100)
cursorstringCursor 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"
Section 10

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 '{ ... }'
Section 11

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;
Section 12

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

EventTrigger
session.createdNew session created via API
session.completedCustomer completed an action
session.expiredSession expired without completion
subscription.pausedSubscription paused via MRRX
subscription.discount_appliedDiscount applied to subscription
subscription.trial_extendedTrial period extended
subscription.canceledSubscription 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

HeaderDescription
X-MRRX-SignatureSignature for verification (see below)
X-MRRX-Event-TypeEvent type (e.g., session.completed)
X-MRRX-Delivery-IDUnique delivery identifier
X-MRRX-Retry-AttemptRetry attempt number (only on retries)
Content-Typeapplication/json
User-AgentMRRX-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:

Event type
HTTP status code
Response time (ms)
Response body (first 1KB)
Success/failure status
Error message (if failed)
Attempt number (for retries)

Retry Behavior

Failed deliveries can be manually retried from the dashboard
After 10 consecutive failures, the endpoint is automatically disabled
Test events can be sent anytime from the webhook settings page
Delivery logs cover both test and retry attempts
Section 13

API Versioning

The API uses URL-based versioning: /api/v1/...

Breaking changes ship under new versions
Old versions are supported for 12 months after deprecation
Non-breaking additions (new fields) may arrive without a version change
Section 14

Best Practices

Session Flow Integration

Add a cancel button
Place it on your account or billing page where the subscriber would naturally look to manage their plan.
Create a session on click
Call POST /api/v1/sessionsserver-side with the subscriber's Stripe IDs.
Redirect immediately to session.url
The hosted page handles offers, surveys, and Stripe execution. Don't rebuild this client-side.
Handle return via return_url
After the flow completes, the customer is sent back to your return_url. Show a confirmation matching their outcome.
Use webhooks for source-of-truth
Don't poll. Subscribe to 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:

Create sessions with test Stripe customer and subscription IDs
Walk through every retention flow (pause, discount, downgrade, extend trial, cancel)
Verify webhooks arrive correctly using the test button in Settings > Webhooks
Inspect delivery logs for status codes and response times
Verify the resulting subscription state in the Stripe dashboard
Have questions?
Contact hello@mrrx.app or browse the rest of the docs.

Ready to integrate?

Walk through the full setup, end-to-end, in twelve minutes.