# Integrating Octany

This is the human-readable counterpart to [`openapi.yaml`](openapi.yaml). If
you only need a contract, read that. If you need to understand *why* things
look the way they do, read this.

## Concepts

| Octany | Stripe equivalent | Notes |
| --- | --- | --- |
| **Account** | Account | Top-level tenant. Every API call is scoped to one account. |
| **Product** | Product + Price | A single object; price/interval/VAT live on the product. |
| **Subscription** | Subscription | Has a single product. Status drives billing behavior. |
| **Order** | Invoice + PaymentIntent merged | Each scheduled charge produces one order. One-off charges also create orders. |
| **Billing method** | PaymentMethod | Card, Swish, Billecta, Trustly, Fortnox invoice. |
| **Webhook endpoint** | Webhook endpoint | URL + auto-generated 32-char secret. |

Two distinguishing facts:

1. **Account is in the path.** `/api/{account}/subscriptions`. The account
   UUID is part of every URL — you don't pass it in a body or rely on the
   key alone.
2. **No separate price object.** A product *is* a price. To change the
   price, update the product (or swap the subscription to a different
   product via `POST /subscription/{id}/product`).

## Authentication

```
X-API-KEY: <your-api-key>
```

API keys are issued from the **Developers → API tokens** page in the
admin UI and are scoped to a single account. There is no master key — to
operate across accounts, you hold multiple keys.

### Integration API (partner-platform-only)

A separate surface at `/api/integration/{account}/…` uses two headers:

```
X-INTEGRATION-ID:  <integration-id>
X-INTEGRATION-KEY: <integration-secret>
```

It exposes account-onboarding and SSO-token endpoints used by partner
platforms that resell Octany under their own brand. It is *not* a
replacement for the public API.

## Base URL

```
https://api.octany.com/api/{account}
```

Replace `{account}` with the account UUID.

## Pagination

List endpoints use page-based pagination. Default page size:

| Endpoint | Default `per_page` | Configurable |
| --- | --- | --- |
| `GET /subscriptions` | 20 | no |
| `GET /products` | 100 | no |
| `GET /subscription/{id}/orders` | 20 | yes — `?per_page=50` |

Response envelope:

```json
{
  "data": [ /* resources */ ],
  "pagination": {
    "total": 142,
    "count": 20,
    "per_page": 20,
    "current_page": 1,
    "total_pages": 8,
    "links": { "next": "https://…?page=2" }
  }
}
```

Use `?page=2` to advance. There is no cursor pagination yet.

## Filtering

Filters are passed as `filter[<key>]=<value>` query parameters. Currently
supported:

- `GET /subscriptions` — `filter[reference_id]`
- `GET /subscription/{id}/orders` — `filter[product_id]`, `filter[state]` (comma-separated or repeated)

## Locale

The `SetLocale` middleware reads `X-Locale` from API requests and applies
it to translations and date/currency formatting in responses. If not set,
it defaults to **`sv` (Swedish)** — set the header explicitly if you want
English error messages or formatted prices.

```
X-Locale: en
```

## Errors

Errors come back in a consistent JSON envelope:

```json
{
  "error": {
    "code": "validation_failed",
    "message": "The url field is required."
  }
}
```

Status codes:

| Code | Meaning |
| --- | --- |
| 200 | Success |
| 401 | Missing or invalid `X-API-KEY` |
| 402 | Charge could not be processed (e.g. `POST /subscription/{id}/order` returned `null` from the action) |
| 404 | Account/resource not found, or token doesn't match the URL account |
| 422 | Validation error (`error.message` will contain the first validation message) |
| 429 | Rate-limited (60 requests per minute per token/IP today) |

> **Caveat — lossy errors.** When validation fails, only the *first*
> validation message is surfaced today. If you need the full bag, that's
> filed as a backend improvement.

## Idempotency

Not yet supported. POST endpoints have no `Idempotency-Key` handling. If a
network call times out, you must reconcile by listing the affected
resource (e.g. `GET /subscription/{id}/orders` after a `POST
/subscription/{id}/order`).

## Money and amounts

