RecurDesk API

Integrate billing, customers, invoices, and usage tracking into your applications. The RecurDesk REST API uses JSON over HTTPS with standard HTTP methods and status codes.

Base URL https://api.recurdesk.com/api/v1/

Authentication

All API requests require an API key sent via the Authorization header using the Bearer scheme. API keys are scoped to a single workspace and inherit the permissions of the user who created them.

Getting your API key

Navigate to Settings → API Keys in your RecurDesk workspace to generate a key. Keys use the rdk_live_ prefix for production and rdk_test_ for sandbox environments.

Authorization Header
Authorization: Bearer rdk_live_a1b2c3d4e5f6g7h8i9j0...

If the key is missing, invalid, or revoked, the API returns 401 Unauthorized.

Rate Limits

The API enforces a rate limit of 100 requests per minute per API key. When exceeded, requests return 429 Too Many Requests with a Retry-After header indicating how many seconds to wait.

Header Description
X-RateLimit-Limit Maximum requests per window (100)
X-RateLimit-Remaining Requests remaining in current window
X-RateLimit-Reset Unix timestamp when the window resets
Retry-After Seconds to wait (only on 429 responses)

Response Format

All responses are JSON. List endpoints return a paginated envelope with data and meta fields. Single-resource endpoints return the object directly.

List response
{
  "data": [
    { "id": "cust_8f3a...", "name": "Acme Corp", ... },
    { "id": "cust_2b7e...", "name": "Globex Inc", ... }
  ],
  "meta": {
    "page": 1,
    "per_page": 25,
    "total": 142,
    "total_pages": 6
  }
}
Single resource response
{
  "id": "cust_8f3a...",
  "name": "Acme Corp",
  "email": "billing@acme.com",
  "currency_code": "USD",
  "created_at": "2026-01-15T09:30:00Z"
}

Errors

Errors return a JSON object with a single error field containing a human-readable description. The HTTP status code indicates the error category.

Error response
{
  "error": "Customer with this email already exists"
}
Status Meaning When it occurs
400 Bad Request Missing required field, invalid value, malformed JSON
401 Unauthorized Missing, invalid, or revoked API key
403 Forbidden API key lacks permission for the requested action
404 Not Found Resource does not exist or was deleted
409 Conflict Duplicate resource, idempotency key collision
429 Too Many Requests Rate limit exceeded
500 Internal Server Error Unexpected server-side failure

Customers

Manage your customer records. Customers are the billable entities that receive invoices and own subscriptions.

GET /customers

Retrieve a paginated list of customers.

Query Parameters

ParameterTypeDescription
pageintegerPage number (default: 1)
per_pageintegerResults per page, max 100 (default: 25)
searchstringFilter by name or email (partial match)
statusstringactive or archived (default: active)

Response

200 OK
{
  "data": [
    {
      "id": "3e7a1f2c-8b4d-4e6a-9c0f-1a2b3c4d5e6f",
      "name": "Acme Corp",
      "email": "billing@acme.com",
      "phone": "+1-555-0100",
      "currency_code": "USD",
      "billing_address": "123 Main St, New York, NY 10001",
      "tax_number": "US-123456789",
      "notes": "Enterprise plan customer",
      "created_at": "2026-01-15T09:30:00Z",
      "updated_at": "2026-03-20T14:22:00Z"
    }
  ],
  "meta": { "page": 1, "per_page": 25, "total": 42, "total_pages": 2 }
}
GET /customers/:id

Retrieve a single customer by ID.

Response

200 OK
{
  "id": "3e7a1f2c-8b4d-4e6a-9c0f-1a2b3c4d5e6f",
  "name": "Acme Corp",
  "email": "billing@acme.com",
  "phone": "+1-555-0100",
  "currency_code": "USD",
  "billing_address": "123 Main St, New York, NY 10001",
  "tax_number": "US-123456789",
  "notes": "Enterprise plan customer",
  "created_at": "2026-01-15T09:30:00Z",
  "updated_at": "2026-03-20T14:22:00Z"
}
POST /customers

