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.

initial_billing_mode

Controls whether the initial period is not billed, billed immediately, or added to the next invoice.

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.

Shipping selection

An account can connect a shipping provider (Dropp, Pósturinn or in-store pickup) and define shipping options in the dashboard. When the account offers active shipping options and the cart contains a product that is not marked as electronic, POST /api/v2/checkouts/ must include a shipping object:

{
  "shipping": {
    "option": 12,
    "location_id": "9591",
    "location_name": "Póstbox Hallveigarstíg",
    "location_address": "Hallveigarstíg 1, 101 Reykjavík"
  }
}

option is the ID of an active shipping option on the account. location_id is required for options that need a location choice, for example parcel lockers or Dropp pickup points. On finalize the selection is stored as an immutable snapshot on the contract and returned as shipping_selection in contract responses, with the name, price, service code and chosen pickup location as they were at the time of purchase.

A shipping option can have a free-shipping threshold (free_above_amount). When the order total reaches the threshold, the shipping price is recorded as zero in the snapshot.

Note

The shipping price (price_amount) is only recorded on the snapshot in this version; it is not added to billing runs and is therefore not charged. Do not present it to customers as a fee that will be collected.

Fixed billing date

Recurring monthly or yearly prices can have billing_day_of_month. If more than one recurring price in the same quote or contract has such a day, they must reference the same day of month. Prices fixed to different days cannot be mixed, for example one price on day 1 and another on day 15 of the month.

Prices with a fixed day can, however, be mixed with prices without billing_day_of_month. Prices without their own day follow the single fixed day present in the contract.

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.

For recurring items, the response now distinguishes:

  • first_period_recurring_*: what is charged in the first run, including initial proration when the first run is shorter than a normal renewal period.

  • recurring_*: the normal renewal amount after the first shorter period has passed.

Individual recurring lines also expose service_period_start_at, service_period_end_at, proration_factor, and renewal_line_* so an integration can clearly show the difference between the first billing and later renewals.

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",
  "first_period_recurring_subtotal_amount": "4000.0000",
  "first_period_recurring_tax_amount": "0.0000",
  "first_period_recurring_total_amount": "4000.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",
      "service_period_start_at": "2026-05-20T00:00:00Z",
      "service_period_end_at": "2026-06-20T00:00:00Z",
      "proration_factor": "1.000000",
      "line_total_amount": "4000.0000",
      "renewal_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

  • initial_billing_mode

  • 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.

initial_billing_mode makes initial billing explicit:

none

Default value. The contract and recurring contract items are created, and next_billing_at is set from the next billing time of the contract items. No billing run or initial proration line is created for the period from contract creation until the first scheduled billing. This is suitable when the initial period should be free of charge or is billed outside Áskell.

create_initial_run

Creates a scheduled initial BillingRun with recurring lines for the first period and any one-time initial_items. The first billing follows the same initial-proration rules as POST /api/v2/subscription-offer-quotes/ and can therefore be lower than the recurring_* amounts if the contract starts inside a shorter initial period.

next_invoice

Does not create an initial BillingRun. Instead, initial proration lines for the recurring products are stored as pending lines and targeted at the first scheduled billing. When the first regular billing run is created, it includes both the initial period and the next regular period. This is suitable when service should start immediately but the initial period should be prorated onto the first due date.

initial_items are only allowed with initial_billing_mode=create_initial_run. They are not allowed with none or next_invoice because they do not become persistent contract items and therefore cannot be moved automatically to the next regular billing.

Example: service starts immediately but the initial period is billed on the first due date:

curl https://askell.is/api/v2/subscription-contracts/ \
  -H "Authorization: Api-Key your-secret-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_reference": "customer-123",
    "currency": "ISK",
    "items": [
      {
        "price": 200,
        "quantity": 1
      }
    ],
    "initial_billing_mode": "next_invoice"
  }'

If the contract is created on May 21 and the first scheduled billing is June 1, the prorated period from May 21 to June 1 is added to the June 1 billing together with the regular period from June 1 to July 1.

Fetch and update a contract

A contract is fetched from:

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

The contracts of the account are fetched from GET /api/v2/subscription-contracts/. The list supports the following filters:

Filter

Description

state

Contract state: inactive, active, paused, canceled or ended.

customer

The customer ID.

customer_reference

The customer reference.

Note

The list supports no ordering parameter and no filters other than those listed above. Unknown parameters do not return an error, they are simply ignored, so for example ?ordering=-created_at, ?reference=... or ?legacy_subscription_id=... return the list unchanged. The ordering is fixed: newest contracts first (-created_at, -id). This differs from the legacy GET /api/subscriptions/, which supports ordering on active_until and start_date.

Fields in the contract response

The same response format is used for contract creation, a single contract, lists and lifecycle calls. The main fields are these:

Field

Description

id

The contract ID.

