Docs

RetentBase API integration guide

End-to-end setup instructions for the hosted cancellation flow, result verification, and Advanced API integration paths.

Before you start

Public docs intentionally avoid workspace-specific secrets and identifiers. Create a workspace first, then use /dashboard/docs for your workspaceKey and return-host setup, plus/dashboard/settings?tab=api for API keys and the outbound webhook signing secret.

Hosted helper API POST /api/v1/cancel-link is available on Core and Advanced. Session diagnostics (GET /api/v1/cancel-sessions/:id) and custom cancellation ingestion/update APIs require Advanced.

Choose integration model

Call the RetentBase API at https://api.retentbase.com. If you use the hosted cancellation flow, redirect customers to the cancel host at https://cancel.retentbase.com.

Plan model: Core = Hosted flow only. Advanced = Hosted flow or Custom API + advanced analytics.

Advanced API: complete reference

Use this integration when your product owns the cancellation UX and RetentBase is your analytics and outcome ledger.

Keep this flow standalone: do not pass hosted session identifiers into the custom API.

Canonical request schema

Use eventId, reasonKey, optional outcome, an optionaloffer object, optional context, and optional metadata.

Use the same eventId on retries to keep ingestion idempotent.

POST /api/v1/cancellations (recommended first call)

await fetch("https://api.retentbase.com/api/v1/cancellations", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer " + process.env.RETENTBASE_API_KEY,
  },
  body: JSON.stringify({
    environment: "sandbox",
    eventId: "cancel_evt_8f30a7bf",
    userId: "user_456",
    reasonKey: "too_expensive",
    outcome: "churned", // omit to leave outcome pending for PATCH
    offer: {
      type: "discount",
      accepted: true,
      label: "20% off next month",
    },
    reasonText: "Needs lower monthly price",
    context: {
      plan: "pro",
      mrr: 129,
      tenureDays: 42,
    },
    occurredAt: new Date().toISOString(),
    metadata: {
      source: "billing_settings_page",
    },
  }),
});

PATCH /api/v1/cancellations/{eventId}

await fetch("https://api.retentbase.com/api/v1/cancellations/cancel_evt_8f30a7bf", {
  method: "PATCH",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer " + process.env.RETENTBASE_API_KEY,
  },
  body: JSON.stringify({
    environment: "sandbox",
    outcome: "recovered",
    effectiveAt: new Date().toISOString(),
    metadata: {
      actor: "success_playbook",
      channel: "cs_call",
    },
  }),
});
POST/api/v1/cancellations

Creates or reuses a cancellation event (idempotent by environment + eventId).

Notes

  • eventId is your idempotency key. Reuse it for safe retries.
  • Keep environment consistent if you plan to PATCH the event later.

Availability

Advanced plan only

Auth

Authorization: Bearer <Workspace API Key> or x-api-key

Content-Type

application/json

Idempotency

Reuse the same eventId for safe retries.

JSON Body

FieldTypeRequiredDescription
environment"prod" | "sandbox"NoEnvironment for the event. Defaults to prod.
eventIdstringYesStable idempotency key from your system.
userIdstringNoOptional external customer identifier from your system.
reasonKeystringYesMust match an enabled reason key in the workspace.
outcome"churned" | "recovered" | "abandoned"NoOptional final outcome. Omit to create a pending event and update later.
offer.type"none" | "discount" | "pause"NoOffer shown to the customer. Defaults to none when omitted.
offer.acceptedbooleanNoWhether the customer accepted the offer. Defaults to false.
offer.labelstringNoOptional offer label for analytics.
reasonTextstringNoOptional free-text customer note.
context.planstringNoOptional customer plan label.
context.mrrnumberNoOptional monthly recurring revenue used for revenue cohorts.
context.tenureDaysintegerNoOptional customer tenure in days used for lifecycle cohorts.
occurredAtstring(ISO datetime)NoOptional event timestamp. Defaults to current time.
metadataobjectNoOptional flat object (string/number/boolean/null values).

Success Responses

StatusMeaning
201Event created successfully.
200Existing event returned (idempotent replay).

Error Responses

StatusMeaning
400Validation failed (missing/invalid fields).
401Missing or invalid API key.
402Workspace is read-only due to expired subscription access.
403Advanced plan required or demo workspace blocked.
415Content-Type must be application/json.
429Rate limit exceeded.
PATCH/api/v1/cancellations/{eventId}

