Subscription Contracts V2

Subscription Contracts V2 is Askell’s new subscription model. It is separate from the older PlanVariant / Subscription flow and is instead built around a catalog, prices, bundles, quotes, checkout flows, and billing runs.

All calls to V2 API endpoints require a secret API key:

Authorization: Api-Key your-secret-api-key

Older token-based authentication is still supported in some V2 calls for compatibility with older integrations. New integrations should use secret API keys.

Quick overview of a typical flow

A common integration flow is this:

  1. Fetch products, prices, or bundles from the catalog.

  2. Calculate a quote with POST /api/v2/subscription-offer-quotes/.

  3. Fetch eligible payment processors with POST /api/v2/payment-processor-options/.

  4. Create a checkout with POST /api/v2/checkouts/.

  5. Finalize the checkout with POST /api/v2/checkouts/{token}/finalize/.

  6. Monitor the status of the checkout and, when applicable, the initial billing run.

If the integration does not need payment pre-processing, steps 3-5 can be skipped and the contract can be created directly with POST /api/v2/subscription-contracts/.

See also

For the legacy subscription flow based on PlanVariant and Subscription, see Subscriptions.

Warning

checkout_url in V2 is not a public hosted payment page. It points to the API URL for the checkout object itself and is therefore suitable for system-to-system integrations, not for directly redirecting a user in a browser.

When to use V2

V2 should be used when selling products and subscriptions from a catalog instead of older plans and PlanVariant identifiers. This includes:

  1. When selling individual products or a combination of products as a subscription contract.

  2. When using bundles with selectable prices and quantities.

  3. When price changes need to be scheduled with price versions.

  4. When recurring billing runs need clearer history and retry support.

Legacy payment pages and the older /api/subscriptions/ flow continue to use the legacy Subscription model and are documented separately under Subscriptions.

Core objects

The main V2 objects are these:

Object

Description

CatalogProduct

A product in the catalog.

CatalogPrice

A stable price identifier that a contract is linked to.

CatalogPriceVersion

A time-bounded price version that defines an amount for a period.

BundleTemplate

A sellable bundle that combines one or more products.

SubscriptionContract

The new persistent subscription contract.

BillingRun

A single billing run on a contract.

BillingRunAttempt

A single attempt to collect a billing run.

V2Checkout

An intermediate object that stores the quote and payment prerequisites before the contract is created.

Before you start

Before a payment flow can be completed or a contract can be created, the following must already exist:

  1. The customer must already exist in Askell.

  2. A payment processor (AccountPaymentProcessor) must be configured for the relevant currency and payment method.

  3. If the checkout / finalize flow is used, the customer must already have a verified payment method that matches the selected payment processor.

See also Prerequisites before calling finalize, which explains in more detail what is validated immediately before finalize creates the contract and billing.

Catalog lookup

Use the following endpoints to fetch products and prices from the catalog:

curl https://askell.is/api/v2/catalog/products/ \
  -H "Authorization: Api-Key your-secret-api-key"
curl "https://askell.is/api/v2/catalog/prices/?billing_type=recurring&currency=ISK" \
  -H "Authorization: Api-Key your-secret-api-key"

Common catalog filters:

Filter

Description

active

By default only active records are shown. all or any can also be used.

reference

Product or bundle reference, depending on the endpoint.

product

Product ID or product reference on the price endpoint.

currency

Price currency.

billing_type

recurring or one_time.

recurrence_type

Recurrence type for recurring prices.

Price versions

In V2, a contract is linked to a stable CatalogPrice identifier, while the amount itself can change over time through CatalogPriceVersion. This makes it possible to schedule price changes in advance without moving the customer to a new price identifier.

Responses from catalog price endpoints include, among other things, the following fields:

Field

Description

unit_amount

The current display amount for the price.

current_version_id

The ID of the price version currently considered active.

versions

Past, current, and future price versions.

effective_from

The start of the validity period for the currently displayed version.

effective_to

The end of the validity period for the currently displayed version.

Integrations that only need the current amount can generally read unit_amount. Integrations that want to display or schedule price changes should use versions.

Bundles and price selection

Bundles are fetched from dedicated endpoints:

curl https://askell.is/api/v2/bundle-templates/ \
  -H "Authorization: Api-Key your-secret-api-key"
curl https://askell.is/api/v2/bundle-templates/42/ \
  -H "Authorization: Api-Key your-secret-api-key"

A bundle can contain a fixed product setup, allow price selection for specific bundle items, or use automatically active recurring prices on a related product.

Add-ons

Once a bundle has been selected and the relevant price selections provided, you can query which add-on products or add-on bundles are available:

curl https://askell.is/api/v2/bundle-templates/42/addons/ \
  -H "Authorization: Api-Key your-secret-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "bundle_item_selections": [
      {
        "bundle_item": 100,
        "selected_price": 200
      }
    ]
  }'

Quote and request data

The same basic fields are used repeatedly in quotes, checkout flows, and direct contract creation.

Field

Description

bundle_template

The ID of the selected bundle.

bundle_quantity

Bundle quantity.

bundle_item_selections

Price or quantity selections for individual items within a bundle.

items

Directly selected recurring products without a bundle.

initial_items

One-time products or products that should only appear on the first billing run.

additional_items

Add-on products selected through add-on rules.

additional_bundles

Add-on bundles selected through add-on rules.

Only one primary input form for recurring products may be used at a time:

  • bundle_template

  • items

initial_items are only used on the initial billing. They do not become persistent SubscriptionContractItem rows. See also Direct contract creation.

Quote before creating the contract

POST /api/v2/subscription-offer-quotes/ builds a non-persistent quote from the request data and returns a summary of recurring lines, initial lines, taxes, totals, and the estimated billing schedule.

Example quote for a bundle:

curl https://askell.is/api/v2/subscription-offer-quotes/ \
  -H "Authorization: Api-Key your-secret-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "bundle_template": 42,
    "bundle_quantity": 2,
    "bundle_item_selections": [
      {
        "bundle_item": 100,
        "selected_price": 200
      }
    ],
    "additional_items": [
      {
        "rule_id": 300,
        "price": 400,
        "quantity": 1
      }
    ],
    "initial_items": [
      {
        "price": 500,
        "quantity": 1
      }
    ]
  }'

A shortened response might look like this:

{
  "input_mode": "bundle",
  "bundle_template_id": 42,
  "bundle_quantity": 2,
  "currency": "ISK",
  "period_start_at": "2026-05-20T00:00:00Z",
  "period_end_at": "2026-06-20T00:00:00Z",
  "subtotal_amount": "4500.0000",
  "tax_amount": "0.0000",
  "total_amount": "4500.0000",
  "recurring_subtotal_amount": "4000.0000",
  "recurring_tax_amount": "0.0000",
  "recurring_total_amount": "4000.0000",
  "billing_schedule_preview": {
    "kind": "interval"
  },
  "recurring_items": [
    {
      "key": "bundle-item-100",
      "source": "bundle",
      "creates_contract_item": true,
      "price_id": 200,
      "product_id": 10,
      "product_name": "Vefáskrift",
      "billing_type": "recurring",
      "quantity": 2,
      "unit_amount": "2000.0000",
      "line_total_amount": "4000.0000"
    }
  ],
  "initial_lines": [
    {
      "key": "initial-item-500",
      "source": "initial_items",
      "creates_contract_item": false,
      "price_id": 500,
      "product_id": 11,
      "product_name": "Áskrifendagjöf",
      "billing_type": "one_time",
      "quantity": 1,
      "unit_amount": "500.0000",
      "line_total_amount": "500.0000"
    }
  ]
}

Example quote for direct products without a bundle:

curl https://askell.is/api/v2/subscription-offer-quotes/ \
  -H "Authorization: Api-Key your-secret-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "currency": "ISK",
    "items": [
      {
        "price": 200,
        "quantity": 1
      }
    ],
    "initial_items": [
      {
        "price": 500,
        "quantity": 1
      }
    ]
  }'

Direct contract creation

POST /api/v2/subscription-contracts/ creates a V2 contract directly. This flow is suitable when the integration has already confirmed the order and does not need to run payment pre-processing in the same call.

You can submit, among other things:

  • customer_reference

  • currency

  • bundle_template or items

  • bundle_item_selections

  • additional_items

  • additional_bundles

  • initial_items

  • metadata

  • payment_processor_override if the contract should be pinned to a specific payment processor

initial_items do not become persistent SubscriptionContractItem rows. They are only placed on the first billing run lines if the initial billing is based on them.

Fetch and update a contract

A contract is fetched from:

GET /api/v2/subscription-contracts/{id}/

Lists and filters support, among other things:

  • state

  • customer_reference

  • reference

  • legacy_subscription_id

List pagination

V2 list endpoints only use pagination if page_size is provided.

  • page_size enables pagination

  • the default page_size is 10 when pagination is enabled

  • the maximum page_size is 1000

  • page selects the page

Example:

GET /api/v2/subscription-contracts/?page_size=25&page=2

When pagination is enabled, the response uses the standard shape with count, next, previous, and results. If page_size is not provided, an unpaginated list is returned.

The V2 detail endpoint only allows limited updates. The contract is not intended to be a freely editable order draft, but rather a business-critical state that should evolve through defined lifecycle calls.