customer_id / customer

The customer ID and the nested customer data.

customer_reference

The customer reference.

state

inactive, active, paused, canceled or ended.

currency

Contract currency.

recurring

Whether the contract keeps billing. Becomes false on cancellation.

contract_version

Increments on every change to the contract.

billing_anchor_at

The billing anchor time.

billing_timezone / billing_time

The time zone and time of day that billing runs at.

billing_advance_policy

When the billing period advances: on_run_created or on_run_succeeded.

next_billing_at

The next scheduled billing. null after cancellation.

trial_start_at / trial_end_at

Trial period, if applicable.

cancel_at_period_end

See Validity and cancellation.

cancel_at

See Validity and cancellation.

canceled_at

See Validity and cancellation.

ended_at

See Validity and cancellation.

service_active / service_state

Whether service should be delivered right now, and the overall service state (active, inactive, partial, past_due).

entitlement_summary

A summary of the service entitlements on the contract.

billing_attention_required

Billing on the contract needs attention, for example after a failed charge.

has_future_pause / paused_until

Whether a pause is scheduled in the future, and the date the pause runs until.

pauses and current_pauses / future_pauses / past_pauses

Recorded pauses on the contract.

items

Contract items with price, quantity and service periods.

latest_billing_run

The latest billing run, with period_start_at and period_end_at.

initial_billing_run

The initial billing run, if one was created.

discount

The active discount on the contract, if any.

delivery_address / shipping_selection

Delivery address and the chosen shipping option.

legacy_subscription_ids

IDs of legacy subscriptions that were migrated into the contract.

metadata

Free-form data belonging to the integration.

subscriber_page

URL of the subscriber page for the contract.

created_at / updated_at

Creation and modification time.

subscriber_page is the URL of the subscriber page for the contract, where the customer can manage their own subscription. It is the same URL that is shown in the dashboard and the field is read-only.

{
  "id": 33,
  "subscriber_page": "https://askell.is/change_contract/<token>/"
}

Validity and cancellation

Important

V2 contracts have neither active_from nor active_until. active_until is a field on the legacy Subscription model and is derived from its last billing log. The equivalent information in V2 comes from the contract state, the service periods of the items and the billing runs.

To answer the question “is this subscription active, and for how long?” use:

  • state and service_active / service_state for the current status.

  • next_billing_at for the next billing.

  • latest_billing_run.period_end_at for the end of the period that was last billed.

  • items[].current_service_period_end_at and items[].entitled_until for the service entitlements of individual items.

  • paused_until if the contract is paused.

Cancellation is expressed in four fields:

Field

Meaning

cancel_at_period_end

true while a cancellation is scheduled for the end of the current period. The field returns to false once the cancellation has been applied.

cancel_at

The time the cancellation takes or took effect. Set both for a scheduled and an immediate cancellation.

canceled_at

The time the cancellation was actually applied. null while a cancellation is only scheduled.

ended_at

The time the contract ended.

Typical cases:

  1. POST /cancel/ with cancel_at_period_end=true: state stays active, cancel_at_period_end becomes true, cancel_at is set to the end of the current period, and canceled_at and ended_at remain null.

  2. POST /cancel/ with a cancel_at in the future: cancel_at_period_end is false, cancel_at holds the chosen time, and canceled_at and ended_at remain null.

  3. POST /cancel/ without cancel_at_period_end and cancel_at: the cancellation is immediate. state becomes canceled, all three time fields are set to the current time, recurring becomes false and next_billing_at becomes null.

  4. When a scheduled cancellation is applied: state becomes canceled, cancel_at_period_end goes to false, canceled_at is the time the cancellation ran, and ended_at is the time the cancellation was due to take effect.

  5. POST /restart/ on a contract with a scheduled cancellation clears cancel_at_period_end and cancel_at.

A contract can also end without a cancellation, for example when a contract for a one-time purchase concludes. Then state becomes ended and ended_at is set, while canceled_at stays null.

The same fields are included in subscription_contract.* webhooks, see Webhooks.

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 with PATCH. Only metadata, payment_processor_override, delivery_address, accounting_department and accounting_cost_center can be changed there; other fields return a 400 error. 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.

Item changes on an active contract

Note

Updated August 19 2026: unit_amount_override is now supported in items/add, items/update and proration-preview, and unsupported fields now return a 400 error instead of being ignored. See Change log.

V2 supports prorated changes to the items of an active contract:

Path

Description

POST /proration-preview/

Previews the impact of a change without executing it.

POST /items/add/

Adds a new item to the contract.

POST /items/update/

Updates an item’s price, quantity, discount or fixed unit amount.

POST /items/remove/

Removes an item from the contract.

Main request fields:

Action

Fields

Required

POST /proration-preview/

operation (add_item, update_item, remove_item, pause, resume, cancel, restart) along with the fields of the selected operation

