Octany. for/devs
§ 04 — API reference

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.

Base contract Every endpoint is scoped to one account: 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.

VersionDonor on subscriptions and ordersUsed by
2contact: 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.
1A 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.

Moving to version 2 Send 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"
}
StatuscodeExample message / notes
400bad_requestFor example an unsupported Octany-Version header: “Unsupported Octany-Version header. Use one of: 1, 2.”
401unauthenticated“Missing or invalid X-API-KEY header.”
403forbidden“Your plan does not include API access.” Also includes a legacy error key with the same text.
404not_foundUnknown route: “The route api/… could not be found.” Missing record: “Resource not found.”
405method_not_allowede.g. “Product is not deletable”
422validation_failedThe first validation message, plus errors, which maps each field to a list of messages.
429rate_limited“Too Many Attempts.” The limit is 120 requests per minute. The response sends Retry-After, X-RateLimit-Limit and X-RateLimit-Remaining headers.
5xxserver_error“Server Error”
Other 4xxclient_errore.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.

GET/subscriptions

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"
GET/subscription/{id}

Retrieve one subscription by its Octany subscription ID.

curl "https://app.octany.com/api/$ACCOUNT/subscription/76963688" \
  -H "X-API-KEY: $OCTANY_KEY"
POST/subscription/{id}/cancel

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"
POST/subscription/{id}/product

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 }'
GET/subscription/{id}/orders

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"
POST/subscription/{id}/order

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 #

GET/order/{id}

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

created confirmed payment_pending_user payment_pending payment_failed payment_cancelled payment_refunded paid

§ 04.4 Products #

GET/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"
GET/product/{id}

Retrieve one product by numeric product ID.

curl "https://app.octany.com/api/$ACCOUNT/product/42" \
  -H "X-API-KEY: $OCTANY_KEY"
POST/product/{id}

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/product/{id}

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.

GET/contacts

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"
GET/contacts/{id}

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"
POST/contacts

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"
        }
      }'
PATCH/contacts/{id}

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/contacts/{id}

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

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 #

POST/webhooks/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.