Amounts are integers in **cents** (or öre, for SEK), matching Stripe's
convention. `Subscription.price` is `cents()` — i.e. `4995` means
49.95 of `currency`.

Product objects also expose `price_with_vat` and `price_without_vat` so
the consumer doesn't have to guess.

## End-to-end example: create a checkout, listen for webhooks

This is the canonical "first integration" flow. Adjust paths to your
embedding context.

### 1. Set up a webhook endpoint

```bash
curl -X POST https://api.octany.com/api/$ACCOUNT/webhooks/endpoints \
  -H "X-API-KEY: $OCTANY_KEY" \
  -d url=https://app.example.com/octany/webhook
```

> The response **does not include the secret** today. Fetch it from
> **Developers → Webhooks** in the admin UI and store it in your env as
> `OCTANY_WEBHOOK_SECRET`. This is filed for fix.

### 2. Build a hosted-checkout URL

There is no `POST /checkout/sessions`. Instead, link directly to the
hosted checkout page with the account UUID and product:

```
https://checkout.octany.com/checkout/{account-uuid}/product/{product-id}
```

You can pass query params: `?email=...&referenceId=...&successUrl=...&locale=en`.
After payment, Octany redirects to the `successUrl` you provided.

> A proper checkout-session API (POST → `{ url, expires_at }`) is on the
> roadmap.

### 3. Receive the webhook

Octany POSTs to your endpoint with these headers:

```
User-Agent: Octany/1.0
Octany-Signature: <hex sha256 hmac>
Content-Type: application/json
```

Verify the signature before parsing. See [webhooks.md](webhooks.md) for
the recipe and event catalog.

### 4. Manage the subscription

Once `subscription.created` arrives, store `data.id` (the subscription's
`uniq_id`) on your user record. From then on:

- Cancel: `POST /subscription/{id}/cancel`
- Charge a one-off: `POST /subscription/{id}/order` with a `product_id`
- Show "update card" / "see invoices" / "cancel" links: those URLs come
  back inline on the subscription resource (`update_card_url`,
  `invoices_url`, `cancel_url`) — they're signed and time-limited. This
  is how end users self-serve; there is no separate customer-portal API.

## Subscription status

The `status` field on `Subscription` is a string with eight values, in
roughly this lifecycle order:

| Value | Meaning |
| --- | --- |
| `pending` | Created, payment not yet received. |
| `future_start` | Has a `starts_at` in the future. Won't bill until then. |
| `trialing` | In a trial period (`trial_ends_at` in the future). |
| `active` | Paid, currently working. The success state. |
| `delayed` | `renews_at` is in the past — a renewal charge is being retried. |
| `unpaid` | Renewal attempts have failed. Needs admin intervention. |
| `cancelled` | Cancelled by user or admin (`cancelled_at` is set). May still be active until `ends_at`. |
| `expired` | Reached the end of a fixed billing-cycle product. |

## Order state

| Value | Meaning |
| --- | --- |
| `created` | New order — may be awaiting payment depending on billing method. |
| `confirmed` | Confirmed by the user, payment not yet received. |
| `payment_pending_user` | Waiting on the user to complete payment (e.g. Swish app open). |
| `payment_pending` | Billing method is processing. |
| `payment_failed` | Charge attempt failed. |
| `payment_cancelled` | User or system cancelled. |
| `payment_refunded` | Refunded. |
| `paid` | Settled. |

## Hosted checkout

Until a checkout-session API ships, the integration pattern is:

1. Server-side, build the URL: `https://checkout.octany.com/checkout/{account-uuid}/product/{product-id}?email=…&referenceId=…&successUrl=…&locale=en`.
2. Redirect the user (or open in a popup).
3. Listen for `order.paid` and `subscription.created` webhooks to update
   your own records.
4. Use the `referenceId` you passed in to correlate the webhook back to
   your own user record.

For SCA/3DS, the hosted checkout owns the flow — Stripe Elements is
embedded server-side and handles authentication challenges before the
order reaches `paid`.

For Fundraising embeds, the full list of Checkout query parameters
(UTM, name-your-price, role tagging, business-customer pre-fill), and the
embeddable cart's `window.Octany` JavaScript API, see
[`frontend.md`](frontend.md).
