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.
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:- Flat Rate: A fixed amount per interval (e.g., UGX 10,000/month).
- Per Unit: Charged based on the number of units (e.g., UGX 5,000 per seat).
- Tiered: Different prices for different volume ranges (e.g., first 100 free, then UGX 100/unit).
- Usage-Based: Metered billing based on reported usage (e.g., UGX 1 per API call).
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
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | The name of the plan (e.g., “Pro Plan”). |
| description | string | No | A brief description of what the plan includes. |
| interval | string | Yes | Billing interval - “month” or “year”. |
| currency | string | Yes | The currency code (e.g., “UGX”, “KES”, “USD”). |
| price | number | Yes | The amount to charge per billing interval. |
| trialDays | number | No | Number of free trial days before billing starts (defaults to 0). |
| metadata | object | No | Arbitrary JSON data for storing additional plan information. |
Examples
- React
- cURL
Copy
// 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
Copy
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>
);
};
Copy
curl -X POST 'https://giant-goldfish-922.convex.site/api/v1/plans' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer $YOUR_CREDIBILL_API_KEY' \
-d '{
"name": "Pro Plan",
"description": "Advanced features for growing teams",
"interval": "month",
"currency": "UGX",
"price": 50000,
"trialDays": 14
}'
Response
When you create a plan, the API responds with the newly created plan’s ID.Copy
{
"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
- React
- cURL
Copy
// 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
Copy
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>
);
};
Copy
curl -X GET 'https://giant-goldfish-922.convex.site/api/v1/plans' \
-H 'Authorization: Bearer $YOUR_CREDIBILL_API_KEY'
Response
When you fetch your app’s plans, the API responds with a list of all configured plans.Copy
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
| planId | string | Yes | The unique ID of the plan to update. |
| name | string | No | Update the plan name. |
| description | string | No | Update the plan description. |
| price | number | No | Update the plan price. |
| trialDays | number | No | Update the trial period in days. |
| metadata | object | No | Update or add additional plan metadata. |
Examples
- React
- cURL
Copy
// 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
Copy
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>
);
};
Copy
# Update plan name and description
curl -X PATCH 'https://giant-goldfish-922.convex.site/api/v1/plans' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer $YOUR_CREDIBILL_API_KEY' \
-d '{
"planId": "k89cq3gkzhiys60bzuf53bh3hte8zx2d",
"name": "Premium Plan",
"description": "Updated premium tier with more features"
}'
# Update plan price
curl -X PATCH 'https://giant-goldfish-922.convex.site/api/v1/plans' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer $YOUR_CREDIBILL_API_KEY' \
-d '{
"planId": "k89cq3gkzhiys60bzuf53bh3hte8zx2d",
"price": 75000
}'
# Update multiple fields at once
curl -X PATCH 'https://giant-goldfish-922.convex.site/api/v1/plans' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer $YOUR_CREDIBILL_API_KEY' \
-d '{
"planId": "k89cq3gkzhiys60bzuf53bh3hte8zx2d",
"name": "Pro Plan",
"price": 60000,
"trialDays": 30
}'
Response
When you update a plan, the API responds with a success message.Copy
{
"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
| Parameter | Type | Required | Description |
|---|---|---|---|
| planId | string | Yes | The unique ID of the plan to delete. |
Examples
- React
- cURL
Copy
// 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
Copy
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>
);
};
Copy
curl -X DELETE 'https://giant-goldfish-922.convex.site/api/v1/plans/delete?planId=k89cq3gkzhiys60bzuf53bh3hte8zx2d' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer $YOUR_CREDIBILL_API_KEY'
Response
When you delete a plan, the API responds with a success message.Copy
{
"success": true,
"message": "Plan deleted"
}