Lifecycle actions

V2 supports the following lifecycle calls on contracts:

Path

Description

POST /cancel/

Cancels the contract, either immediately or at the end of the period.

POST /restart/

Restarts a contract that has been stopped.

POST /pause/

Temporarily pauses a contract.

POST /resume/

Resumes a paused contract.

DELETE /pause/{pause_id}/

Deletes a scheduled pause.

POST /activate/

Activates an inactive contract if it can move to the active state.

Main request fields:

Action

Fields

Required

POST /cancel/

cancel_at_period_end, reason

Both optional

POST /restart/

reason

Optional

POST /pause/

start_date, end_date, reason

start_date and end_date are required

POST /resume/

reason

Optional

Example of canceling a contract at the end of the period:

curl -X POST https://askell.is/api/v2/subscription-contracts/123/cancel/ \
  -H "Authorization: Api-Key your-secret-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "cancel_at_period_end": true,
    "reason": "Viðskiptavinur óskaði eftir lokun"
  }'

Checkout flow

When payment pre-processing is required before a contract becomes active, the checkout flow is recommended:

  1. Calculate or confirm the quote.

  2. Fetch eligible payment processors with POST /api/v2/payment-processor-options/.

  3. Create a checkout with POST /api/v2/checkouts/.

  4. Finalize the checkout with POST /api/v2/checkouts/{token}/finalize/.

POST /api/v2/payment-processor-options/ returns the payment processors that are valid for the given quote, currency, and collection method. If only one processor is available, it can be used as the default. If more than one is eligible, the integration should allow a choice.

A shortened response might look like this:

{
  "currency": "ISK",
  "collection_method": "card",
  "selected_account_payment_processor_id": 7,
  "selection_reason": "single_eligible",
  "requires_selection": false,
  "results": [
    {
      "account_payment_processor_id": 7,
      "display_name": "Straumur / Adyen",
      "payment_processor": "adyen",
      "collection_method": "card",
      "resolution_source": "eligible",
      "supports_initial_charge": true,
      "supports_recurring_charge": true,
      "render_mode": "adyen_checkout",
      "payment_processor_type": "adyen",
      "is_3d_secure": true,
      "card_collection_in_frontend": true,
      "supports_checkout": true,
      "address_required": false,
      "registration_mode": "delayed_tokenization",
      "public_registration_config": {}
    }
  ]
}

POST /api/v2/checkouts/ creates a checkout object that stores the quote snapshot, customer, selected payment processor, and other prerequisite data.

GET /api/v2/checkouts/{token}/ fetches the status of the checkout object. checkout_url points to this endpoint.

POST /api/v2/checkouts/{token}/finalize/ attempts to complete contract creation and the initial billing.

Prerequisites before calling finalize

finalize now performs a preflight check before creating the contract. The following must therefore be valid before attempting finalize:

  1. The customer must exist.

  2. The selected payment processor must be valid for the quote.

  3. The customer must have a verified payment method that matches the selected payment processor, unless the initial billing amount is 0 ISK.

If these prerequisites are not met, an error response is returned and no contract or billing run is created.

It is important to distinguish between two kinds of 400 responses from finalize:

  1. Precondition failure: no contract and no billing run are created.

  2. Failed payment attempt: the contract and billing run may already have been created, but checkout.status becomes failed.

A shortened finalize response might look like this:

{
  "id": 15,
  "token": "f2dd7db2-4ae1-45c0-b8f6-2f98d1b70a9a",
  "checkout_url": "https://askell.is/api/v2/checkouts/f2dd7db2-4ae1-45c0-b8f6-2f98d1b70a9a/",
  "status": "succeeded",
  "customer_id": 1008,
  "customer_reference": "customer-123",
  "currency": "ISK",
  "subtotal_amount": "4500.0000",
  "tax_amount": "0.0000",
  "total_amount": "4500.0000",
  "contract_id": 33,
  "initial_billing_run_id": 32,
  "account_payment_processor_id": 7,
  "quote_snapshot": {
    "input_mode": "bundle",
    "total_amount": "4500.0000"
  }
}

Outcomes from finalize

Status

HTTP

Description

Next step

succeeded

200

The contract has been created and the initial billing has completed.

Store contract_id and start regular status monitoring as needed.

pending_external

200

An external processor must complete its flow before the billing is considered complete.

Monitor the checkout and the related billing run until the status changes.

failed

400

The payment attempt failed after the contract and billing run were created.

Inspect contract_id and initial_billing_run_id in the response, read the error body, and decide whether to retry or wait for another action.

Precondition failure

400

The request data or payment prerequisites were invalid; no contract was created.

Correct the request data or payment method before trying again.

Billing runs and retries

V2 billing runs are read from:

GET /api/v2/billing-runs/
GET /api/v2/billing-runs/{id}/

A failed billing run can be retried with the following endpoint:

POST /api/v2/billing-runs/{id}/retry/

The billing run detail endpoint response includes, among other things, lines, attempts, related transactions, and status.

A shortened response might look like this:

{
  "id": 32,
  "contract_id": 33,
  "customer_id": 1008,
  "customer_reference": "customer-123",
  "period_start_at": "2026-05-20T00:00:00Z",
  "period_end_at": "2026-06-20T00:00:00Z",
  "state": "succeeded",
  "currency": "ISK",
  "subtotal_amount": "4500.0000",
  "tax_amount": "0.0000",
  "total_amount": "4500.0000",
  "attempt_count": 1,
  "lines": [
    {
      "id": 71,
      "price_id": 200,
      "price_version_id": 15,
      "product_name": "Vefáskrift",
      "quantity": 2,
      "line_total_amount": "4000.0000",
      "service_period_start_at": "2026-05-20T00:00:00Z",
      "service_period_end_at": "2026-06-20T00:00:00Z"
    }
  ],
  "attempts": [
    {
      "id": 44,
      "attempt_no": 1,
      "state": "succeeded",
      "transaction_id": 19,
      "fail_code": null,
      "fail_message": null
    }
  ]
}

Versioning policy and rate limits

V2 is versioned by URL, that is, under /api/v2/. New breaking changes should therefore appear under a new major-version path rather than silently changing the meaning of existing endpoints.

No documented rate limits are defined on this page. Integrations should still expect transient failures, use retries with backoff, and log responses with status codes such as 400, 404, and 5xx.

Example end-to-end flow in Python

  • python
import requests

API_KEY = 'your api key here'
headers = {
    "Authorization": f"Api-Key {API_KEY}",
    "Content-Type": "application/json",
}

quote_payload = {
    "customer_reference": "customer-123",
    "currency": "ISK",
    "bundle_template": 42,
    "bundle_item_selections": [
        {
            "bundle_item": 100,
            "selected_price": 200,
        }
    ],
    "initial_items": [
        {
            "price": 500,
            "quantity": 1,
        }
    ],
}

try:
    quote_response = requests.post(
        "https://askell.is/api/v2/subscription-offer-quotes/",
        json=quote_payload,
        headers=headers,
    )
    quote_response.raise_for_status()

    processor_response = requests.post(
        "https://askell.is/api/v2/payment-processor-options/",
        json=quote_payload,
        headers=headers,
    )
    processor_response.raise_for_status()
    processor_options = processor_response.json()["results"]

    # Hér er gert ráð fyrir að aðeins einn færsluhirðir komi til greina.
    # Ef fleiri en einn er í boði þarf samþættingin að leyfa val.
    checkout_payload = {
        **quote_payload,
        "collection_method": "card",
        "account_payment_processor": processor_options[0]["account_payment_processor_id"],
    }

    checkout_response = requests.post(
        "https://askell.is/api/v2/checkouts/",
        json=checkout_payload,
        headers=headers,
    )
    checkout_response.raise_for_status()
    checkout = checkout_response.json()

    finalize_response = requests.post(
        f"https://askell.is/api/v2/checkouts/{checkout['token']}/finalize/",
        json={},
        headers=headers,
    )
    finalized_checkout = finalize_response.json()

    if finalize_response.status_code == 400:
        contract_id = finalized_checkout.get("contract_id")
        initial_billing_run_id = finalized_checkout.get("initial_billing_run_id")

        if contract_id:
            # Greiðslutilraun mistókst, en samningur og innheimtulota gætu þegar verið til.
            # Hér ætti raunveruleg samþætting að skrá þetta og meta hvort reyna eigi aftur.
            print(
                "Checkout failed after contract creation",
                contract_id,
                initial_billing_run_id,
            )
        else:
            # Forsendubrestur: enginn samningur var stofnaður.
            finalize_response.raise_for_status()
    else:
        finalize_response.raise_for_status()
except requests.HTTPError as exc:
    response = exc.response
    try:
        error_body = response.json()
    except ValueError:
        error_body = {"raw": response.text}
    print(response.status_code, error_body)
    raise

Common errors

Error

HTTP

Description

customer_reference

400

The customer was not found or is missing from the request data.

account_payment_processor

400

The selected payment processor is not valid for the quote.

payment_method

400

No verified payment method was found for the customer.

bundle_item_selections

400

A required price selection is missing or does not match the bundle.

additional_items

400

The selected add-on product does not match an active add-on rule.

items

400

The direct price selection is invalid or does not match the currency.

Not found

404

The contract, checkout, or billing run was not found for the account.