Skip to main content
We strongly advise managing all plan operations (create, read, update, delete) through the Credibill dashboard instead of using the API. The dashboard provides a user-friendly interface for defining pricing structures, managing trials, and configuring complex pricing models. Use the API only when you need programmatic plan operations in exceptional cases.
Plans define the pricing structure, billing interval, and feature set for a subscription.

Plan Structure

A Plan consists of:
  • Name: A human-readable name for the plan (e.g., Pro Plan, Basic Plan).
  • Description: A brief description of what the plan offers.
  • Interval: How often the customer is billed (e.g., month, year).
  • Currency: The currency for the plan (e.g., UGX, KES).
  • Prices: The amount to be charged.
  • Trial Period: Optional free trial duration before billing starts.

Pricing Models

Credibill supports flexible pricing models:
  1. Flat Rate: A fixed amount per interval (e.g., UGX 10,000/month).
  2. Per Unit: Charged based on the number of units (e.g., UGX 5,000 per seat).
  3. Tiered: Different prices for different volume ranges (e.g., first 100 free, then UGX 100/unit).
  4. Usage-Based: Metered billing based on reported usage (e.g., UGX 1 per API call).
Note: Features can be defined within your application and not Credibill.

How to create a plan

Create a new plan with pricing and billing configuration. This endpoint allows you to define how customers are charged when subscribed to this plan.
  • Endpoint: https://giant-goldfish-922.convex.site/api/v1/plans
  • Method: POST
  • Authentication: Authorization: Bearer <CREDIBILL_API_KEY>

Request Body Schema

FieldTypeRequiredDescription
namestringYesThe name of the plan (e.g., “Pro Plan”).
descriptionstringNoA brief description of what the plan includes.
intervalstringYesBilling interval - “month” or “year”.
currencystringYesThe currency code (e.g., “UGX”, “KES”, “USD”).
pricenumberYesThe amount to charge per billing interval.
trialDaysnumberNoNumber of free trial days before billing starts (defaults to 0).
metadataobjectNoArbitrary JSON data for storing additional plan information.

Examples

// src/hooks/useCreatePlan.ts

import { useMutation, UseMutationOptions } from "@tanstack/react-query";

export type PlanData = {
  name: string,
  description?: string,
  interval: "month" | "year",
  currency: string,
  price: number,
  trialDays?: number,
  metadata?: Record<string, any>,
};

export type CreatePlanResponse = {
  success: boolean,
  planId: string,
};

const useApiKey = () => {
// !! REPLACE THIS PLACEHOLDER !!
const apiKey = 'YOUR_CREDIBILL_API_KEY';
return apiKey;
};

const CREDIBILL_URL = 'https://giant-goldfish-922.convex.site';
const ENDPOINT = '/api/v1/plans';

type UseCreatePlanOptions = Omit<UseMutationOptions<
CreatePlanResponse,
Error,
PlanData

> , 'mutationFn'>;

/\*\*

