diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 3eebe245..90ea33ab 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.129.0"
+ ".": "0.130.0"
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 02064e31..74e9fd9b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## [0.130.0](https://github.com/kernel/kernel-node-sdk/compare/v0.129.0...v0.130.0) (2026-10-24)
+
+
+### Features
+
+* Support Link recurring spend requests on vault Link cards ([6becbe5](https://github.com/kernel/kernel-node-sdk/commit/6becbe5b788088db5334548a1b488208447181df))
+
## [0.129.0](https://github.com/kernel/kernel-node-sdk/compare/v0.128.0...v0.129.0) (2026-10-21)
diff --git a/api.md b/api.md
index e6269a01..3ed75618 100644
--- a/api.md
+++ b/api.md
@@ -545,6 +545,8 @@ Types:
- KernelCredentialVaultItemState
- KernelWalletState
- KernelWalletVaultItemSpec
+- LinkShippingAddress
+- LinkUserInfo
- ManagedAuthCredentialVaultField
- ManagedAuthCredentialVaultItemSpec
- ManagedAuthCredentialVaultItemSpecInput
diff --git a/package.json b/package.json
index bbbe93b8..18ae683a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@onkernel/sdk",
- "version": "0.129.0",
+ "version": "0.130.0",
"description": "The official TypeScript library for the Kernel API",
"author": "Kernel <>",
"types": "dist/index.d.ts",
diff --git a/src/core/pagination.ts b/src/core/pagination.ts
index 85e1d84d..cebdc295 100644
--- a/src/core/pagination.ts
+++ b/src/core/pagination.ts
@@ -200,7 +200,7 @@ export class OffsetPagination- extends AbstractPage
- {
return false;
}
- return super.hasNextPage();
+ return this.nextPageRequestOptions() != null;
}
nextPageRequestOptions(): PageRequestOptions | null {
@@ -221,6 +221,12 @@ export class OffsetPagination
- extends AbstractPage
- {
if (offset === 0) {
return null;
}
+ // Cursors can move in either direction. Stop only when the server repeats
+ // the cursor and would re-request the same page.
+ const requestOffset = (this.options.query as OffsetPaginationParams | undefined)?.offset;
+ if (typeof requestOffset === 'number' && offset === requestOffset) {
+ return null;
+ }
return {
...this.options,
diff --git a/src/resources/vaults/index.ts b/src/resources/vaults/index.ts
index b0de68f1..3b3ab789 100644
--- a/src/resources/vaults/index.ts
+++ b/src/resources/vaults/index.ts
@@ -35,6 +35,8 @@ export {
type KernelCredentialVaultItemState,
type KernelWalletState,
type KernelWalletVaultItemSpec,
+ type LinkShippingAddress,
+ type LinkUserInfo,
type ManagedAuthCredentialVaultField,
type ManagedAuthCredentialVaultItemSpec,
type ManagedAuthCredentialVaultItemSpecInput,
diff --git a/src/resources/vaults/items.ts b/src/resources/vaults/items.ts
index 4f0d67af..8cf6107e 100644
--- a/src/resources/vaults/items.ts
+++ b/src/resources/vaults/items.ts
@@ -368,7 +368,8 @@ export namespace CardVaultItemSpec {
*/
export interface LinkCardVaultItemSpec {
/**
- * Integer amount in minor currency units, at most 50000.
+ * Integer amount in minor currency units, at most 50000. With recurring, the
+ * spending limit per period, not a lifetime total.
*/
amount: number;
@@ -398,6 +399,19 @@ export namespace CardVaultItemSpec {
metadata?: { [key: string]: string };
+ /**
+ * Recurring terms for a subscription or other repeated purchase. The amount
+ * becomes the spending limit for each period of interval_count intervals, for
+ * example every 2 weeks. The merchant schedules charges. The card stays ready
+ * after each charge until it expires or is deleted; do not create a new card each
+ * period. Deleting the card, its wallet or its vault first cancels the credential
+ * with Link, so later charges decline; if Link cannot be reached, deletion returns
+ * 409 provider_revocation_pending and should be retried. Deletion does not cancel
+ * the subscription with the merchant. Recurring terms cannot be changed once the
+ * card is authorized.
+ */
+ recurring?: LinkCardVaultItemSpec.Recurring;
+
totals?: Array;
}
@@ -438,6 +452,26 @@ export namespace CardVaultItemSpec {
}
}
+ /**
+ * Recurring terms for a subscription or other repeated purchase. The amount
+ * becomes the spending limit for each period of interval_count intervals, for
+ * example every 2 weeks. The merchant schedules charges. The card stays ready
+ * after each charge until it expires or is deleted; do not create a new card each
+ * period. Deleting the card, its wallet or its vault first cancels the credential
+ * with Link, so later charges decline; if Link cannot be reached, deletion returns
+ * 409 provider_revocation_pending and should be retried. Deletion does not cancel
+ * the subscription with the merchant. Recurring terms cannot be changed once the
+ * card is authorized.
+ */
+ export interface Recurring {
+ interval: 'day' | 'week' | 'month' | 'year';
+
+ /**
+ * Number of intervals per period. Link defaults it to 1.
+ */
+ interval_count?: number;
+ }
+
export interface Total {
/**
* Total amount in minor currency units.
@@ -713,7 +747,7 @@ export namespace CredentialAccountVaultItem {
export interface AvailableExpansion {
description: string;
- type: 'payment_methods';
+ type: 'payment_methods' | 'link_user_info' | 'link_shipping_addresses';
}
/**
@@ -1088,7 +1122,7 @@ export namespace CredentialVaultItem {
export interface AvailableExpansion {
description: string;
- type: 'payment_methods';
+ type: 'payment_methods' | 'link_user_info' | 'link_shipping_addresses';
}
/**
@@ -1291,7 +1325,8 @@ export interface FillVaultItemOperationRequest {
page_url?: string;
/**
- * Total operation deadline in milliseconds, not a per-field timeout.
+ * @deprecated Ignored. Fill sets no operation deadline; typing pace adapts to the
+ * length of the values instead. Accepted for compatibility.
*/
timeout_ms?: number;
}
@@ -1327,8 +1362,11 @@ export interface KernelCardState {
provider: 'kernel';
/**
- * pending_authorization on a Visa purchase waits for the cardholder to approve it
- * through the spend_approval action; an unused link expires after 30 minutes.
+ * pending_authorization waits for cardholder approval on Visa purchases or fresh
+ * security-code collection on alias-mode purchases through the spend_approval
+ * action; an unused link expires after 30 minutes. Alias-mode cards become ready
+ * after collection and expire at the temporary security-code alias deadline, by
+ * default five minutes later. An expired alias purchase requires a new card.
* recovery_required means verification, approving the purchase or issuing the
* one-time code has an unresolved outcome. A server-classified initial attestation
* failure before intent dispatch may advertise restart_verification to prepare a
@@ -1386,12 +1424,13 @@ export namespace KernelCardState {
* spend_approval action: the cardholder approves it with a Visa passkey, and
* Kernel registers a Visa intent for one transaction up to the amount before
* issuing the code. For supported cards whose network cannot be enrolled for
- * agentic tokens, authorize prepares checkout details without issuing a one-time
- * code. The card can be filled into one checkout before its expires_at, one hour
- * after authorization. Fill types those details on merchant_url's origin. The
- * amount, merchant_url and merchant_country are recorded but do not restrict
- * payment requests sent by the browser. Card updates are not supported; delete and
- * create a new item instead.
+ * agentic tokens, authorize returns pending_authorization with a spend_approval
+ * action to collect a fresh security code in a hosted field. Once collected, the
+ * card is ready and can be filled before its expires_at, by default five minutes
+ * after collection. An expired card requires a new purchase. Fill types those
+ * details on merchant_url's origin. The amount, merchant_url and merchant_country
+ * are recorded but do not restrict payment requests sent by the browser. Card
+ * updates are not supported; delete and create a new item instead.
*/
export interface KernelCardVaultItemSpec {
/**
@@ -1527,6 +1566,196 @@ export interface KernelWalletVaultItemSpec {
provider: 'kernel';
}
+/**
+ * One entry of Link's `GET /shipping_addresses` response, passed through as Link
+ * reports it. Kernel never stores it.
+ */
+export interface LinkShippingAddress {
+ /**
+ * Link shipping address ID.
+ */
+ id: string;
+
+ is_default: boolean;
+
+ nickname: string | null;
+
+ /**
+ * Link's nullable postal address fields. Omitted from the shipping address when
+ * Link reports null.
+ */
+ address?: LinkShippingAddress.Address;
+}
+
+export namespace LinkShippingAddress {
+ /**
+ * Link's nullable postal address fields. Omitted from the shipping address when
+ * Link reports null.
+ */
+ export interface Address {
+ /**
+ * State, province or region.
+ */
+ administrative_area: string | null;
+
+ country_code: string | null;
+
+ dependent_locality: string | null;
+
+ line_1: string | null;
+
+ line_2: string | null;
+
+ /**
+ * City or town.
+ */
+ locality: string | null;
+
+ name: string | null;
+
+ postal_code: string | null;
+
+ sorting_code: string | null;
+ }
+}
+
+/**
+ * Link's `GET /userinfo` response for the connected wallet, passed through as Link
+ * reports it when the `link_user_info` expansion is requested. Kernel never stores
+ * it. Null fields are null at Link; omitted fields were not reported. Amounts are
+ * in the minor unit of `default_spend_request_currency`.
+ */
+export interface LinkUserInfo {
+ /**
+ * Stable Link user ID.
+ */
+ id: string | null;
+
+ email: string | null;
+
+ first_name: string | null;
+
+ last_name: string | null;
+
+ name: string | null;
+
+ phone: string | null;
+
+ /**
+ * The Link user's postal address. Omitted when Link reports none.
+ */
+ address?: LinkUserInfo.Address;
+
+ /**
+ * Link's agent wallet spend limits, per transaction and over rolling periods.
+ */
+ agent_wallet_spend_limits?: LinkUserInfo.AgentWalletSpendLimits;
+
+ /**
+ * Verification Link requires from the user before the agent wallet can spend.
+ */
+ agent_wallet_step_up?: LinkUserInfo.AgentWalletStepUp;
+
+ /**
+ * Lowercase ISO currency Link applies to a spend request created without one.
+ */
+ default_spend_request_currency?: string;
+
+ /**
+ * Whether the user can fund purchases from a Link balance.
+ */
+ eligible_for_balance?: boolean;
+}
+
+export namespace LinkUserInfo {
+ /**
+ * The Link user's postal address. Omitted when Link reports none.
+ */
+ export interface Address {
+ city: string | null;
+
+ country: string | null;
+
+ line1: string | null;
+
+ line2: string | null;
+
+ postal_code: string | null;
+
+ state: string | null;
+ }
+
+ /**
+ * Link's agent wallet spend limits, per transaction and over rolling periods.
+ */
+ export interface AgentWalletSpendLimits {
+ daily: AgentWalletSpendLimits.Daily;
+
+ per_transaction: AgentWalletSpendLimits.PerTransaction;
+
+ thirty_day: AgentWalletSpendLimits.ThirtyDay;
+ }
+
+ export namespace AgentWalletSpendLimits {
+ export interface Daily {
+ /**
+ * Null means unlimited.
+ */
+ limit: number | null;
+
+ /**
+ * Amount still available in the rolling period; null means unlimited.
+ */
+ remaining: number | null;
+
+ /**
+ * Amount already spent in the rolling period.
+ */
+ used: number;
+ }
+
+ export interface PerTransaction {
+ /**
+ * Null means unlimited.
+ */
+ limit: number | null;
+ }
+
+ export interface ThirtyDay {
+ /**
+ * Null means unlimited.
+ */
+ limit: number | null;
+
+ /**
+ * Amount still available in the rolling period; null means unlimited.
+ */
+ remaining: number | null;
+
+ /**
+ * Amount already spent in the rolling period.
+ */
+ used: number;
+ }
+ }
+
+ /**
+ * Verification Link requires from the user before the agent wallet can spend.
+ */
+ export interface AgentWalletStepUp {
+ /**
+ * URL where the user completes verification; null when no action is available.
+ */
+ action_url: string | null;
+
+ /**
+ * Link status such as not_required, ssn_verification, identity_verification,
+ * contact_support or complete.
+ */
+ status: string;
+ }
+}
+
export interface ManagedAuthCredentialVaultField {
/**
* Text, email, and password have form inputs; totp and passkey do not and are
@@ -2255,7 +2484,7 @@ export namespace VaultItem {
export interface AvailableExpansion {
description: string;
- type: 'payment_methods';
+ type: 'payment_methods' | 'link_user_info' | 'link_shipping_addresses';
}
/**
@@ -2284,6 +2513,20 @@ export namespace VaultItem {
* Live, non-persisted data requested through the item GET expand parameter.
*/
export interface Expanded {
+ /**
+ * Link's saved shipping addresses, present when the `link_shipping_addresses`
+ * expansion is requested.
+ */
+ link_shipping_addresses?: Array;
+
+ /**
+ * Link's `GET /userinfo` response for the connected wallet, passed through as Link
+ * reports it when the `link_user_info` expansion is requested. Kernel never stores
+ * it. Null fields are null at Link; omitted fields were not reported. Amounts are
+ * in the minor unit of `default_spend_request_currency`.
+ */
+ link_user_info?: ItemsAPI.LinkUserInfo;
+
payment_methods?: Array;
}
}
@@ -2335,7 +2578,7 @@ export namespace VaultItem {
export interface AvailableExpansion {
description: string;
- type: 'payment_methods';
+ type: 'payment_methods' | 'link_user_info' | 'link_shipping_addresses';
}
/**
@@ -2506,7 +2749,7 @@ export namespace VaultItemOperationResponse {
export interface AvailableExpansion {
description: string;
- type: 'payment_methods';
+ type: 'payment_methods' | 'link_user_info' | 'link_shipping_addresses';
}
/**
@@ -2535,6 +2778,20 @@ export namespace VaultItemOperationResponse {
* Live, non-persisted data requested through the item GET expand parameter.
*/
export interface Expanded {
+ /**
+ * Link's saved shipping addresses, present when the `link_shipping_addresses`
+ * expansion is requested.
+ */
+ link_shipping_addresses?: Array;
+
+ /**
+ * Link's `GET /userinfo` response for the connected wallet, passed through as Link
+ * reports it when the `link_user_info` expansion is requested. Kernel never stores
+ * it. Null fields are null at Link; omitted fields were not reported. Amounts are
+ * in the minor unit of `default_spend_request_currency`.
+ */
+ link_user_info?: ItemsAPI.LinkUserInfo;
+
payment_methods?: Array;
}
}
@@ -2586,7 +2843,7 @@ export namespace VaultItemOperationResponse {
export interface AvailableExpansion {
description: string;
- type: 'payment_methods';
+ type: 'payment_methods' | 'link_user_info' | 'link_shipping_addresses';
}
/**
@@ -2912,7 +3169,7 @@ export interface ItemRetrieveParams {
* Query param: Live fields advertised by `available_expansions` to include in
* `expanded`.
*/
- expand?: Array<'payment_methods'>;
+ expand?: Array<'payment_methods' | 'link_user_info' | 'link_shipping_addresses'>;
/**
* Query param: Hold for up to this many seconds while the item is pending
@@ -3172,7 +3429,8 @@ export declare namespace ItemPerformOperationParams {
page_url?: string;
/**
- * Body param: Total operation deadline in milliseconds, not a per-field timeout.
+ * @deprecated Body param: Ignored. Fill sets no operation deadline; typing pace
+ * adapts to the length of the values instead. Accepted for compatibility.
*/
timeout_ms?: number;
}
@@ -3624,6 +3882,8 @@ export declare namespace Items {
type KernelCredentialVaultItemState as KernelCredentialVaultItemState,
type KernelWalletState as KernelWalletState,
type KernelWalletVaultItemSpec as KernelWalletVaultItemSpec,
+ type LinkShippingAddress as LinkShippingAddress,
+ type LinkUserInfo as LinkUserInfo,
type ManagedAuthCredentialVaultField as ManagedAuthCredentialVaultField,
type ManagedAuthCredentialVaultItemSpec as ManagedAuthCredentialVaultItemSpec,
type ManagedAuthCredentialVaultItemSpecInput as ManagedAuthCredentialVaultItemSpecInput,
diff --git a/src/resources/vaults/vaults.ts b/src/resources/vaults/vaults.ts
index a2cc8814..f51a3e63 100644
--- a/src/resources/vaults/vaults.ts
+++ b/src/resources/vaults/vaults.ts
@@ -45,6 +45,8 @@ import {
KernelCredentialVaultItemState,
KernelWalletState,
KernelWalletVaultItemSpec,
+ LinkShippingAddress,
+ LinkUserInfo,
ManagedAuthCredentialVaultField,
ManagedAuthCredentialVaultItemSpec,
ManagedAuthCredentialVaultItemSpecInput,
@@ -299,6 +301,8 @@ export declare namespace Vaults {
type KernelCredentialVaultItemState as KernelCredentialVaultItemState,
type KernelWalletState as KernelWalletState,
type KernelWalletVaultItemSpec as KernelWalletVaultItemSpec,
+ type LinkShippingAddress as LinkShippingAddress,
+ type LinkUserInfo as LinkUserInfo,
type ManagedAuthCredentialVaultField as ManagedAuthCredentialVaultField,
type ManagedAuthCredentialVaultItemSpec as ManagedAuthCredentialVaultItemSpec,
type ManagedAuthCredentialVaultItemSpecInput as ManagedAuthCredentialVaultItemSpecInput,
diff --git a/src/version.ts b/src/version.ts
index 54ba18a0..bfde34bc 100644
--- a/src/version.ts
+++ b/src/version.ts
@@ -1 +1 @@
-export const VERSION = '0.129.0'; // x-release-please-version
+export const VERSION = '0.130.0'; // x-release-please-version
diff --git a/tests/api-resources/vaults/items.test.ts b/tests/api-resources/vaults/items.test.ts
index 8f128c86..f2381ea9 100644
--- a/tests/api-resources/vaults/items.test.ts
+++ b/tests/api-resources/vaults/items.test.ts
@@ -88,6 +88,7 @@ describe('resource items', () => {
},
],
metadata: { foo: 'string' },
+ recurring: { interval: 'day', interval_count: 1 },
totals: [
{
amount: 0,
diff --git a/tests/pagination.test.ts b/tests/pagination.test.ts
index 3e5fadee..33917873 100644
--- a/tests/pagination.test.ts
+++ b/tests/pagination.test.ts
@@ -46,8 +46,32 @@ describe('OffsetPagination', () => {
expect(page.hasNextPage()).toBe(false);
});
- test('stops on an empty page', () => {
+ test('continues past an empty page while X-Has-More is true', () => {
+ // A server-side filter can leave a page empty with more records ahead.
const page = pageWith({ 'x-next-offset': '300', 'x-has-more': 'true' }, [], 200);
+ expect(page.hasNextPage()).toBe(true);
+ expect(page.nextPageRequestOptions()?.query).toEqual({ offset: 300 });
+ });
+
+ test('stops on an empty page when X-Has-More is false', () => {
+ const page = pageWith({ 'x-next-offset': '0', 'x-has-more': 'false' }, [], 200);
+ expect(page.hasNextPage()).toBe(false);
+ });
+
+ test('stops when X-Next-Offset repeats the requested offset', () => {
+ const page = pageWith({ 'x-next-offset': '200', 'x-has-more': 'true' }, [], 200);
+ expect(page.nextPageRequestOptions()).toBeNull();
+ expect(page.hasNextPage()).toBe(false);
+ });
+
+ test('continues past an empty page with a descending cursor', () => {
+ const page = pageWith({ 'x-next-offset': '200', 'x-has-more': 'true' }, [], 300);
+ expect(page.hasNextPage()).toBe(true);
+ expect(page.nextPageRequestOptions()?.query).toEqual({ offset: 200 });
+ });
+
+ test('stops on an empty first page without pagination headers', () => {
+ const page = pageWith({}, []);
expect(page.hasNextPage()).toBe(false);
});