operation optional, defaults to update_item

POST /items/add/

price, quantity, discount_percent, unit_amount_override, effective_at, proration_behavior, settlement_behavior, include_pending_adjustments, reason, idempotency_key, preview_token

price is required; others are optional

POST /items/update/

contract_item, price, quantity, discount_percent, unit_amount_override, effective_at, proration_behavior, settlement_behavior, include_pending_adjustments, reason, idempotency_key, preview_token

contract_item is required; others are optional

POST /items/remove/

contract_item, effective_at, proration_behavior, settlement_behavior, include_pending_adjustments, reason, idempotency_key, preview_token

contract_item is required; others are optional

In items/update you only need to send the fields you want to change; omitted fields keep the item’s current values. If the request contains no actual change, a 400 response with code set to no_change is returned.

Unknown fields in requests to these endpoints return a 400 error instead of being silently ignored. The same applies to fields not supported by the selected operation, for example unit_amount_override in items/remove.

Fixed item price (unit_amount_override)

unit_amount_override sets a fixed per-unit amount for an item instead of the catalog price. The field is supported at contract creation and also in items/add, items/update and proration-preview.

In items/update the following applies:

  • If the field is omitted, the item’s current fixed price is kept unchanged.

  • If an explicit null is sent, the fixed price is removed and the item reverts to the catalog price.

  • Changing only the fixed price counts as a valid change and does not return no_change.

Proration lines created when only the fixed price changes get the proration_reason value unit_amount_changed.

Example of setting a fixed price on an item:

curl -X POST https://askell.is/api/v2/subscription-contracts/123/items/update/ \
  -H "Authorization: Api-Key your-secret-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "contract_item": 456,
    "unit_amount_override": "8000.0000",
    "settlement_behavior": "invoice_now",
    "idempotency_key": "item-456-fast-verd"
  }'

The same preview pattern applies as for lifecycle operations: first call proration-preview with the same fields, store the preview_token from the response and send it with the execution call.

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, cancel_at, effective_at, reason, proration_behavior, settlement_behavior, include_pending_adjustments, idempotency_key, preview_token

All optional

POST /restart/

effective_at, reason, proration_behavior, settlement_behavior, include_pending_adjustments, idempotency_key, preview_token

All optional

POST /pause/

start_date, end_date, reason, proration_behavior, settlement_behavior, include_pending_adjustments, idempotency_key, preview_token

start_date and end_date are required; others are optional

POST /resume/

effective_at, reason, proration_behavior, settlement_behavior, include_pending_adjustments, idempotency_key, preview_token

All optional

Lifecycle operations that can affect pricing within an active service period support the same proration pattern as item changes:

  1. Call POST /api/v2/subscription-contracts/{id}/proration-preview/ with operation set to cancel, pause, resume or restart.

  2. Store the preview_token from the response if the same operation is to be executed right away.

  3. Send the same preview_token back in the execution call along with the same settings for proration_behavior and settlement_behavior.

include_pending_adjustments=true is only valid when settlement_behavior=invoice_now and must then be sent in both the preview and the execution.

proration_behavior can be none, create_prorations, or always_invoice. settlement_behavior can be next_invoice, invoice_now, or credit_balance. refund_manual and operations for one-time refunds or manual adjustments are not part of the external V2 API flow at this time.

Contract credit

A prorated credit, for example from cancellation, pause, or item changes, can create a credit transaction on the contract. This credit is recorded as immutable transaction history on the contract.

The current rule for the future implementation is that credit is applied after taxes and line totals have been calculated, but before attempting to charge the payment processor. Credit can be used partially or fully, and if credit covers the entire billing run, no processor charge is attempted. Credit created by the same billing run cannot be used by that same run.

Automatic use of credit in the next billing run is not yet part of the external API flow. Until then, credit is recorded and visible in the dashboard/history, but it does not automatically reduce the next charge.

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"
  }'

See Validity and cancellation for how a cancellation is reflected in cancel_at_period_end, cancel_at, canceled_at and ended_at after the call has been applied.

Example of previewing a prorated restart:

curl -X POST https://askell.is/api/v2/subscription-contracts/123/proration-preview/ \
  -H "Authorization: Api-Key your-secret-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "operation": "restart",
    "proration_behavior": "create_prorations",
    "settlement_behavior": "invoice_now"
  }'

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": {}
    }
  ]
}

render_mode tells the frontend how card details are collected: generic_card_form (card form hosted by Áskell), verifone_encrypted_card (encryption in the browser), adyen_checkout (Straumur/Adyen drop-in) or teya_checkout (Teya Embedded Checkout). Both adyen_checkout and teya_checkout use delayed-tokenization: the card is stored with the payment processor after the payment page has completed the checkout and the payment session is finalized with a webhook (or a synchronous confirmation with Teya).

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.