- Custom React Query hook for creating a plan
- @param options - Optional configuration options for useMutation
- @returns The mutation object provided by TanStack Query
  \*/
  export const useCreatePlan = (
  options?: UseCreatePlanOptions
  ) => {
  const apiKey = useApiKey();

return useMutation<CreatePlanResponse, Error, PlanData>({
mutationFn: async (data: PlanData) => {
if (!apiKey) {
throw new Error("API Key is required for authentication.");
}

      const response = await fetch(`${CREDIBILL_URL}${ENDPOINT}`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${apiKey}`,
        },
        body: JSON.stringify(data),
      });

      if (!response.ok) {
        const errorBody = await response.json();
        throw new Error(errorBody.error || `HTTP error! Status: ${response.status}`);
      }

      return response.json();
    },
    ...options,

});
};

Usage in a Component

import React, { useState } from 'react';
import { useCreatePlan } from './hooks/useCreatePlan';

export const CreatePlanForm = () => {
  const [name, setName] = useState('');
  const [description, setDescription] = useState('');
  const [interval, setInterval] = useState<'month' | 'year'>('month');
  const [currency, setCurrency] = useState('UGX');
  const [price, setPrice] = useState('');
  const [trialDays, setTrialDays] = useState('');

  const { mutate, isPending, isError, isSuccess, data, error, reset } = useCreatePlan({
    onSuccess: (responseData) => {
      console.log(`Success! Plan ID: ${responseData.planId}`);
    },
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (isPending) return;

    reset();

    mutate({
      name: name.trim(),
      description: description.trim() || undefined,
      interval,
      currency: currency.trim(),
      price: parseFloat(price),
      trialDays: trialDays ? parseInt(trialDays) : undefined,
    });
  };

  return (
    <div className="p-6 max-w-lg mx-auto bg-white rounded-xl shadow-lg space-y-4">
      <div className="p-3 bg-yellow-100 border border-yellow-400 text-yellow-700 rounded-lg text-sm">
        ⚠️ <strong>Recommendation:</strong> Consider creating plans through the dashboard for a better experience.
      </div>

      <h2 className="text-2xl font-bold text-gray-800">Create a Plan</h2>
      <form onSubmit={handleSubmit} className="space-y-4">
        <div>
          <label htmlFor="name" className="block text-sm font-medium text-gray-700">
            Plan Name (Required)
          </label>
          <input
            id="name"
            type="text"
            value={name}
            onChange={(e) => setName(e.target.value)}
            placeholder="Pro Plan"
            required
            disabled={isPending}
            className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
          />
        </div>

        <div>
          <label htmlFor="description" className="block text-sm font-medium text-gray-700">
            Description (Optional)
          </label>
          <textarea
            id="description"
            value={description}
            onChange={(e) => setDescription(e.target.value)}
            placeholder="Describe what's included in this plan"
            disabled={isPending}
            rows={3}
            className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
          />
        </div>

        <div className="grid grid-cols-2 gap-4">
          <div>
            <label htmlFor="interval" className="block text-sm font-medium text-gray-700">
              Interval (Required)
            </label>
            <select
              id="interval"
              value={interval}
              onChange={(e) => setInterval(e.target.value as 'month' | 'year')}
              disabled={isPending}
              className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
            >
              <option value="month">Monthly</option>
              <option value="year">Yearly</option>
            </select>
          </div>

          <div>
            <label htmlFor="currency" className="block text-sm font-medium text-gray-700">
              Currency (Required)
            </label>
            <input
              id="currency"
              type="text"
              value={currency}
              onChange={(e) => setCurrency(e.target.value.toUpperCase())}
              placeholder="UGX"
              required
              disabled={isPending}
              className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
            />
          </div>
        </div>

        <div className="grid grid-cols-2 gap-4">
          <div>
            <label htmlFor="price" className="block text-sm font-medium text-gray-700">
              Price (Required)
            </label>
            <input
              id="price"
              type="number"
              value={price}
              onChange={(e) => setPrice(e.target.value)}
              placeholder="10000"
              required
              disabled={isPending}
              step="0.01"
              className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
            />
          </div>

          <div>
            <label htmlFor="trialDays" className="block text-sm font-medium text-gray-700">
              Trial Days (Optional)
            </label>
            <input
              id="trialDays"
              type="number"
              value={trialDays}
              onChange={(e) => setTrialDays(e.target.value)}
              placeholder="0"
              disabled={isPending}
              className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
            />
          </div>
        </div>

        <button
          type="submit"
          disabled={isPending}
          className="w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition duration-150 ease-in-out disabled:opacity-50"
        >
          {isPending ? 'Processing...' : 'Create Plan'}
        </button>
      </form>

      {isSuccess && (
        <div className="mt-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded-lg">
          Success! Plan ID: <span className="font-mono">{data?.planId}</span>
        </div>
      )}
      {isError && (
        <div className="mt-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded-lg">
          Error: {error?.message}
        </div>
      )}
    </div>
  );
};

Response

When you create a plan, the API responds with the newly created plan’s ID.
{
  "success": true,
  "planId": "k89cq3gkzhiys60bzuf53bh3hte8zx2d"
}

Getting your app’s plans

Retrieve all plans configured for your application. This endpoint returns a list of all available plans that customers can subscribe to.
  • Endpoint: https://giant-goldfish-922.convex.site/api/v1/plans
  • Method: GET
  • Authentication: Authorization: Bearer <CREDIBILL_API_KEY>

Examples

// src/hooks/useGetPlans.ts

import { useQuery, UseQueryOptions } from "@tanstack/react-query";

export type Plan = {
  _id: string,
  name: string,
  description?: string,
  interval: "month" | "year",
  currency: string,
  price: number,
  trialDays?: number,
  metadata?: Record<string, any>,
  [key: string]: any,
};

export type GetPlansResponse = {
  plans: Plan[],
};

const useApiKey = () => {
// !! REPLACE THIS PLACEHOLDER !!
const apiKey = 'YOUR_CREDIBILL_API_KEY';
return apiKey;
};

const CREDIBILL_URL = 'https://giant-goldfish-922.convex.site';
const ENDPOINT = '/api/v1/plans';

type UseGetPlansOptions = Omit<UseQueryOptions<
GetPlansResponse,
Error

> , 'queryFn' | 'queryKey'>;

/\*\*

- Custom React Query hook for fetching all plans for your app
- @param options - Optional configuration options for useQuery
- @returns The query object provided by TanStack Query
  \*/
  export const useGetPlans = (
  options?: UseGetPlansOptions
  ) => {
  const apiKey = useApiKey();

return useQuery<GetPlansResponse, Error>({
queryKey: ['plans'],
queryFn: async () => {
if (!apiKey) {
throw new Error("API Key is required for authentication.");
}

      const response = await fetch(`${CREDIBILL_URL}${ENDPOINT}`, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${apiKey}`,
        },
      });

      if (!response.ok) {
        const errorBody = await response.json();
        throw new Error(errorBody.error || `HTTP error! Status: ${response.status}`);
      }

      return response.json();
    },
    ...options,

});
};

