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:
- 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).
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
| 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
// 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>
);
};
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.
{
"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>
);
};
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.
{
"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
// 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>
);
};
# 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.
{
"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
// 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>
);
};
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.
{
"success": true,
"message": "Plan deleted"
}