Subscription Lifecycle
Subscriptions move through several states:Past Due
Payment failed (e.g., expired card). Credibill will retry according to your
retry settings. The customer may still have access.
Unpaid
Retries have been exhausted. Access should usually be revoked. This only
applies to card payments.
Proration
When a customer changes plans (upgrades or downgrades) in the middle of a billing cycle, Credibill automatically calculates Proration.- Upgrade: Charged immediately for the difference.
- Downgrade: Nothing happens.
How to subscribe a customer
Subscribe a customer to a plan, which creates a recurring billing relationship and starts generating invoices on the specified schedule.- Endpoint:
https://giant-goldfish-922.convex.site/api/v1/subscriptions/create - Method:
POST - Authentication: Authorization:
Bearer <CREDIBILL_API_KEY>
Request Body Schema
| Field | Type | Required | Description |
|---|---|---|---|
| customerId | string | Yes | The unique ID of the customer to subscribe. |
| planId | string | Yes | The unique ID of the plan to subscribe the customer to. |
| startDate | string | No | ISO 8601 date string for when the subscription should start (defaults to today). |
Examples
- React
- cURL
Copy
// src/hooks/useSubscribeCustomer.ts
import { useMutation, UseMutationOptions } from "@tanstack/react-query";
export type SubscriptionData = {
customerId: string,
planId: string,
startDate?: string,
};
export type SubscribeCustomerResponse = {
success: boolean,
subscriptionId: string,
status: 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/subscriptions/create';
type UseSubscribeCustomerOptions = Omit<UseMutationOptions<
SubscribeCustomerResponse,
Error,
SubscriptionData
> , 'mutationFn'>;
/\*\*
- Custom React Query hook for subscribing a customer to a plan
- @param options - Optional configuration options for useMutation
- @returns The mutation object provided by TanStack Query
\*/
export const useSubscribeCustomer = (
options?: UseSubscribeCustomerOptions
) => {
const apiKey = useApiKey();
return useMutation<SubscribeCustomerResponse, Error, SubscriptionData>({
mutationFn: async (data: SubscriptionData) => {
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 { useSubscribeCustomer } from './hooks/useSubscribeCustomer';
export const SubscribeCustomerForm = () => {
const [customerId, setCustomerId] = useState('');
const [planId, setPlanId] = useState('');
const [startDate, setStartDate] = useState('');
const { mutate, isPending, isError, isSuccess, data, error, reset } = useSubscribeCustomer({
onSuccess: (responseData) => {
console.log(`Success! Subscription ID: ${responseData.subscriptionId}`);
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (isPending) return;
reset();
mutate({
customerId: customerId.trim(),
planId: planId.trim(),
startDate: startDate.trim() || undefined,
});
};
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">Subscribe Customer to Plan</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="customerId" className="block text-sm font-medium text-gray-700">
Customer ID (Required)
</label>
<input
id="customerId"
type="text"
value={customerId}
onChange={(e) => setCustomerId(e.target.value)}
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="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)}
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="startDate" className="block text-sm font-medium text-gray-700">
Start Date (Optional - ISO 8601)
</label>
<input
id="startDate"
type="text"
placeholder="2024-01-15"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
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>
<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...' : 'Subscribe Customer'}
</button>
</form>
{isSuccess && (
<div className="mt-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded-lg">
Success! Subscription ID: <span className="font-mono">{data?.subscriptionId}</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/subscriptions/create' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer $YOUR_CREDIBILL_API_KEY' \
-d '{
"customerId": "j97bp2fjzhbx59ayye52ag2gsd7ydw1c",
"planId": "k89cq3gkzhiys60bzuf53bh3hte8zx2d",
"startDate": "2024-01-15"
}'
Response
When you subscribe a customer to a plan, the API responds with a JSON object containing the subscription details.Copy
{
"success": true,
"subscriptionId": "m90dro4hlizjt71cvwg64cj4juf9ze3e",
"status": "active",
"customerId": "j97bp2fjzhbx59ayye52ag2gsd7ydw1c",
"planId": "k89cq3gkzhiys60bzuf53bh3hte8zx2d",
"startDate": "2024-01-15",
"nextBillingDate": "2024-02-15"
}
Getting Subscriptions
Retrieve subscription information by fetching a specific subscription or listing subscriptions with optional filters.- Endpoint:
https://giant-goldfish-922.convex.site/api/subscriptions - Method:
GET - Authentication: Authorization:
Bearer <CREDIBILL_API_KEY>
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| subscriptionId | string | No | The unique ID of the subscription. If provided, fetches a single subscription. |
| customerId | string | No | Filter subscriptions by customer ID. |
| planId | string | No | Filter subscriptions by plan ID. |
| status | string | No | Filter subscriptions by status (e.g., “active”, “canceled”, “past_due”). |
Examples
- React
- cURL
Copy
// src/hooks/useGetSubscriptions.ts
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
export type Subscription = {
_id: string,
customerId: string,
planId: string,
status: "trialing" | "active" | "past_due" | "unpaid" | "canceled",
startDate: string,
nextBillingDate: string,
[key: string]: any,
};
export type GetSubscriptionsResponse = {
subscription?: Subscription,
subscriptions?: Subscription[],
};
export type GetSubscriptionsFilters = {
subscriptionId?: string,
customerId?: string,
planId?: string,
status?: 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/subscriptions';
type UseGetSubscriptionsOptions = Omit<UseQueryOptions<
GetSubscriptionsResponse,
Error
> , 'queryFn' | 'queryKey'>;
/\*\*
- Custom React Query hook for fetching subscription(s)
- @param filters - Optional filters (subscriptionId, customerId, planId, status)
- @param options - Optional configuration options for useQuery
- @returns The query object provided by TanStack Query
\*/
export const useGetSubscriptions = (
filters?: GetSubscriptionsFilters,
options?: UseGetSubscriptionsOptions
) => {
const apiKey = useApiKey();
return useQuery<GetSubscriptionsResponse, Error>({
queryKey: ['subscriptions', filters],
queryFn: async () => {
if (!apiKey) {
throw new Error("API Key is required for authentication.");
}
const url = new URL(`${CREDIBILL_URL}${ENDPOINT}`);
if (filters?.subscriptionId) {
url.searchParams.append('subscriptionId', filters.subscriptionId);
}
if (filters?.customerId) {
url.searchParams.append('customerId', filters.customerId);
}
if (filters?.planId) {
url.searchParams.append('planId', filters.planId);
}
if (filters?.status) {
url.searchParams.append('status', filters.status);
}
const response = await fetch(url.toString(), {
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, { useState } from 'react';
import { useGetSubscriptions } from './hooks/useGetSubscriptions';
export const SubscriptionsViewer = () => {
const [customerId, setCustomerId] = useState('');
const [status, setStatus] = useState('');
const { data, isLoading, isError, error } = useGetSubscriptions({
customerId: customerId || undefined,
status: status || undefined,
});
return (
<div className="p-6 max-w-2xl mx-auto bg-white rounded-xl shadow-lg space-y-4">
<h2 className="text-2xl font-bold text-gray-800">View Subscriptions</h2>
<div className="space-y-3">
<div>
<label htmlFor="customerId" className="block text-sm font-medium text-gray-700">
Customer ID (Optional)
</label>
<input
id="customerId"
type="text"
value={customerId}
onChange={(e) => setCustomerId(e.target.value)}
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="status" className="block text-sm font-medium text-gray-700">
Status (Optional)
</label>
<select
id="status"
value={status}
onChange={(e) => setStatus(e.target.value)}
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="">All Statuses</option>
<option value="active">Active</option>
<option value="trialing">Trialing</option>
<option value="past_due">Past Due</option>
<option value="unpaid">Unpaid</option>
<option value="canceled">Canceled</option>
</select>
</div>
</div>
{isLoading && <p className="text-gray-600">Loading subscriptions...</p>}
{isError && (
<div className="p-3 bg-red-100 border border-red-400 text-red-700 rounded-lg">
Error: {error?.message}
</div>
)}
{data?.subscriptions && data.subscriptions.length > 0 && (
<div className="mt-6">
<h3 className="text-lg font-semibold text-gray-700 mb-4">Subscriptions</h3>
<div className="space-y-3">
{data.subscriptions.map((sub) => (
<div key={sub._id} className="p-4 border border-gray-200 rounded-lg">
<p className="font-mono text-sm text-gray-600">ID: {sub._id}</p>
<p className="text-sm text-gray-700">Status: <span className="font-semibold">{sub.status}</span></p>
<p className="text-sm text-gray-700">Next Billing: {new Date(sub.nextBillingDate).toLocaleDateString()}</p>
</div>
))}
</div>
</div>
)}
{data?.subscription && (
<div className="mt-6 p-4 border border-gray-200 rounded-lg bg-gray-50">
<h3 className="text-lg font-semibold text-gray-700 mb-3">Subscription Details</h3>
<p className="font-mono text-sm text-gray-600">ID: {data.subscription._id}</p>
<p className="text-sm text-gray-700">Status: <span className="font-semibold">{data.subscription.status}</span></p>
<p className="text-sm text-gray-700">Next Billing: {new Date(data.subscription.nextBillingDate).toLocaleDateString()}</p>
</div>
)}
</div>
);
};
Copy
# Get a single subscription by ID
curl -X GET 'https://giant-goldfish-922.convex.site/api/subscriptions?subscriptionId=m90dro4hlizjt71cvwg64cj4juf9ze3e' \
-H 'Authorization: Bearer $YOUR_CREDIBILL_API_KEY'
# List all subscriptions for a customer
curl -X GET 'https://giant-goldfish-922.convex.site/api/subscriptions?customerId=j97bp2fjzhbx59ayye52ag2gsd7ydw1c' \
-H 'Authorization: Bearer $YOUR_CREDIBILL_API_KEY'
# List active subscriptions for a plan
curl -X GET 'https://giant-goldfish-922.convex.site/api/subscriptions?planId=k89cq3gkzhiys60bzuf53bh3hte8zx2d&status=active' \
-H 'Authorization: Bearer $YOUR_CREDIBILL_API_KEY'
# List all subscriptions
curl -X GET 'https://giant-goldfish-922.convex.site/api/subscriptions' \
-H 'Authorization: Bearer $YOUR_CREDIBILL_API_KEY'
Response Examples
Get Single Subscription:Copy
{
"subscription": {
"_id": "m90dro4hlizjt71cvwg64cj4juf9ze3e",
"customerId": "j97bp2fjzhbx59ayye52ag2gsd7ydw1c",
"planId": "k89cq3gkzhiys60bzuf53bh3hte8zx2d",
"status": "active",
"startDate": "2024-01-15",
"nextBillingDate": "2024-02-15"
}
}
Copy
{
"subscriptions": [
{
"_id": "m90dro4hlizjt71cvwg64cj4juf9ze3e",
"customerId": "j97bp2fjzhbx59ayye52ag2gsd7ydw1c",
"planId": "k89cq3gkzhiys60bzuf53bh3hte8zx2d",
"status": "active",
"startDate": "2024-01-15",
"nextBillingDate": "2024-02-15"
},
{
"_id": "n01eso5imjzku82dwxh75dk5kug0af4f",
"customerId": "j97bp2fjzhbx59ayye52ag2gsd7ydw1c",
"planId": "l98dro4hlizjt71cvwg64cj4juf9ze3e",
"status": "canceled",
"startDate": "2023-12-01",
"nextBillingDate": null
}
]
}
How to cancel a subscription
Cancel a customer’s subscription. You can choose to cancel immediately or at the end of the current billing period.- Endpoint:
https://giant-goldfish-922.convex.site/api/v1/subscriptions/cancel - Method:
DELETE - Authentication: Authorization:
Bearer <CREDIBILL_API_KEY>
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| subscriptionId | string | Yes | The unique ID of the subscription to cancel. |
| cancelAtPeriodEnd | boolean | No | If “true”, cancels at the end of the current billing period. Otherwise, immediate. |
Examples
- React
- cURL
Copy
// src/hooks/useCancelSubscription.ts
import { useMutation, UseMutationOptions } from "@tanstack/react-query";
export type CancelSubscriptionData = {
subscriptionId: string,
cancelAtPeriodEnd?: boolean,
};
export type CancelSubscriptionResponse = {
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/subscriptions/cancel';
type UseCancelSubscriptionOptions = Omit<UseMutationOptions<
CancelSubscriptionResponse,
Error,
CancelSubscriptionData
> , 'mutationFn'>;
/\*\*
- Custom React Query hook for canceling a subscription
- @param options - Optional configuration options for useMutation
- @returns The mutation object provided by TanStack Query
\*/
export const useCancelSubscription = (
options?: UseCancelSubscriptionOptions
) => {
const apiKey = useApiKey();
return useMutation<CancelSubscriptionResponse, Error, CancelSubscriptionData>({
mutationFn: async (data: CancelSubscriptionData) => {
if (!apiKey) {
throw new Error("API Key is required for authentication.");
}
const url = new URL(`${CREDIBILL_URL}${ENDPOINT}`);
url.searchParams.append('subscriptionId', data.subscriptionId);
if (data.cancelAtPeriodEnd !== undefined) {
url.searchParams.append('cancelAtPeriodEnd', String(data.cancelAtPeriodEnd));
}
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 { useCancelSubscription } from './hooks/useCancelSubscription';
export const CancelSubscriptionForm = () => {
const [subscriptionId, setSubscriptionId] = useState('');
const [cancelAtPeriodEnd, setCancelAtPeriodEnd] = useState(false);
const { mutate, isPending, isError, isSuccess, error, reset } = useCancelSubscription({
onSuccess: () => {
console.log('Subscription cancelled successfully');
setSubscriptionId('');
setCancelAtPeriodEnd(false);
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (isPending) return;
reset();
mutate({
subscriptionId: subscriptionId.trim(),
cancelAtPeriodEnd,
});
};
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">Cancel Subscription</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="subscriptionId" className="block text-sm font-medium text-gray-700">
Subscription ID (Required)
</label>
<input
id="subscriptionId"
type="text"
value={subscriptionId}
onChange={(e) => setSubscriptionId(e.target.value)}
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 className="flex items-center">
<input
id="cancelAtPeriodEnd"
type="checkbox"
checked={cancelAtPeriodEnd}
onChange={(e) => setCancelAtPeriodEnd(e.target.checked)}
disabled={isPending}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="cancelAtPeriodEnd" className="ml-2 block text-sm text-gray-700">
Cancel at end of billing period (otherwise cancel immediately)
</label>
</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-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 ? 'Processing...' : 'Cancel Subscription'}
</button>
</form>
{isSuccess && (
<div className="mt-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded-lg">
Success! Subscription has been cancelled.
</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
# Cancel subscription immediately
curl -X DELETE 'https://giant-goldfish-922.convex.site/api/v1/subscriptions/cancel?subscriptionId=m90dro4hlizjt71cvwg64cj4juf9ze3e' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer $YOUR_CREDIBILL_API_KEY'
# Cancel subscription at end of billing period
curl -X DELETE 'https://giant-goldfish-922.convex.site/api/v1/subscriptions/cancel?subscriptionId=m90dro4hlizjt71cvwg64cj4juf9ze3e&cancelAtPeriodEnd=true' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer $YOUR_CREDIBILL_API_KEY'
Response
When you cancel a subscription, the API responds with a success message.Copy
{
"success": true,
"message": "Subscription cancelled"
}