Usage in a Component

import React from 'react';
import { useGetPlans } from './hooks/useGetPlans';

export const PlansShowcase = () => {
  const { data, isLoading, isError, error } = useGetPlans();

  if (isLoading) {
    return <div className="text-center py-8">Loading plans...</div>;
  }

  if (isError) {
    return (
      <div className="p-3 bg-red-100 border border-red-400 text-red-700 rounded-lg">
        Error: {error?.message}
      </div>
    );
  }

  return (
    <div className="p-6 max-w-4xl mx-auto">
      <h2 className="text-3xl font-bold text-gray-800 mb-8">Choose Your Plan</h2>

      {data?.plans && data.plans.length > 0 ? (
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          {data.plans.map((plan) => (
            <div
              key={plan._id}
              className="p-6 border border-gray-200 rounded-lg shadow-md hover:shadow-lg transition-shadow"
            >
              <h3 className="text-xl font-bold text-gray-800 mb-2">{plan.name}</h3>
              {plan.description && (
                <p className="text-sm text-gray-600 mb-4">{plan.description}</p>
              )}

              <div className="mb-4">
                <span className="text-3xl font-bold text-gray-900">{plan.price}</span>
                <span className="text-sm text-gray-600 ml-2">{plan.currency}</span>
                <span className="text-sm text-gray-600">/{plan.interval}</span>
              </div>

              {plan.trialDays && plan.trialDays > 0 && (
                <div className="mb-4 p-2 bg-green-100 rounded">
                  <p className="text-sm text-green-800">
                    🎉 {plan.trialDays}-day free trial
                  </p>
                </div>
              )}

              <button className="w-full py-2 px-4 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors">
                Subscribe Now
              </button>
            </div>
          ))}
        </div>
      ) : (
        <div className="p-4 bg-gray-100 rounded-lg text-center">
          <p className="text-gray-600">No plans available yet.</p>
        </div>
      )}
    </div>
  );
};

