Skip to main content
A Subscription connects a Customer to a Plan. It governs the generation of invoices and the automatic collection of payments on a recurring schedule.

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.
Proration cannot be disabled.

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

FieldTypeRequiredDescription
customerIdstringYesThe unique ID of the customer to subscribe.
planIdstringYesThe unique ID of the plan to subscribe the customer to.
startDatestringNoISO 8601 date string for when the subscription should start (defaults to today).

Examples

// 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>
  );
};

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

ParameterTypeRequiredDescription
subscriptionIdstringNoThe unique ID of the subscription. If provided, fetches a single subscription.
customerIdstringNoFilter subscriptions by customer ID.
planIdstringNoFilter subscriptions by plan ID.
statusstringNoFilter subscriptions by status (e.g., “active”, “canceled”, “past_due”).

Examples

// 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>
  );
};

Response Examples

Get Single Subscription:
{
  "subscription": {
    "_id": "m90dro4hlizjt71cvwg64cj4juf9ze3e",
    "customerId": "j97bp2fjzhbx59ayye52ag2gsd7ydw1c",
    "planId": "k89cq3gkzhiys60bzuf53bh3hte8zx2d",
    "status": "active",
    "startDate": "2024-01-15",
    "nextBillingDate": "2024-02-15"
  }
}
List Subscriptions:
{
  "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

ParameterTypeRequiredDescription
subscriptionIdstringYesThe unique ID of the subscription to cancel.
cancelAtPeriodEndbooleanNoIf “true”, cancels at the end of the current billing period. Otherwise, immediate.

Examples

// 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>
  );
};

Response

When you cancel a subscription, the API responds with a success message.
{
  "success": true,
  "message": "Subscription cancelled"
}