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.
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.
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: 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.
{
"data": [
{ "id": "cust_8f3a...", "name": "Acme Corp", ... },
{ "id": "cust_2b7e...", "name": "Globex Inc", ... }
],
"meta": {
"page": 1,
"per_page": 25,
"total": 142,
"total_pages": 6
}
} {
"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": "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.
/customers Retrieve a paginated list of customers.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
page | integer | Page number (default: 1) |
per_page | integer | Results per page, max 100 (default: 25) |
search | string | Filter by name or email (partial match) |
status | string | active or archived (default: active) |
Response
{
"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 }
} /customers/:id Retrieve a single customer by ID.
Response
{
"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"
} /customers Create a new customer.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Customer display name |
email | string | Yes | Billing email address |
phone | string | No | Phone number |
currency_code | string | No | ISO 4217 code (default: workspace currency) |
billing_address | string | No | Full billing address |
tax_number | string | No | Tax/VAT identification number |
notes | string | No | Internal notes |
{
"name": "Globex Inc",
"email": "accounts@globex.com",
"currency_code": "USD",
"billing_address": "742 Evergreen Terrace, Springfield, IL 62704",
"tax_number": "US-987654321"
} {
"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"
} /customers/:id Update an existing customer. Only include the fields you want to change.
{
"name": "Globex International",
"phone": "+1-555-0200"
} {
"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.
/invoices Retrieve a paginated list of invoices.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
page | integer | Page number (default: 1) |
per_page | integer | Results per page, max 100 (default: 25) |
customer_id | uuid | Filter by customer |
status | string | Draft, Sent, Paid, Overdue, or Void |
Response
{
"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 }
} /invoices/:id Retrieve a single invoice with full line item details.
Response
{
"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.
/subscriptions Retrieve a paginated list of subscriptions.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
page | integer | Page number (default: 1) |
per_page | integer | Results per page, max 100 (default: 25) |
customer_id | uuid | Filter by customer |
status | string | Active, Paused, or Cancelled |
Response
{
"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 }
} /subscriptions/:id Retrieve a single subscription by ID.
Response
{
"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.
/products Retrieve a paginated list of products.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
page | integer | Page number (default: 1) |
per_page | integer | Results per page, max 100 (default: 25) |
search | string | Filter by product name (partial match) |
Response
{
"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 }
} /products/:id Retrieve a single product with all pricing tiers.
Response
{
"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.
/usage Record a usage event against a subscription.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
subscription_id | uuid | Yes | The subscription to record usage against |
quantity | decimal | Yes | Usage amount (e.g., API calls, GB transferred) |
description | string | No | Human-readable description of the usage event |
timestamp | datetime | No | When usage occurred (default: now). ISO 8601 format. |
idempotency_key | string | Yes | Unique key to prevent duplicate recording |
{
"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"
} {
"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"
} 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.
# 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"
}' 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")
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")}"); 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
$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"; 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"])
}