Response

When you fetch your app’s plans, the API responds with a list of all configured plans.
{
  "plans": [
    {
      "_id": "k89cq3gkzhiys60bzuf53bh3hte8zx2d",
      "name": "Basic Plan",
      "description": "Perfect for getting started",
      "interval": "month",
      "currency": "UGX",
      "price": 10000,
      "trialDays": 7
    },
    {
      "_id": "l98dro4hlizjt71cvwg64cj4juf9ze3e",
      "name": "Pro Plan",
      "description": "Advanced features for growing teams",
      "interval": "month",
      "currency": "UGX",
      "price": 50000,
      "trialDays": 14
    },
    {
      "_id": "m01eso5imjzku82dwxh75dk5kug0af4f",
      "name": "Enterprise Plan",
      "description": "Custom solutions for large organizations",
      "interval": "year",
      "currency": "UGX",
      "price": 500000,
      "trialDays": null
    }
  ]
}

How to update a plan

Update an existing plan’s configuration. You can modify the name, description, price, trial period, and other plan attributes.
  • Endpoint: https://giant-goldfish-922.convex.site/api/v1/plans
  • Method: PATCH
  • Authentication: Authorization: Bearer <CREDIBILL_API_KEY>

Request Body Schema

FieldTypeRequiredDescription
planIdstringYesThe unique ID of the plan to update.
namestringNoUpdate the plan name.
descriptionstringNoUpdate the plan description.
pricenumberNoUpdate the plan price.
trialDaysnumberNoUpdate the trial period in days.
metadataobjectNoUpdate or add additional plan metadata.

Examples

// src/hooks/useUpdatePlan.ts

import { useMutation, UseMutationOptions } from "@tanstack/react-query";

export type UpdatePlanData = {
  planId: string,
  name?: string,
  description?: string,
  price?: number,
  trialDays?: number,
  metadata?: Record<string, any>,
};

export type UpdatePlanResponse = {
  success: boolean,
  message: string,
};

const useApiKey = () => {
// !! REPLACE THIS PLACEHOLDER !!
const apiKey = 'YOUR_CREDIBILL_API_KEY';
return apiKey;
};

const CREDIBILL_URL = 'https://giant-goldfish-922.convex.site';
const ENDPOINT = '/api/v1/plans';

type UseUpdatePlanOptions = Omit<UseMutationOptions<
UpdatePlanResponse,
Error,
UpdatePlanData

> , 'mutationFn'>;

/\*\*

