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:
Fetch products, prices, or bundles from the catalog.
Calculate a quote with
POST /api/v2/subscription-offer-quotes/.Fetch eligible payment processors with
POST /api/v2/payment-processor-options/.Create a
checkoutwithPOST /api/v2/checkouts/.Finalize the
checkoutwithPOST /api/v2/checkouts/{token}/finalize/.Monitor the status of the
checkoutand, 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:
When selling individual products or a combination of products as a subscription contract.
When using bundles with selectable prices and quantities.
When price changes need to be scheduled with price versions.
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 |
|---|---|
|
A product in the catalog. |
|
A stable price identifier that a contract is linked to. |
|
A time-bounded price version that defines an amount for a period. |
|
A sellable bundle that combines one or more products. |
|
The new persistent subscription contract. |
|
A single billing run on a contract. |
|
A single attempt to collect a billing run. |
|
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:
The customer must already exist in Askell.
A payment processor (
AccountPaymentProcessor) must be configured for the relevant currency and payment method.If the
checkout/finalizeflow 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¤cy=ISK" \
-H "Authorization: Api-Key your-secret-api-key"
Common catalog filters:
Filter |
Description |
|---|---|
|
By default only active records are shown. |
|
Product or bundle reference, depending on the endpoint. |
|
Product ID or product reference on the price endpoint. |
|
Price currency. |
|
|
|
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 |
|---|---|
|
The current display amount for the price. |
|
The ID of the price version currently considered active. |
|
Past, current, and future price versions. |
|
The start of the validity period for the currently displayed version. |
|
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 |
|---|---|
|
The ID of the selected bundle. |
|
Bundle quantity. |
|
Price or quantity selections for individual items within a bundle. |
|
Directly selected recurring products without a bundle. |
|
One-time products or products that should only appear on the first billing run. |
|
Controls whether the initial period is not billed, billed immediately, or added to the next invoice. |
|
Add-on products selected through add-on rules. |
|
Add-on bundles selected through add-on rules. |
Only one primary input form for recurring products may be used at a time:
bundle_templateitems
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.
When the shipping option has a rate table by zone and weight band, the fee is calculated when the checkout is created, from the postal code of the delivery address (delivery_address in the request, otherwise the customer’s address) and the weight of the products (weight_grams on the products multiplied by quantity; products with dimensions use their volumetric weight if it is higher). If the rate table does not cover the shipment — the postal code is outside every zone, a product has no weight, or the shipment is heavier than the top band — the request is rejected with 400, a shipping explanation and shipping_code set to shipping_not_available. The snapshot on the contract then records the zone_name and weight_band (min-max in grams) the price was read from; both are empty strings for an option with a fixed price.
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.
The shipping fee is part of the quote: subtotal_amount, tax_amount and total_amount on the checkout response include it, and shipping_fee shows the fee broken down (amount, subtotal_amount, tax_amount, total_amount) or null. The amount the card is authorized for is therefore the same as the amount charged. If the first period ships no products — a trial period defers the products to the first renewal — shipping_fee is null and the fee is in neither the checkout amount nor the first billing run; the agreed price is still recorded in the snapshot, and the first billing run that ships products charges it.
Every billing run that ships products gets a line of type shipping in lines with the fee, and the billing run’s total_amount (and the transaction charged) includes it. The first billing run that ships products — the first billing run, or the first renewal after a trial period — charges exactly the agreed fee from the snapshot; later renewals use the shipping option’s current price and evaluate the free-shipping threshold against the renewal’s product amount after discounts. Coupon codes do not reduce the fee. If the option has been deleted, the agreed fee from the snapshot is used. The fee is only charged when the shipping feature is enabled for the account; a shipping selection recorded before fees were charged is not billed.
VAT on the fee follows the shipping option’s tax_rate (set in the dashboard), or the account’s default tax rate if none is set. Electronic orders and billing runs without a product line get no shipping line.
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_referencecurrencybundle_templateoritemsbundle_item_selectionsadditional_itemsadditional_bundlesinitial_itemsinitial_billing_modemetadatareference, the contract’s reference in an external system, see Contract reference and metadatapayment_processor_overrideif 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:
noneDefault value. The contract and recurring contract items are created, and
next_billing_atis 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_runCreates a scheduled initial
BillingRunwith recurring lines for the first period and any one-timeinitial_items. The first billing follows the same initial-proration rules asPOST /api/v2/subscription-offer-quotes/and can therefore be lower than therecurring_*amounts if the contract starts inside a shorter initial period.next_invoiceDoes 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 |
|---|---|
|
Contract state: |
|
The customer ID. |
|
The customer reference. |
|
The contract reference. Only contracts with exactly the same reference are returned. |
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 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 |
|---|---|
|
The contract ID. |
|
The customer ID and the nested customer data. |
|
The customer reference. |
|
The contract’s reference in an external system, or |
|
|
|
Contract currency. |
|
Whether the contract keeps billing. Becomes |
|
Increments on every change to the contract. |
|
The billing anchor time. |
|
The time zone and time of day that billing runs at. |
|
When the billing period advances: |
|
The next scheduled billing. |
|
Trial period, if applicable. |
|
|
|
|
|
|
|
|
|
Whether service should be delivered right now, and the overall service state ( |
|
A summary of the service entitlements on the contract. |
|
Billing on the contract needs attention, for example after a failed charge. |
|
Whether a pause is scheduled in the future, and the date the pause runs until. |
|
Recorded pauses on the contract. |
|
Contract items with price, quantity and service periods. |
|
The latest billing run, with |
|
The initial billing run, if one was created. |
|
The active discount on the contract, if any. |
|
Delivery address and the chosen shipping option. |
|
IDs of legacy subscriptions that were migrated into the contract. |
|
The integration’s metadata, plus where the contract came from in |
|
URL of the subscriber page for the contract. |
|
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:
stateandservice_active/service_statefor the current status.next_billing_atfor the next billing.latest_billing_run.period_end_atfor the end of the period that was last billed.items[].current_service_period_end_atanditems[].entitled_untilfor the service entitlements of individual items.paused_untilif the contract is paused.
Cancellation is expressed in four fields:
Field |
Meaning |
|---|---|
|
|
|
The time the cancellation takes or took effect. Set both for a scheduled and an immediate cancellation. |
|
The time the cancellation was actually applied. |
|
The time the contract ended. |
Typical cases:
POST /cancel/withcancel_at_period_end=true:statestaysactive,cancel_at_period_endbecomestrue,cancel_atis set to the end of the current period, andcanceled_atandended_atremainnull.POST /cancel/with acancel_atin the future:cancel_at_period_endisfalse,cancel_atholds the chosen time, andcanceled_atandended_atremainnull.POST /cancel/withoutcancel_at_period_endandcancel_at: the cancellation is immediate.statebecomescanceled, all three time fields are set to the current time,recurringbecomesfalseandnext_billing_atbecomesnull.When a scheduled cancellation is applied:
statebecomescanceled,cancel_at_period_endgoes tofalse,canceled_atis the time the cancellation ran, andended_atis the time the cancellation was due to take effect.POST /restart/on a contract with a scheduled cancellation clearscancel_at_period_endandcancel_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_sizeenables paginationthe default
page_sizeis10when pagination is enabledthe maximum
page_sizeis1000pageselects 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, reference, 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.
Contract reference and metadata
reference is the contract’s reference in an external system, such as the seller’s order number. It is separate from customer_reference, which is the customer’s reference. reference can be at most 128 characters and must not contain a comma; otherwise the request is rejected with 400 and an error on reference. A blank value or null means no reference. The reference can be sent when the contract is created, changed or cleared with PATCH, and lists can be filtered by it with ?reference=.... V2 payment pages put the subscription_reference from the page URL in reference, see Payment pages. A contract created by a checkout gets the contract_reference the seller backend sends when it creates a checkout session, or a checkout with POST /api/v2/checkouts/, see Contract reference.
metadata is the integration’s metadata. A contract created by a checkout gets the metadata of the checkout session and of the checkout, see Embedded checkout, and Áskell records where the contract came from in askell_source:
|
Contract origin |
|---|---|
|
A checkout in a checkout session. Comes with |
|
|
|
A V2 payment page. Comes with |
Contracts created directly with POST /api/v2/subscription-contracts/ get no askell_source; there metadata is stored as sent.
Áskell itself uses the following keys in contract metadata: askell_source, billing_anchor_mode, activation_failure, email_markers, copied_legacy_pauses, copied_legacy_extra_data, migration_source, migration_cadence_mode, legacy_subscription_ids and seed. billing_anchor_mode, for example, controls when the contract is billed. metadata in PATCH replaces the integration’s keys, but these keys stay as they are: those on the contract keep their values and any sent in the request are ignored. {"metadata": {}} therefore clears only the integration’s keys. Nor are these keys copied to the contract from checkout or checkout session metadata.
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 |
|---|---|
|
Previews the impact of a change without executing it. |
|
Adds a new item to the contract. |
|
Updates an item’s price, quantity, discount or fixed unit amount. |
|
Removes an item from the contract. |
Scheduled changes are canceled with POST /scheduled-changes/{scheduled_change_id}/cancel/, see Scheduled item changes (apply_at=period_end).
Main request fields:
Action |
Fields |
Required |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
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.
apply_at in items/update and proration-preview (with operation=update_item) controls when the change takes effect:
now(default): the item is changed immediately and the change is prorated fromeffective_at.effective_atmust not be in the future; such a value returns a400response withcodeset tofuture_effective_at_not_supported. Omiteffective_atto use the current time.period_end: the change waits for the item’s next renewal, see Scheduled item changes (apply_at=period_end).
An update_item preview also returns apply_at, scheduled_for (when a scheduled change takes effect, otherwise null), next_billing_at_after_change (the item’s next billing after the change) and billing_cadence_after_change (the contract’s billing cadence after the change, in the same shape as billing_cadence). These fields are not included in previews of other operations.
Scheduled item changes (apply_at=period_end)
With apply_at=period_end the item’s current price stays in force until the end of the current service period, with no refund, and the new price, quantity or discount takes effect at the next renewal. This suits, for example, a same-interval downgrade from Pro to Plus, or moving from a yearly price to a monthly price at the end of the year already paid for.
Nothing is charged and no proration lines are created when the change is scheduled. The preview returns
net_amount_due_nowas0, no lines, and anext_billing_estimatebased on renewing at the new price.effective_at,settlement_behaviorandinclude_pending_adjustmentsare not allowed withperiod_endandproration_behaviormay only benone; anything else returns a400response (effective_atas a field error, the others withcodeset toinvalid_apply_at).items/updatereturnschange_idasnulland the scheduled change inscheduled_change. The item itself is unchanged until the change takes effect.When the renewal billing run is created, the change is first applied to the item and the run is then priced at the new values. A change (
update_itemwithproration_behavior=none) is recorded in the contract’s history. For a billing cadence change the new cadence starts at the renewal, for example a monthly period instead of a year.Each item can have only one scheduled change. Another scheduled change or an immediate change to the same item returns a
409response withcodeset toscheduled_change_exists; cancel the existing one first.If the item is removed or the contract is canceled (immediately, at period end, or on a set date on or before the renewal), the scheduled change is canceled automatically. A change cannot be scheduled on a contract that ends at period end (
codecontract_cancellation_scheduled).A paused contract creates no billing runs, so the change waits for the first renewal after the pause ends. If the billing anchor changes,
scheduled_formoves with the item’s next renewal.Only active contracts can schedule changes.
Pending scheduled changes are listed in scheduled_changes on the contract and in scheduled_change on the item. Each entry includes, among others, id, contract_item, state (scheduled, applied, canceled or failed), scheduled_for, price_id, price, quantity, discount_percent, unit_amount_override, from_price_id, from_quantity, applied_at, canceled_at, applied_change_id and billing_run_id.
POST /api/v2/subscription-contracts/{id}/scheduled-changes/{scheduled_change_id}/cancel/ cancels a scheduled change that has not taken effect yet (optional field: reason). The response contains scheduled_change, replayed and contract. Canceling a change that is already canceled returns replayed=true; a change that has already taken effect returns 409 with code scheduled_change_not_cancelable.
The subscription_contract_scheduled_change.created, subscription_contract_scheduled_change.applied and subscription_contract_scheduled_change.canceled webhooks are sent when a change is scheduled, takes effect or is canceled. They are not covered by subscription_contract.*, see Scheduled item changes (subscription_contract_scheduled_change.*).
Example of downgrading an item at the next renewal:
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,
"price": 789,
"apply_at": "period_end",
"idempotency_key": "item-456-plus-vid-endurnyjun"
}'
Changing the billing cadence immediately
If the new price has a different billing cadence than the current price (a different interval, interval_count, recurrence_type or calendar schedule) and apply_at=now, a new service period starts immediately:
The unused part of the current period is credited based on what was billed for it.
A full period at the new price is charged from
effective_at(proration_factoris1for a regular interval price).The billing anchor of the item and the contract moves to
effective_atand the next billing is at the end of the new period, for example about a year later when moving from a monthly price to a yearly price.next_billing_at_after_changein the preview shows that time.The change only takes effect once the difference has been collected. Until then the item keeps its current price and cadence: items/update returns applies_on_payment=true and the item shows the change in pending_interval_change (change_id, billing_run_id, status, price_id, quantity, effective_at, next_billing_at). The preview also returns applies_on_payment. When the billing run succeeds the item is changed, the new period is granted from effective_at and the subscription_contract_item.entitlement_changed webhook is sent. If nothing is left to collect (the credit covers the charge), the change takes effect immediately.
While the payment is pending, the item’s renewal on the old cadence is held back, and other changes to the item (immediate or scheduled) return
409withcodepending_interval_change, as does moving the contract’s billing anchor (change-anchor). Removing the item is not allowed while the billing run is open (409billing_run_overlap).If the billing run fails terminally nothing is changed and the item keeps billing on the old cadence. If a manual retry of the run later succeeds, the change takes effect, unless the item was changed or removed or the contract was canceled in the meantime; then the change is not applied. Renewals on the old cadence billed after effective_at are then credited to the contract for the time the new period covers.
Canceling the contract (immediately or at period end), removing the item or moving the billing anchor after a failed payment cancels the change and cancels its billing run if it has not been collected.
The difference is collected immediately, so
settlement_behaviordefaults toinvoice_now;next_invoicereturns400withcodeinterval_change_requires_invoice_now. Withproration_behavior=noneno credit is calculated, but the new period is still charged in full and starts immediately. If the credit exceeds the charge, the difference becomes a credit balance on the contract.A billing cadence change is only supported on contracts with a single recurring item; otherwise a
400is returned withcodeinterval_change_requires_single_item. The same applies toapply_at=period_end.
To move from a yearly price to a monthly price without a refund, use apply_at=period_end.
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
nullis 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 |
|---|---|
|
Cancels the contract, either immediately or at the end of the period. |
|
Restarts a contract that has been stopped. |
|
Temporarily pauses a contract. |
|
Resumes a paused contract. |
|
Deletes a scheduled pause. |
|
Activates an inactive contract if it can move to the active state. |
|
Moves the billing anchor, see Changing the billing anchor. |
Main request fields:
Action |
Fields |
Required |
|---|---|---|
|
|
All optional |
|
|
All optional |
|
|
|
|
|
All optional |
|
|
|
Lifecycle operations that can affect pricing within an active service period support the same proration pattern as item changes:
Call
POST /api/v2/subscription-contracts/{id}/proration-preview/withoperationset tocancel,pause,resume,restartorchange_anchor.Store the
preview_tokenfrom the response if the same operation is to be executed right away.Send the same
preview_tokenback in the execution call along with the same settings forproration_behaviorandsettlement_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 manual adjustments are not part of the external V2 API flow at this time, but the whole charge of a billing run can be refunded; see Refunding a billing run.
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.
Credit balance is automatically deducted from the contract’s later billing runs, both renewals and proration runs collected immediately, after taxes and line totals have been calculated but before the payment processor is charged. Credit can be used partially or in full, and if the credit covers the whole billing run the payment processor is not charged. Credit created in a billing run cannot be used by that same run. If a billing run fails terminally or is canceled, the credit it used is released again.
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"
}'
Changing the billing anchor
POST /api/v2/subscription-contracts/{id}/change-anchor/ moves the billing anchor of an active contract. The next billing of the contract and of all its active items moves to new_billing_anchor_at: billing_anchor_at takes the new time, next_billing_at becomes that day at the contract’s billing time, and the next regular period starts there. The new time can be earlier or later than the current next_billing_at.
Field |
Description |
|---|---|
|
Required. The new time of the next billing. Must be after |
|
The time the proration is calculated from. Defaults to the time the call is received. To use a |
|
|
|
|
|
Includes the contract’s pending lines in the billing run that is created right away. Only valid with |
|
The reason for the change. |
|
A call with the same key and the same fields, |
|
The |
With proration, every active item whose price is not never_prorate gets two lines with proration_reason set to anchor_changed:
A credit line (
proration_credit) for the unused part of the paid period, fromeffective_atto its end. It is only created when the item has a paid period that coverseffective_at.A debit line (
proration_debit) fromeffective_atup to the new billing anchor, prorated against the item’s full period that ends at the new time. With a monthly price,effective_aton January 20 and a new billing anchor on February 1, the factor is 12/31, that is 12 days of the period from January 1 to February 1.
Overall, the customer therefore pays roughly for the gap between the end of the paid period and the new time, or receives credit if the new time is earlier. With next_invoice the lines wait and go on the billing at the new time, together with the first regular period from there. Other pending lines of the contract items, for example from an earlier change with next_invoice, move to the same billing. With invoice_now a billing run for the difference between the lines is created right away; if the credit exceeds the debit, the difference becomes credit on the contract.
Important
The move does not extend service entitlements. The items’ entitled_until, current_service_period_start_at and current_service_period_end_at stay unchanged, and the billing run that invoice_now creates does not change them either, even when it succeeds, because entitlements are only granted from the regular lines of a billing run. If the new time is later than the end of the paid period, the contract’s service_state becomes past_due and service_active becomes false from the end of the paid period until the billing run at the new time grants entitlements for the next period, which by default happens when it succeeds.
The call returns 400 or 409 with a code from the following table:
|
HTTP |
Meaning |
|---|---|---|
|
|
The contract is not in the |
|
|
|
|
|
|
|
|
With proration, the new time is more than one item period after |
|
|
With proration: an item’s period cannot be derived from its price. |
|
|
An item’s price has no effective price version. |
|
|
|
|
|
|
|
|
The |
|
|
The |
|
|
The contract or its pending lines have changed since the preview, or the fields differ from it. |
|
|
A billing run being processed covers the period the change affects. |
|
|
A scheduled billing run covers the period the change affects. |
|
|
The |
|
|
A billing-interval change on an item is awaiting payment (see the item’s |
Example of moving the next billing to June 1:
curl -X POST https://askell.is/api/v2/subscription-contracts/123/change-anchor/ \
-H "Authorization: Api-Key your-secret-api-key" \
-H "Content-Type: application/json" \
-d '{
"new_billing_anchor_at": "2026-06-01T00:00:00Z",
"effective_at": "2026-05-16T00:00:00Z",
"idempotency_key": "samningur-123-1-juni"
}'
The response has the same shape as the items/update response: change_id, billing_run_id (the billing run that invoice_now created, otherwise null), pending_adjustment_ids (the change’s pending lines), consumed_pending_adjustment_ids, replayed and contract.
Checkout flow
When payment pre-processing is required before a contract becomes active, the checkout flow is recommended:
Calculate or confirm the quote.
Fetch eligible payment processors with
POST /api/v2/payment-processor-options/.Create a
checkoutwithPOST /api/v2/checkouts/.Finalize the
checkoutwithPOST /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.
With contract_reference you can give the contract the checkout creates a reference, such as an order number. It goes into the contract’s reference as soon as the contract is created, so it is already in subscription_contract.created. The same rules apply as for reference, see Contract reference and metadata and Contract reference.
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.
Hosted card form and embedding
When render_mode is generic_card_form, a card can be registered in a card form hosted by Áskell:
POST /api/v2/checkouts/{token}/payment-method-registrations/
The response contains checkout_url, which points at the hosted card form, and status_url, which reads the status of the registration with a secret API key.
The hosted card form sends a Content-Security-Policy header with frame-ancestors. By default that lists the account’s allowed checkout origins (checkout_allowed_origins). To embed the form on your own site, send allowed_origin, either when the checkout is created or when the registration is created:
{
"mode": "hosted",
"allowed_origin": "https://shop.example",
"return_url": "https://shop.example/complete"
}
allowed_origin is a single http(s) origin with no path. http is accepted only for localhost and loopback addresses (127.0.0.0/8 and [::1]). An origin on the registration takes precedence over an origin on the checkout, and a supplied origin takes precedence over the account’s allowed checkout origins. Checkout-session browser endpoints do not accept allowed_origin; the sales channel’s allowed origins apply there.
Prerequisites before calling finalize
finalize now performs a preflight check before creating the contract. The following must therefore be valid before attempting finalize:
The customer must exist.
The selected payment processor must be valid for the quote.
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:
Precondition failure: no contract and no billing run are created.
Failed payment attempt: the contract and billing run may already have been created, but
checkout.statusbecomesfailed.
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 |
|---|---|---|---|
|
|
The contract has been created and the initial billing has completed. |
Store |
|
|
An external processor must complete its flow before the billing is considered complete. |
Monitor the |
|
|
The payment attempt failed after the contract and billing run were created. |
Inspect |
Precondition failure |
|
The request data or payment prerequisites were invalid; no contract was created. |
Correct the request data or payment method before trying again. |
Billing runs, retries, and refunds
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
}
]
}
See also
A billing run that pays for a physical product creates a fulfillment order. They are documented under Fulfillment orders V2.
Refunding a billing run
The charge of a succeeded billing run can be refunded with the following endpoint:
POST /api/v2/billing-runs/{id}/refund/
The request has no body and requires a secret API key. Public keys and legacy token authentication are not supported on this endpoint.
Only a billing run in the succeeded state that has a settled transaction can be refunded, and the transaction’s payment processor must support refunds. Refunds are full refunds: the whole amount of the transaction is refunded to the payment method that was charged. Partial refunds are not supported.
HTTP |
Description |
Next step |
|---|---|---|
|
The payment processor refunded the transaction. The billing run is now in the |
No further action is needed. |
|
The refund is not reflected on the billing run yet, which is still in the |
Do not treat the response as an error and do not immediately resend the request. Wait for the |
|
The billing run is not in the |
Read the error message in |
|
No billing run with this |
Check the billing run |
Every refund request that the payment processor accepts is recorded in the billing run’s metadata.refund_requests with actor, requested_at, outcome (refunded or pending), and transaction_id. Failed requests are not recorded.
Concurrent refund requests for the same billing run are processed one at a time. A request that arrives after an earlier request refunded the charge gets a 400 response and the billing run is in the refunded state. If a request gets no response, for example because of a timeout, read the billing run with GET /api/v2/billing-runs/{id}/ and check state and metadata.refund_requests before sending the request again.
V2 payment.* events carry the transaction’s uuid and the billing_run_id of the billing run that charged it. Use billing_run_id in the endpoint above. The legacy POST /api/payments/<uuid>/refund/ endpoint only refunds individual payments. Given the uuid of a transaction charged by a V2 billing run, it answers with 400, including billing_run_id and pointing to POST /api/v2/billing-runs/{id}/refund/.
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
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 |
|---|---|---|
|
|
The customer was not found or is missing from the request data. |
|
|
The selected payment processor is not valid for the quote. |
|
|
No verified payment method was found for the customer. |
|
|
A required price selection is missing or does not match the bundle. |
|
|
The selected add-on product does not match an active add-on rule. |
|
|
The direct price selection is invalid or does not match the currency. |
Not found |
|
The contract, |