Subscription Lifecycle
Subscriptions move through several states:1
Trialing
The subscription is in a free trial period. No payment has been collected
yet.
2
Active
Payment has been successfully collected, and the customer has access to the
service.
3
Past Due
Payment failed (e.g., expired card). Credibill will retry according to your
retry settings. The customer may still have access.
4
Unpaid
Retries have been exhausted. Access should usually be revoked. This only
applies to card payments.
5
Canceled
The subscription has been terminated. No further invoices will be generated.
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
// 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
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>
);
};
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.{
"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
// 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
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>
);
};
# 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:{
"subscription": {
"_id": "m90dro4hlizjt71cvwg64cj4juf9ze3e",
"customerId": "j97bp2fjzhbx59ayye52ag2gsd7ydw1c",
"planId": "k89cq3gkzhiys60bzuf53bh3hte8zx2d",
"status": "active",
"startDate": "2024-01-15",
"nextBillingDate": "2024-02-15"
}
}
{
"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
// 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
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>
);
};
# 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.{
"success": true,
"message": "Subscription cancelled"
}