- Custom React Query hook for updating a plan
- @param options - Optional configuration options for useMutation
- @returns The mutation object provided by TanStack Query
  \*/
  export const useUpdatePlan = (
  options?: UseUpdatePlanOptions
  ) => {
  const apiKey = useApiKey();

return useMutation<UpdatePlanResponse, Error, UpdatePlanData>({
mutationFn: async (data: UpdatePlanData) => {
if (!apiKey) {
throw new Error("API Key is required for authentication.");
}

      const response = await fetch(`${CREDIBILL_URL}${ENDPOINT}`, {
        method: 'PATCH',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${apiKey}`,
        },
        body: JSON.stringify(data),
      });

      if (!response.ok) {
        const errorBody = await response.json();
        throw new Error(errorBody.error || `HTTP error! Status: ${response.status}`);
      }

      return response.json();
    },
    ...options,

});
};

Usage in a Component

import React, { useState } from 'react';
import { useUpdatePlan } from './hooks/useUpdatePlan';

export const UpdatePlanForm = ({ planId, currentPlan }: {
  planId: string;
  currentPlan?: { name: string; description?: string; price: number; trialDays?: number };
}) => {
  const [name, setName] = useState(currentPlan?.name || '');
  const [description, setDescription] = useState(currentPlan?.description || '');
  const [price, setPrice] = useState(String(currentPlan?.price) || '');
  const [trialDays, setTrialDays] = useState(String(currentPlan?.trialDays || ''));

  const { mutate, isPending, isError, isSuccess, error, reset } = useUpdatePlan({
    onSuccess: () => {
      console.log('Plan updated successfully');
    },
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (isPending) return;

    reset();

    const updates: Record<string, any> = { planId };
    if (name.trim()) updates.name = name.trim();
    if (description.trim()) updates.description = description.trim();
    if (price) updates.price = parseFloat(price);
    if (trialDays) updates.trialDays = parseInt(trialDays);

    mutate(updates as UpdatePlanData);
  };

  return (
    <div className="p-6 max-w-lg mx-auto bg-white rounded-xl shadow-lg space-y-4">
      <h2 className="text-2xl font-bold text-gray-800">Update Plan</h2>
      <form onSubmit={handleSubmit} className="space-y-4">
        <div>
          <label htmlFor="planId" className="block text-sm font-medium text-gray-700">
            Plan ID
          </label>
          <input
            id="planId"
            type="text"
            value={planId}
            disabled
            className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-100 text-gray-600"
          />
        </div>

        <div>
          <label htmlFor="name" className="block text-sm font-medium text-gray-700">
            Plan Name (Optional)
          </label>
          <input
            id="name"
            type="text"
            value={name}
            onChange={(e) => setName(e.target.value)}
            placeholder="Leave blank to keep current"
            disabled={isPending}
            className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
          />
        </div>

        <div>
          <label htmlFor="description" className="block text-sm font-medium text-gray-700">
            Description (Optional)
          </label>
          <textarea
            id="description"
            value={description}
            onChange={(e) => setDescription(e.target.value)}
            placeholder="Leave blank to keep current"
            disabled={isPending}
            rows={3}
            className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
          />
        </div>

        <div className="grid grid-cols-2 gap-4">
          <div>
            <label htmlFor="price" className="block text-sm font-medium text-gray-700">
              Price (Optional)
            </label>
            <input
              id="price"
              type="number"
              value={price}
              onChange={(e) => setPrice(e.target.value)}
              placeholder="Leave blank to keep current"
              disabled={isPending}
              step="0.01"
              className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
            />
          </div>

          <div>
            <label htmlFor="trialDays" className="block text-sm font-medium text-gray-700">
              Trial Days (Optional)
            </label>
            <input
              id="trialDays"
              type="number"
              value={trialDays}
              onChange={(e) => setTrialDays(e.target.value)}
              placeholder="Leave blank to keep current"
              disabled={isPending}
              className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
            />
          </div>
        </div>

        <button
          type="submit"
          disabled={isPending}
          className="w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition duration-150 ease-in-out disabled:opacity-50"
        >
          {isPending ? 'Processing...' : 'Update Plan'}
        </button>
      </form>

      {isSuccess && (
        <div className="mt-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded-lg">
          Success! Plan has been updated.
        </div>
      )}
      {isError && (
        <div className="mt-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded-lg">
          Error: {error?.message}
        </div>
      )}
    </div>
  );
};

Response

When you update a plan, the API responds with a success message.
{
  "success": true,
  "message": "Plan updated"
}

How to delete a plan

Delete a plan from your application. This action removes the plan and prevents new subscriptions from being created with this plan. Existing subscriptions are not affected.
  • Endpoint: https://giant-goldfish-922.convex.site/api/v1/plans/delete
  • Method: DELETE
  • Authentication: Authorization: Bearer <CREDIBILL_API_KEY>

Query Parameters

ParameterTypeRequiredDescription
planIdstringYesThe unique ID of the plan to delete.

Examples

// src/hooks/useDeletePlan.ts

import { useMutation, UseMutationOptions } from "@tanstack/react-query";

export type DeletePlanData = {
  planId: string,
};

export type DeletePlanResponse = {
  success: boolean,
  message: string,
};

const useApiKey = () => {
// !! REPLACE THIS PLACEHOLDER !!
const apiKey = 'YOUR_CREDIBILL_API_KEY';
return apiKey;
};

const CREDIBILL_URL = 'https://giant-goldfish-922.convex.site';
const ENDPOINT = '/api/v1/plans/delete';

type UseDeletePlanOptions = Omit<UseMutationOptions<
DeletePlanResponse,
Error,
DeletePlanData

> , 'mutationFn'>;

/\*\*

- Custom React Query hook for deleting a plan
- @param options - Optional configuration options for useMutation
- @returns The mutation object provided by TanStack Query
  \*/
  export const useDeletePlan = (
  options?: UseDeletePlanOptions
  ) => {
  const apiKey = useApiKey();

return useMutation<DeletePlanResponse, Error, DeletePlanData>({
mutationFn: async (data: DeletePlanData) => {
if (!apiKey) {
throw new Error("API Key is required for authentication.");
}

      const url = new URL(`${CREDIBILL_URL}${ENDPOINT}`);
      url.searchParams.append('planId', data.planId);

      const response = await fetch(url.toString(), {
        method: 'DELETE',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${apiKey}`,
        },
      });

      if (!response.ok) {
        const errorBody = await response.json();
        throw new Error(errorBody.error || `HTTP error! Status: ${response.status}`);
      }

      return response.json();
    },
    ...options,

});
};