Create a new customer.

Request Body

FieldTypeRequiredDescription
namestringYesCustomer display name
emailstringYesBilling email address
phonestringNoPhone number
currency_codestringNoISO 4217 code (default: workspace currency)
billing_addressstringNoFull billing address
tax_numberstringNoTax/VAT identification number
notesstringNoInternal notes
Request example
{
  "name": "Globex Inc",
  "email": "accounts@globex.com",
  "currency_code": "USD",
  "billing_address": "742 Evergreen Terrace, Springfield, IL 62704",
  "tax_number": "US-987654321"
}
201 Created
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "name": "Globex Inc",
  "email": "accounts@globex.com",
  "phone": null,
  "currency_code": "USD",
  "billing_address": "742 Evergreen Terrace, Springfield, IL 62704",
  "tax_number": "US-987654321",
  "notes": null,
  "created_at": "2026-03-30T10:00:00Z",
  "updated_at": "2026-03-30T10:00:00Z"
}
PUT /customers/:id

Update an existing customer. Only include the fields you want to change.

Request example
{
  "name": "Globex International",
  "phone": "+1-555-0200"
}
200 OK
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "name": "Globex International",
  "email": "accounts@globex.com",
  "phone": "+1-555-0200",
  "currency_code": "USD",
  "billing_address": "742 Evergreen Terrace, Springfield, IL 62704",
  "tax_number": "US-987654321",
  "notes": null,
  "created_at": "2026-03-30T10:00:00Z",
  "updated_at": "2026-03-30T11:15:00Z"
}

Invoices

Access invoice records. Invoices are generated automatically by the billing engine or created manually. They cannot be modified or deleted via the API -- only viewed.

GET /invoices

Retrieve a paginated list of invoices.

Query Parameters

ParameterTypeDescription
pageintegerPage number (default: 1)
per_pageintegerResults per page, max 100 (default: 25)
customer_iduuidFilter by customer
statusstringDraft, Sent, Paid, Overdue, or Void

Response

200 OK
{
  "data": [
    {
      "id": "inv_7c9d2e4f-1a3b-5c7d-9e0f-2a4b6c8d0e1f",
      "invoice_number": "INV-2026-0042",
      "customer_id": "3e7a1f2c-8b4d-4e6a-9c0f-1a2b3c4d5e6f",
      "customer_name": "Acme Corp",
      "status": "Sent",
      "currency_code": "USD",
      "subtotal": "1250.00",
      "tax_total": "112.50",
      "total": "1362.50",
      "issue_date": "2026-03-01",
      "due_date": "2026-03-31",
      "line_items": [
        {
          "description": "Pro Plan - March 2026",
          "quantity": "5.00",
          "unit_price": "250.00",
          "tax_rate": "9.00",
          "amount": "1250.00"
        }
      ],
      "created_at": "2026-03-01T00:00:00Z"
    }
  ],
  "meta": { "page": 1, "per_page": 25, "total": 156, "total_pages": 7 }
}
GET /invoices/:id

Retrieve a single invoice with full line item details.

Response

200 OK
{
  "id": "inv_7c9d2e4f-1a3b-5c7d-9e0f-2a4b6c8d0e1f",
  "invoice_number": "INV-2026-0042",
  "customer_id": "3e7a1f2c-8b4d-4e6a-9c0f-1a2b3c4d5e6f",
  "customer_name": "Acme Corp",
  "status": "Sent",
  "currency_code": "USD",
  "subtotal": "1250.00",
  "tax_total": "112.50",
  "total": "1362.50",
  "issue_date": "2026-03-01",
  "due_date": "2026-03-31",
  "paid_at": null,
  "line_items": [
    {
      "description": "Pro Plan - March 2026",
      "quantity": "5.00",
      "unit_price": "250.00",
      "tax_rate": "9.00",
      "amount": "1250.00"
    }
  ],
  "notes": "Payment due within 30 days",
  "created_at": "2026-03-01T00:00:00Z"
}