Updates final business outcome for an existing cancellation event.

Notes

  • Use the same environment as the original POST request.
  • PATCH only sets the final billing outcome; it does not change the reason or offer data.

Availability

Advanced plan only

Auth

Authorization: Bearer <Workspace API Key> or x-api-key

Content-Type

application/json

Path Parameters

FieldTypeRequiredDescription
eventIdstringYesThe eventId from the original POST request.

JSON Body

FieldTypeRequiredDescription
environment"prod" | "sandbox"NoEnvironment for the update.
outcome"churned" | "recovered"YesFinal billing outcome.
effectiveAtstring(ISO datetime)NoOptional timestamp for the effective outcome.
metadataobjectNoOptional flat object recorded with the outcome update.

Success Responses

StatusMeaning
200Outcome updated.

Error Responses

StatusMeaning
400Invalid body or path parameter.
401Missing or invalid API key.
403Advanced plan required or demo workspace blocked.
404Event not found in selected environment.
409Event exists but in the other environment.
415Content-Type must be application/json.
429Rate limit exceeded.

Advanced outbound webhooks

RetentBase can push cancellation.created and cancellation.outcome_updated to your HTTPS endpoint after hosted-flow or custom API events.

Delivery rules: target URLs must use public HTTPS, localhost and private network addresses are rejected, requests time out after roughly 6 seconds, and non-2xx responses are retried with backoff.

Headers sent on every delivery

  • x-retentbase-event: webhook event type.
  • x-retentbase-event-id: RetentBase event identifier.
  • x-retentbase-delivery-id: unique delivery attempt identifier for deduplication.
  • x-retentbase-attempt: retry counter starting at 1.
  • x-retentbase-timestamp: UNIX timestamp included in the signature payload.
  • x-retentbase-signature-sha256: HMAC-SHA256 of timestamp + "." + rawBody.

Verify RetentBase webhook signature

import crypto from "node:crypto";

export async function POST(request: Request) {
  const timestamp = request.headers.get("x-retentbase-timestamp");
  const signature = request.headers.get("x-retentbase-signature-sha256");

  if (!timestamp || !signature) {
    return new Response("Missing signature headers", { status: 400 });
  }

  const unixTimestamp = Number(timestamp);
  if (!Number.isFinite(unixTimestamp) || Math.abs(Date.now() / 1000 - unixTimestamp) > 300) {
    return new Response("Stale webhook timestamp", { status: 401 });
  }

  const rawBody = await request.text();
  const expected = crypto
    .createHmac("sha256", process.env.RETENTBASE_SIGNING_SECRET!)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const valid =
    expected.length === signature.length &&
    crypto.timingSafeEqual(Buffer.from(expected, "utf8"), Buffer.from(signature, "utf8"));

  if (!valid) {
    return new Response("Invalid signature", { status: 401 });
  }

  const deliveryId = request.headers.get("x-retentbase-delivery-id");
  // Persist deliveryId to reject duplicate deliveries idempotently.

  const event = JSON.parse(rawBody);
  return new Response("ok", { status: 200 });
}

Implementation checklist

  • Store workspace API keys and any result-token verification helpers only in server-side code.
  • Use stable `eventId` values to guarantee idempotent retries.
  • Use sandbox mode (`environment: "sandbox"`) before enabling production traffic.
  • Sandbox traffic stays isolated from retention health, weekly issue detection, and scheduled report emails.
  • Set Stripe webhook to `POST /api/stripe/webhook` and include `checkout.session.completed`, `customer.subscription.*`, `invoice.paid`, and `invoice.payment_failed`.
  • When using outbound webhooks, reject stale timestamps, verify `x-retentbase-signature-sha256`, and deduplicate on `x-retentbase-delivery-id`.
  • Use the dashboard billing surface for checkout and Stripe portal access instead of hard-coding plan changes.
  • Monitor `GET /api/health` for liveness and send `Authorization: Bearer <health token>` when you need deep database and Stripe status.
  • Log non-2xx responses with status + body to speed up integration debugging.

Related implementation pages

Use these pages to move from the public API reference into implementation planning, retention workflow design, and vendor evaluation.