Browse the API by resource.
The Octany API manages subscriptions, products, orders, contacts, and webhook endpoints. Use this page when you want to understand the human shape of the API. Use the OpenAPI spec when you need machine-readable types.
https://app.octany.com/api/{account}. Send your per-account API key in X-API-KEY.
§ 04.1 Basics #
Base URL
https://app.octany.com/api/{account}
Authentication
X-API-KEY: <your-api-key> X-Locale: en Octany-Version: 2
API keys are scoped to a single account. Set X-Locale if you want formatted strings in a specific language. Error messages are always in English. Send Octany-Version to pick an API version for one request.
API versions
Octany has two API versions. They differ in how subscriptions and orders point to the donor. Use the API v1 / v2 switch in the header to see the examples on this page in either version.
| Version | Donor on subscriptions and orders | Used by |
|---|---|---|
| 2 | contact: the full contact, the same shape as GET /contacts/{id}, including its metadata. null when no contact is linked. | New accounts, and accounts that had no API keys or webhook endpoints when versions were introduced. |
| 1 | A reference to the legacy customer record, which is being phased out. Subscriptions also carry the billing portal links, which version 2 leaves out. | Accounts that already had an integration. |
The version is an account setting. Change it in the admin UI under Settings → API keys by clicking API version. It applies to API responses and webhooks alike. To try the other version on a single request, send the Octany-Version header. Every response returns Octany-Version with the version it used, and an unsupported value returns 400.
Webhooks always use the account's version, and version 2 deliveries carry "version": 2 in the envelope. New fields can appear in either version at any time; only breaking changes get a new version. The changelog lists every difference.
Octany-Version: 2 from one integration at a time and read contact instead of the customer fields. Once every integration handles version 2, switch the account setting so webhooks follow.
Resource IDs
Subscription, order, and webhook-delivery id values are integers in production today (e.g. 76963688). Treat them as opaque identifiers in your storage layer — store them in a column wide enough to fit a future format change (a string column, or bigint). Don't validate the format and don't gate on is_int / typeof === 'number'; just round-trip them as you received them. Product, contact, and account IDs are integers.
Pagination
List endpoints return data plus pagination. Use ?page=2 to move through results.
{
"data": [],
"pagination": {
"total": 142,
"count": 20,
"per_page": 20,
"current_page": 1,
"total_pages": 8,
"links": { "next": "https://app.octany.com/api/{account}/subscriptions?page=2" }
}
}
Errors
Every error from https://app.octany.com/api/{account}/… is JSON, whatever the Accept header says. message is always in English. Branch on code, not on message.
{
"message": "Resource not found.",
"code": "not_found",
"docs_url": "https://build.octany.com/docs/integration.md#errors"
}
| Status | code | Example message / notes |
|---|---|---|
| 400 | bad_request | For example an unsupported Octany-Version header: “Unsupported Octany-Version header. Use one of: 1, 2.” |
| 401 | unauthenticated | “Missing or invalid X-API-KEY header.” |
| 403 | forbidden | “Your plan does not include API access.” Also includes a legacy error key with the same text. |
| 404 | not_found | Unknown route: “The route api/… could not be found.” Missing record: “Resource not found.” |
| 405 | method_not_allowed | e.g. “Product is not deletable” |
| 422 | validation_failed | The first validation message, plus errors, which maps each field to a list of messages. |
| 429 | rate_limited | “Too Many Attempts.” The limit is 120 requests per minute. The response sends Retry-After, X-RateLimit-Limit and X-RateLimit-Remaining headers. |
| 5xx | server_error | “Server Error” |
| Other 4xx | client_error | e.g. 402 when POST /subscription/{id}/order could not process the charge. |
Some endpoints also return older fields next to message, code and docs_url. They are kept for backwards compatibility, so new code can ignore them:
{
"message": "Not Found",
"code": "not_found",
"docs_url": "https://build.octany.com/docs/integration.md#errors",
"status": 404,
"success": false,
"error": { "code": null, "message": null }
}
A few of these older-style errors return HTTP 500 for invalid input, so they come back with code: server_error. One example is an invalid or already registered webhook URL. In that case, read message. It says what was wrong with the request. Full details: integration.md#errors.
Discovery
GET https://app.octany.com/api needs no API key. It returns the base URL, the auth scheme and links to these docs:
{
"name": "Octany API",
"base_url": "https://app.octany.com/api/{account}",
"auth": { "type": "apiKey", "in": "header", "name": "X-API-KEY" },
"docs_url": "https://build.octany.com/api",
"openapi_url": "https://build.octany.com/docs/openapi.yaml",
"llms_txt": "https://build.octany.com/llms.txt"
}
https://app.octany.com/openapi.yaml and https://app.octany.com/api/openapi.yaml redirect (302) to the OpenAPI spec. https://app.octany.com/llms.txt points back to these docs.
§ 04.2 Subscriptions #
Subscriptions represent recurring billing for one product. Store the subscription id after subscription.created arrives, then use it for cancellation, one-off orders, or product changes.
List subscriptions. Supports page and filter[reference_id].
curl "https://app.octany.com/api/$ACCOUNT/subscriptions?filter[reference_id]=user_42" \ -H "X-API-KEY: $OCTANY_KEY"
Retrieve one subscription by its Octany subscription ID.
curl "https://app.octany.com/api/$ACCOUNT/subscription/76963688" \ -H "X-API-KEY: $OCTANY_KEY"
Cancel a subscription. No further renewals will be attempted. Depending on the product, the subscription may remain active until ends_at.
curl -X POST "https://app.octany.com/api/$ACCOUNT/subscription/76963688/cancel" \ -H "X-API-KEY: $OCTANY_KEY"
Swap the product on a subscription.
curl -X POST "https://app.octany.com/api/$ACCOUNT/subscription/76963688/product" \ -H "X-API-KEY: $OCTANY_KEY" \ -H "Content-Type: application/json" \ -d '{ "product_id": 42 }'
List orders for a subscription. Supports page, per_page, filter[product_id], and filter[state].
curl "https://app.octany.com/api/$ACCOUNT/subscription/76963688/orders?filter[state]=paid&per_page=50" \ -H "X-API-KEY: $OCTANY_KEY"
Create a one-off order on an existing subscription, billed through the subscription's billing method.
curl -X POST "https://app.octany.com/api/$ACCOUNT/subscription/76963688/order" \ -H "X-API-KEY: $OCTANY_KEY" \ -H "Content-Type: application/json" \ -d '{ "product_id": 89, "amount": 4995, "quantity": 1, "vat": 2500, "description": "Extra item" }'
§ 04.3 Orders #
Retrieve an order by its Octany order ID. Orders expose totals, VAT, state, reference fields, delivery links, metadata, and custom fields.
curl "https://app.octany.com/api/$ACCOUNT/order/516253755" \ -H "X-API-KEY: $OCTANY_KEY"
Order states
§ 04.4 Products #
List products. Product responses include pricing, VAT, interval, stock flags, images, and display strings.
curl "https://app.octany.com/api/$ACCOUNT/products" \ -H "X-API-KEY: $OCTANY_KEY"
Retrieve one product by numeric product ID.
curl "https://app.octany.com/api/$ACCOUNT/product/42" \ -H "X-API-KEY: $OCTANY_KEY"
Update a product. Only sent fields are updated.
curl -X POST "https://app.octany.com/api/$ACCOUNT/product/42" \ -H "X-API-KEY: $OCTANY_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Monthly membership", "description": "Access to member content.", "price": 9900, "currency": "SEK", "vat_rate": 25, "interval": "month" }'
Recurring product interval values are day, week, month, half-year, and year. Product type rules may restrict which intervals are available for a given product.
Delete a product.
curl -X DELETE "https://app.octany.com/api/$ACCOUNT/product/42" \ -H "X-API-KEY: $OCTANY_KEY"
§ 04.5 Contacts #
Contacts are the people and companies in the account's CRM. A contact's type is person or company, and a person can belong to a company through parent_contact_id.
List contacts, 100 per page, oldest first. Filter with type (person or company), email (exact match), or q (partial match on name, email, or phone). Filters are kept in the pagination links.
curl "https://app.octany.com/api/$ACCOUNT/contacts?email=anna%40example.com" \ -H "X-API-KEY: $OCTANY_KEY"
Retrieve one contact by its numeric ID. Deleted contacts return 404.
curl "https://app.octany.com/api/$ACCOUNT/contacts/1042" \ -H "X-API-KEY: $OCTANY_KEY"
Create a contact. Returns 201 with the new contact. type is required, and at least one of name, email, or phone must be set.
curl -X POST "https://app.octany.com/api/$ACCOUNT/contacts" \ -H "X-API-KEY: $OCTANY_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "person", "name": "Anna Persson", "email": "[email protected]", "phone": "070-123 45 67", "personal_identity_number": "19951027-2389", "parent_contact_id": 1001, "metadata": { "crm_id": "CRM-1001" }, "address": { "line1": "Storgatan 1", "zip": "111 22", "city": "Stockholm", "country": "SE" } }'
Update a contact. Only the fields you send change; send null to clear one. Fields inside address are merged into the existing address. Each change is recorded in the contact's history with the source "API". Keys in metadata are merged into the existing metadata.
curl -X PATCH "https://app.octany.com/api/$ACCOUNT/contacts/1042" \ -H "X-API-KEY: $OCTANY_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "[email protected]", "address": { "city": "Göteborg" } }'
Delete a contact. It disappears from the API and the admin contact list, along with its custom field values.
curl -X DELETE "https://app.octany.com/api/$ACCOUNT/contacts/1042" \ -H "X-API-KEY: $OCTANY_KEY"
Contact rules
- Identity numbers are unique. A
personal_identity_number(people) orcompany_number(companies) that already belongs to another contact in the account is rejected with422. Formatting doesn't matter:19951027-2389and199510272389are the same number. - Creating doesn't deduplicate.
POST /contactsalways creates a new contact, even when another contact has the same email or phone. Look the contact up withGET /contacts?email=…first if you want to update instead. - Values are normalized. Phone numbers are stored in international format (
070-123 45 67becomes+46701234567), and Swedish personal identity numbers as 12 digits. parent_contact_idmust be a company in the same account.- Custom fields aren't part of the contacts API yet.
- Metadata stores your own IDs on a contact, such as the record ID in your CRM. Send it as an object of string values (up to 50 keys, 500 characters each) on
POSTorPATCH. Keys you send are merged into the existing metadata, andnullremoves a key. Changing onlymetadatadoesn't sendcontact.updated, so writing your ID back doesn't start a webhook loop. In API version 2, subscriptions and orders embed the contact with its metadata. - Webhooks. Select the opt-in
contact.created,contact.updated, andcontact.deletedevents on a webhook endpoint to keep your copy in sync. See contact events.
Validation errors
The contacts endpoints return every validation error, keyed by field, with status 422 and code: validation_failed. Messages are always in English.
{
"message": "The type field is required. (and 1 more error)",
"code": "validation_failed",
"docs_url": "https://build.octany.com/docs/integration.md#errors",
"errors": {
"type": ["The type field is required."],
"name": ["At least one of name, email, or phone is required."]
}
}
§ 04.6 Webhook endpoints #
Register a URL to receive signed webhook events. The endpoint secret is generated by Octany and read from the admin UI after creation.
An invalid or already registered URL returns an older-style error with HTTP 500 and code: server_error. Read message to see what was wrong. See Errors.
curl -X POST "https://app.octany.com/api/$ACCOUNT/webhooks/endpoints" \ -H "X-API-KEY: $OCTANY_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://app.example.com/octany/webhook" }'
§ 04.7 Payload shapes #
The subscription and order examples follow the version selected in the header. In version 2 they embed the donor as contact.
Subscription
{
"id": 76963688,
"price": 9900,
"vat": 2500,
"currency": "SEK",
"renews_at": "2026-05-25T09:29:51+00:00",
"reference_id": "member_42",
"reference_name": "Anna Persson",
"status": "active",
"update_card_url": "https://octanybilling.com/...",
"invoices_url": "https://octanybilling.com/...",
"cancel_url": "https://octanybilling.com/...",
"custom_fields": [],
"metadata": {}
}
{
"id": 76963688,
"contact": {
"id": 1042,
"type": "person",
"name": "Anna Persson",
"email": "[email protected]",
"metadata": { "crm_id": "CRM-1001" },
/* … every other contact field */
},
"price": 9900,
"vat": 2500,
"currency": "SEK",
"renews_at": "2026-05-25T09:29:51+00:00",
"reference_id": "member_42",
"reference_name": "Anna Persson",
"status": "active",
"custom_fields": [],
"metadata": {}
}
Order
{
"id": 516253755,
"total": 7920,
"total_with_vat": 9900,
"currency": "SEK",
"reference_id": "member_42",
"state": "paid",
"delivery": { "link": "https://..." },
"custom_fields": [],
"metadata": {}
}
{
"id": 516253755,
"contact": {
"id": 1042,
"type": "person",
"name": "Anna Persson",
"email": "[email protected]",
"metadata": { "crm_id": "CRM-1001" },
/* … every other contact field */
},
"total": 7920,
"total_with_vat": 9900,
"currency": "SEK",
"reference_id": "member_42",
"state": "paid",
"delivery": { "link": "https://..." },
"custom_fields": [],
"metadata": {}
}
Product
{
"id": 42,
"name": "Monthly membership",
"description": "Access to member content.",
"type": "Recurring",
"price": 9900,
"price_with_vat": 9900,
"price_without_vat": 7920,
"currency": "SEK",
"vat_rate": 25,
"interval": "month",
"use_cart": true,
"stock_tracking": false,
"out_of_stock": false
}
Contact
{
"id": 1042,
"type": "person",
"name": "Anna Persson",
"email": "[email protected]",
"phone": "+46701234567",
"personal_identity_number": "199510272389",
"company_number": null,
"parent_contact_id": 1001,
"address": {
"line1": "Storgatan 1",
"line2": null,
"zip": "111 22",
"city": "Stockholm",
"country": "SE"
},
"metadata": { "crm_id": "CRM-1001" },
"first_seen_at": "2026-10-02T09:12:44.000000Z",
"created_at": "2026-10-02T09:12:44.000000Z",
"updated_at": "2026-10-02T09:12:44.000000Z"
}
address is null when the contact has no address.