Subscriptions

View subscription records. Subscriptions link customers to products with a defined billing frequency and pricing.

GET /subscriptions

Retrieve a paginated list of subscriptions.

Query Parameters

ParameterTypeDescription
pageintegerPage number (default: 1)
per_pageintegerResults per page, max 100 (default: 25)
customer_iduuidFilter by customer
statusstringActive, Paused, or Cancelled

Response

200 OK
{
  "data": [
    {
      "id": "sub_4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a",
      "customer_id": "3e7a1f2c-8b4d-4e6a-9c0f-1a2b3c4d5e6f",
      "customer_name": "Acme Corp",
      "product_id": "prod_1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
      "product_name": "Pro Plan",
      "status": "Active",
      "quantity": 5,
      "unit_price": "250.00",
      "currency_code": "USD",
      "frequency": "M",
      "start_date": "2026-01-15",
      "next_billing_date": "2026-04-01",
      "created_at": "2026-01-15T09:30:00Z"
    }
  ],
  "meta": { "page": 1, "per_page": 25, "total": 38, "total_pages": 2 }
}
GET /subscriptions/:id

Retrieve a single subscription by ID.

Response

200 OK
{
  "id": "sub_4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a",
  "customer_id": "3e7a1f2c-8b4d-4e6a-9c0f-1a2b3c4d5e6f",
  "customer_name": "Acme Corp",
  "product_id": "prod_1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "product_name": "Pro Plan",
  "status": "Active",
  "quantity": 5,
  "unit_price": "250.00",
  "currency_code": "USD",
  "frequency": "M",
  "start_date": "2026-01-15",
  "next_billing_date": "2026-04-01",
  "end_date": null,
  "notes": null,
  "created_at": "2026-01-15T09:30:00Z",
  "updated_at": "2026-03-01T00:00:00Z"
}

Products

Browse your product catalog. Products define what you sell, including pricing tiers, usage-based billing, and billing frequencies.

GET /products

Retrieve a paginated list of products.

Query Parameters

ParameterTypeDescription
pageintegerPage number (default: 1)
per_pageintegerResults per page, max 100 (default: 25)
searchstringFilter by product name (partial match)

Response

200 OK
{
  "data": [
    {
      "id": "prod_1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
      "name": "Pro Plan",
      "description": "Full-featured plan for growing teams",
      "type": "Recurring",
      "pricing": [
        {
          "frequency": "M",
          "unit_price": "250.00",
          "currency_code": "USD"
        },
        {
          "frequency": "Y",
          "unit_price": "2500.00",
          "currency_code": "USD"
        }
      ],
      "created_at": "2025-12-01T00:00:00Z"
    }
  ],
  "meta": { "page": 1, "per_page": 25, "total": 12, "total_pages": 1 }
}
GET /products/:id

Retrieve a single product with all pricing tiers.

Response

200 OK
{
  "id": "prod_1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "name": "Pro Plan",
  "description": "Full-featured plan for growing teams",
  "type": "Recurring",
  "pricing": [
    {
      "frequency": "M",
      "unit_price": "250.00",
      "currency_code": "USD"
    },
    {
      "frequency": "Y",
      "unit_price": "2500.00",
      "currency_code": "USD"
    }
  ],
  "tax_rate": "9.00",
  "created_at": "2025-12-01T00:00:00Z",
  "updated_at": "2026-02-10T16:00:00Z"
}

Usage

Record usage events for metered billing. Usage records are aggregated at the end of each billing period and included in the next invoice.

POST /usage

Record a usage event against a subscription.

Request Body

