Skip to main content
For plans with usage-based pricing, you must report usage to Credibill so we know how much to charge.

Recording Usage Events

Report usage events for a subscription to track consumption. Each event is recorded with a quantity and metric type for billing purposes.
  • Endpoint: https://giant-goldfish-922.convex.site/api/v1/usage
  • Method: POST
  • Authentication: Authorization: Bearer <CREDIBILL_API_KEY>

Request Body Schema

FieldTypeRequiredDescription
subscriptionIdstringYesThe unique ID of the subscription to record usage for.
quantitynumberYesThe quantity consumed (must be a positive number).
metricstringYesThe metric type (e.g., “api_calls”, “storage_gb”, “users”).
eventIdstringNoUnique event identifier for idempotency (prevents duplicate charges on retries).
metadataobjectNoArbitrary JSON data for additional context about this usage event.

Examples

// src/hooks/useRecordUsage.ts

import { useMutation, UseMutationOptions } from "@tanstack/react-query";

export type UsageEventData = {
  subscriptionId: string,
  quantity: number,
  metric: string,
  eventId?: string,
  metadata?: Record<string, any>,
};

export type RecordUsageResponse = {
  success: boolean,
  usageEventId: string,
  duplicate: boolean,
};

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/usage';

type UseRecordUsageOptions = Omit<UseMutationOptions<
RecordUsageResponse,
Error,
UsageEventData

> , 'mutationFn'>;

/\*\*

- Custom React Query hook for recording usage events
- @param options - Optional configuration options for useMutation
- @returns The mutation object provided by TanStack Query
  \*/
  export const useRecordUsage = (
  options?: UseRecordUsageOptions
  ) => {
  const apiKey = useApiKey();

return useMutation<RecordUsageResponse, Error, UsageEventData>({
mutationFn: async (data: UsageEventData) => {
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 { useRecordUsage } from './hooks/useRecordUsage';

export const RecordUsageForm = () => {
  const [subscriptionId, setSubscriptionId] = useState('');
  const [quantity, setQuantity] = useState('');
  const [metric, setMetric] = useState('api_calls');
  const [eventId, setEventId] = useState('');

  const { mutate, isPending, isError, isSuccess, data, error, reset } = useRecordUsage({
    onSuccess: (responseData) => {
      if (responseData.duplicate) {
        console.log('Event already recorded (duplicate prevention)');
      } else {
        console.log(`Usage recorded! Event ID: ${responseData.usageEventId}`);
      }
    },
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (isPending) return;

    reset();

    // Generate a unique event ID if not provided (for idempotency)
    const finalEventId = eventId || `event-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;

    mutate({
      subscriptionId: subscriptionId.trim(),
      quantity: parseFloat(quantity),
      metric: metric.trim(),
      eventId: finalEventId,
    });
  };

  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">Record Usage Event</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)}
            placeholder="m90dro4hlizjt71cvwg64cj4juf9ze3e"
            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="quantity" className="block text-sm font-medium text-gray-700">
            Quantity (Required)
          </label>
          <input
            id="quantity"
            type="number"
            value={quantity}
            onChange={(e) => setQuantity(e.target.value)}
            placeholder="100"
            required
            disabled={isPending}
            step="0.01"
            min="0"
            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="metric" className="block text-sm font-medium text-gray-700">
            Metric Type (Required)
          </label>
          <input
            id="metric"
            type="text"
            value={metric}
            onChange={(e) => setMetric(e.target.value)}
            placeholder="e.g., api_calls, storage_gb, users"
            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="eventId" className="block text-sm font-medium text-gray-700">
            Event ID (Optional - For Idempotency)
          </label>
          <input
            id="eventId"
            type="text"
            value={eventId}
            onChange={(e) => setEventId(e.target.value)}
            placeholder="Leave blank to auto-generate"
            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"
          />
          <p className="mt-1 text-xs text-gray-500">
            Unique ID prevents duplicate charges if the request is retried
          </p>
        </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 ? 'Recording...' : 'Record Usage'}
        </button>
      </form>

      {isSuccess && (
        <div className="mt-4 p-3 bg-green-100 border border-green-400 text-green-700 rounded-lg">
          <p className="font-semibold">Success!</p>
          <p className="text-sm">Event ID: <span className="font-mono">{data?.usageEventId}</span></p>
          {data?.duplicate && (
            <p className="text-sm mt-1">⚠️ This was a duplicate event (same eventId)</p>
          )}
        </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 record a usage event, the API responds with the event ID and duplicate status.
{
  "success": true,
  "usageEventId": "usage_event_xyz789",
  "duplicate": false
}
Response Status Codes:
  • 201 Created: New usage event recorded successfully
  • 200 OK: Event already recorded (duplicate, no charge applied)

Aggregation Types

When configuring a metered component in a Plan, you choose how usage is aggregated over the billing period:
  • Sum: Adds up all reported usage (e.g., API calls).
  • Max: Uses the maximum value recorded (e.g., Peak Storage).
  • Last: Uses the most recent value recorded (e.g., Number of Active Users).

Idempotency

To prevent double-billing due to network retries, you should provide an eventId when reporting usage. This ensures a specific event is counted only once, even if the request is retried multiple times. The system will return a duplicate: true flag if you send the same eventId again.