Usage in a Component

import React, { useState } from 'react';
import { useDeletePlan } from './hooks/useDeletePlan';

export const DeletePlanForm = () => {
  const [planId, setPlanId] = useState('');
  const [showConfirm, setShowConfirm] = useState(false);

  const { mutate, isPending, isError, isSuccess, error, reset } = useDeletePlan({
    onSuccess: () => {
      console.log('Plan deleted successfully');
      setPlanId('');
      setShowConfirm(false);
    },
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (isPending) return;

    reset();
    mutate({ planId: planId.trim() });
  };

  return (
    <div className="p-6 max-w-lg mx-auto bg-white rounded-xl shadow-lg space-y-4">
      <h2 className="text-2xl font-bold text-gray-800">Delete Plan</h2>
      <form onSubmit={handleSubmit} className="space-y-4">
        <div>
          <label htmlFor="planId" className="block text-sm font-medium text-gray-700">
            Plan ID (Required)
          </label>
          <input
            id="planId"
            type="text"
            value={planId}
            onChange={(e) => setPlanId(e.target.value)}
            placeholder="Enter the plan ID to delete"
            required
            disabled={isPending}
            className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
          />
        </div>

        {planId && !showConfirm && (
          <button
            type="button"
            onClick={() => setShowConfirm(true)}
            disabled={isPending}
            className="w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-yellow-600 hover:bg-yellow-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-500 transition duration-150 ease-in-out disabled:opacity-50"
          >
            Next: Confirm Deletion
          </button>
        )}

        {showConfirm && (
          <div className="p-4 bg-red-50 border border-red-200 rounded-lg">
            <p className="text-sm font-semibold text-red-800 mb-3">
              ⚠️ Are you sure you want to delete this plan?
            </p>
            <p className="text-sm text-red-700 mb-4">
              This action cannot be undone. Existing subscriptions to this plan will not be affected, but new subscriptions cannot be created.
            </p>
            <div className="flex gap-2">
              <button
                type="submit"
                disabled={isPending}
                className="flex-1 py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition duration-150 ease-in-out disabled:opacity-50"
              >
                {isPending ? 'Deleting...' : 'Delete Plan'}
              </button>
              <button
                type="button"
                onClick={() => setShowConfirm(false)}
                disabled={isPending}
                className="flex-1 py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition duration-150 ease-in-out disabled:opacity-50"
              >
                Cancel
              </button>
            </div>
          </div>
        )}
      </form>

      {isSuccess && (
        <div className="mt-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded-lg">
          Success! Plan has been deleted.
        </div>
      )}
      {isError && (
        <div className="mt-4 p-3 bg-red-100 border border-red-400 text-red-700 rounded-lg">
          Error: {error?.message}
        </div>
      )}
    </div>
  );
};

Response

When you delete a plan, the API responds with a success message.
{
  "success": true,
  "message": "Plan deleted"
}