FieldTypeRequiredDescription
subscription_iduuidYesThe subscription to record usage against
quantitydecimalYesUsage amount (e.g., API calls, GB transferred)
descriptionstringNoHuman-readable description of the usage event
timestampdatetimeNoWhen usage occurred (default: now). ISO 8601 format.
idempotency_keystringYesUnique key to prevent duplicate recording
Request example
{
  "subscription_id": "sub_4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a",
  "quantity": 1500,
  "description": "API calls - 2026-03-30",
  "idempotency_key": "usage_2026-03-30_api-calls_acme"
}
201 Created
{
  "id": "ur_9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d",
  "subscription_id": "sub_4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a",
  "quantity": "1500.00",
  "description": "API calls - 2026-03-30",
  "recorded_at": "2026-03-30T23:59:59Z",
  "idempotency_key": "usage_2026-03-30_api-calls_acme",
  "created_at": "2026-03-30T23:59:59Z"
}
Idempotency

The idempotency_key field is required. If you send the same key twice, the second request returns the original response with 200 OK instead of creating a duplicate. Keys are unique per workspace.

Getting Started

Here is a complete example that lists your customers, creates a new one, and records a usage event. Select your preferred language below.

cURL
# 1. List customers
curl -s https://api.recurdesk.com/api/v1/customers \
  -H "Authorization: Bearer rdk_live_a1b2c3d4e5f6g7h8i9j0"

# 2. Create a customer
curl -s -X POST https://api.recurdesk.com/api/v1/customers \
  -H "Authorization: Bearer rdk_live_a1b2c3d4e5f6g7h8i9j0" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Initech LLC",
    "email": "billing@initech.com",
    "currency_code": "USD"
  }'

# 3. Record usage
curl -s -X POST https://api.recurdesk.com/api/v1/usage \
  -H "Authorization: Bearer rdk_live_a1b2c3d4e5f6g7h8i9j0" \
  -H "Content-Type: application/json" \
  -d '{
    "subscription_id": "sub_4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a",
    "quantity": 1500,
    "description": "API calls - March 2026",
    "idempotency_key": "usage_2026-03-30_api-calls"
  }'
Python (requests)
import requests

BASE_URL = "https://api.recurdesk.com/api/v1"
API_KEY = "rdk_live_a1b2c3d4e5f6g7h8i9j0"

headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
}

# 1. List customers
resp = requests.get(f"{BASE_URL}/customers", headers=headers)
customers = resp.json()["data"]
print(f"Found {resp.json()['meta']['total']} customers")

# 2. Create a customer
new_customer = requests.post(
    f"{BASE_URL}/customers",
    headers=headers,
    json={
        "name": "Initech LLC",
        "email": "billing@initech.com",
        "currency_code": "USD",
    },
).json()
print(f"Created customer: {new_customer['id']}")

# 3. Record usage
usage = requests.post(
    f"{BASE_URL}/usage",
    headers=headers,
    json={
        "subscription_id": "sub_4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a",
        "quantity": 1500,
        "description": "API calls - March 2026",
        "idempotency_key": "usage_2026-03-30_api-calls",
    },
).json()
print(f"Recorded usage: {usage['id']}")

# For async applications, consider using httpx:
# import httpx
# async with httpx.AsyncClient(base_url=BASE_URL, headers=headers) as client:
#     resp = await client.get("/customers")
C# (.NET)
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;

const string baseUrl = "https://api.recurdesk.com/api/v1";
const string apiKey = "rdk_live_a1b2c3d4e5f6g7h8i9j0";

using var client = new HttpClient();
client.BaseAddress = new Uri(baseUrl);
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", apiKey);

// 1. List customers
var listResp = await client.GetAsync("/customers");
var listJson = await listResp.Content.ReadFromJsonAsync<JsonElement>();
var total = listJson.GetProperty("meta").GetProperty("total").GetInt32();
Console.WriteLine($"Found {total} customers");

// 2. Create a customer
var createResp = await client.PostAsJsonAsync("/customers", new
{
    name = "Initech LLC",
    email = "billing@initech.com",
    currency_code = "USD"
});
var created = await createResp.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine($"Created customer: {created.GetProperty("id")}");

// 3. Record usage
var usageResp = await client.PostAsJsonAsync("/usage", new
{
    subscription_id = "sub_4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a",
    quantity = 1500,
    description = "API calls - March 2026",
    idempotency_key = "usage_2026-03-30_api-calls"
});
var usage = await usageResp.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine($"Recorded usage: {usage.GetProperty("id")}");
JavaScript / Node.js (fetch)
const BASE_URL = "https://api.recurdesk.com/api/v1";
const API_KEY = "rdk_live_a1b2c3d4e5f6g7h8i9j0";

const headers = {
  Authorization: `Bearer ${API_KEY}`,
  "Content-Type": "application/json",
};

// 1. List customers
const listResp = await fetch(`${BASE_URL}/customers`, { headers });
const { data: customers, meta } = await listResp.json();
console.log(`Found ${meta.total} customers`);

// 2. Create a customer
const createResp = await fetch(`${BASE_URL}/customers`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    name: "Initech LLC",
    email: "billing@initech.com",
    currency_code: "USD",
  }),
});
const newCustomer = await createResp.json();
console.log(`Created customer: ${newCustomer.id}`);

// 3. Record usage
const usageResp = await fetch(`${BASE_URL}/usage`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    subscription_id: "sub_4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a",
    quantity: 1500,
    description: "API calls - March 2026",
    idempotency_key: "usage_2026-03-30_api-calls",
  }),
});
const usage = await usageResp.json();
console.log(`Recorded usage: ${usage.id}`);
PHP
<?php

$baseUrl = "https://api.recurdesk.com/api/v1";
$apiKey  = "rdk_live_a1b2c3d4e5f6g7h8i9j0";

function apiRequest(string $method, string $url, ?array $body = null): array {
    global $apiKey;
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            "Authorization: Bearer $apiKey",
            "Content-Type: application/json",
        ],
    ]);
    if ($method === "POST") {
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
    }
    $response = curl_exec($ch);
    curl_close($ch);
    return json_decode($response, true);
}

// 1. List customers
$list = apiRequest("GET", "$baseUrl/customers");
echo "Found {$list['meta']['total']} customers\n";

// 2. Create a customer
$created = apiRequest("POST", "$baseUrl/customers", [
    "name"          => "Initech LLC",
    "email"         => "billing@initech.com",
    "currency_code" => "USD",
]);
echo "Created customer: {$created['id']}\n";

// 3. Record usage
$usage = apiRequest("POST", "$baseUrl/usage", [
    "subscription_id" => "sub_4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a",
    "quantity"         => 1500,
    "description"      => "API calls - March 2026",
    "idempotency_key"  => "usage_2026-03-30_api-calls",
]);
echo "Recorded usage: {$usage['id']}\n";
Go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

const (
	baseURL = "https://api.recurdesk.com/api/v1"
	apiKey  = "rdk_live_a1b2c3d4e5f6g7h8i9j0"
)

func apiRequest(method, path string, body any) (map[string]any, error) {
	var reqBody io.Reader
	if body != nil {
		b, _ := json.Marshal(body)
		reqBody = bytes.NewReader(b)
	}
	req, _ := http.NewRequest(method, baseURL+path, reqBody)
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var result map[string]any
	json.NewDecoder(resp.Body).Decode(&result)
	return result, nil
}

func main() {
	// 1. List customers
	list, _ := apiRequest("GET", "/customers", nil)
	meta := list["meta"].(map[string]any)
	fmt.Printf("Found %.0f customers\n", meta["total"])

	// 2. Create a customer
	created, _ := apiRequest("POST", "/customers", map[string]any{
		"name":          "Initech LLC",
		"email":         "billing@initech.com",
		"currency_code": "USD",
	})
	fmt.Printf("Created customer: %s\n", created["id"])

	// 3. Record usage
	usage, _ := apiRequest("POST", "/usage", map[string]any{
		"subscription_id": "sub_4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f9a",
		"quantity":        1500,
		"description":     "API calls - March 2026",
		"idempotency_key": "usage_2026-03-30_api-calls",
	})
	fmt.Printf("Recorded usage: %s\n", usage["id"])
}