Skip to content

Commit e7cb96d

Browse files
feat: Add server-to-server card ingest for Kernel wallets
Stainless-Generated-From: b1128ae3997c8cc46f2bcd3d333a47324fa78cf7
1 parent d4cdb0a commit e7cb96d

6 files changed

Lines changed: 158 additions & 49 deletions

File tree

‎src/kernel/resources/vaults/items.py‎

Lines changed: 84 additions & 36 deletions
Large diffs are not rendered by default.

‎src/kernel/types/vaults/add_payment_method_vault_item_operation_request_param.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@
99

1010
class AddPaymentMethodVaultItemOperationRequestParam(TypedDict, total=False):
1111
"""
12-
Start hosted enrollment of a card in a connected Kernel wallet, including its first card, only when the wallet advertises add_payment_method. Returns a new payment method in pending_enrollment with a single-use card_enrollment URL for the cardholder. Card details go directly to the card vault and are never accepted by this API. Each call creates a separate link that expires after 30 minutes; a failed or expired enrollment does not affect the wallet's other payment methods. Scoped API keys require vaults:write in addition to vaults:use on this vault; otherwise the operation returns 403.
12+
Start enrollment of a card in a connected Kernel wallet, including its first card, only when the wallet advertises add_payment_method. Returns a new payment method in pending_enrollment with a single-use action. Card details go directly to the card vault and are never accepted by this API. With enrollment hosted (the default) the action is a card_enrollment URL for the cardholder, which expires after 30 minutes. With enrollment server the action is a card_ingest URL that the card's issuer posts the card to from its backend, which expires after 5 minutes; server enrollment returns 409 when it is not configured for the organization's card backend. A failed or expired enrollment does not affect the wallet's other payment methods. Scoped API keys require vaults:write in addition to vaults:use on this vault; otherwise the operation returns 403.
1313
"""
1414

1515
type: Required[Literal["add_payment_method"]]
16+
17+
enrollment: Literal["hosted", "server"]
18+
"""hosted returns a card_enrollment link for the cardholder.
19+
20+
server returns a card_ingest URL for the card's issuer to post the card to
21+
server to server.
22+
"""

‎src/kernel/types/vaults/add_payment_method_vault_item_operation_result.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
class AddPaymentMethodVaultItemOperationResult(BaseModel):
1212
"""The payment method created in pending_enrollment.
1313
14-
Deliver action.url to the cardholder; it is a single-use capability and must not be logged. Observe completion through the wallet's payment_methods expansion.
14+
Deliver a card_enrollment action.url to the cardholder; it is a single-use capability and must not be logged. A card_ingest action.url is posted to by the card's issuer. Observe completion through the wallet's payment_methods expansion.
1515
"""
1616

1717
payment_method: VaultPaymentMethod

‎src/kernel/types/vaults/item_perform_operation_params.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,13 @@ class AddPaymentMethodVaultItemOperationRequest(TypedDict, total=False):
314314

315315
type: Required[Literal["add_payment_method"]]
316316

317+
enrollment: Literal["hosted", "server"]
318+
"""hosted returns a card_enrollment link for the cardholder.
319+
320+
server returns a card_ingest URL for the card's issuer to post the card to
321+
server to server.
322+
"""
323+
317324

318325
class RemovePaymentMethodVaultItemOperationRequest(TypedDict, total=False):
319326
id_or_name: Required[str]

‎src/kernel/types/vaults/vault_payment_method.py‎

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,20 @@ class Display(BaseModel):
3333

3434

3535
class Action(BaseModel):
36-
name: Literal["card_enrollment"]
36+
"""The open enrollment of a pending payment method.
37+
38+
card_enrollment is a single-use hosted link for the cardholder. card_ingest is a single-use URL on a Kernel ingest hostname served by the card vault, which the card's issuer posts the card to from its backend; Kernel never receives the card number or CVC.
39+
"""
40+
41+
name: Literal["card_enrollment", "card_ingest"]
3742

3843
url: str
3944

4045
capture_token: Optional[str] = None
4146
"""Secret bearer capability for embedded card capture with @onkernel/vault-react.
4247
43-
Present only for Kernel wallets; never for AgentCard. Treat it like the
44-
enrollment URL and do not log or persist it in browser storage.
48+
Present only on card_enrollment for Kernel wallets; never for AgentCard. Treat
49+
it like the enrollment URL and do not log or persist it in browser storage.
4550
"""
4651

4752

@@ -71,20 +76,40 @@ class VaultPaymentMethod(BaseModel):
7176
"""Provider-neutral payment-method type normalized to lowercase."""
7277

7378
action: Optional[Action] = None
79+
"""The open enrollment of a pending payment method.
80+
81+
card_enrollment is a single-use hosted link for the cardholder. card_ingest is a
82+
single-use URL on a Kernel ingest hostname served by the card vault, which the
83+
card's issuer posts the card to from its backend; Kernel never receives the card
84+
number or CVC.
85+
"""
7486

7587
expires_at: Optional[datetime] = None
76-
"""Expiry of a pending enrollment link."""
88+
"""Expiry of a pending enrollment link or ingest URL."""
89+
90+
issuer_reference: Optional[str] = None
91+
"""Kernel wallets only.
92+
93+
The issuer's reference for a card it pushed with a card_ingest URL.
94+
"""
7795

7896
status: Optional[Literal["pending_enrollment", "active", "removal_pending", "recovery_required"]] = None
7997
"""Kernel wallets only.
8098
81-
pending_enrollment means a card_enrollment link is open; active means the card
82-
is stored and can pay when capabilities.single_use_card.eligible is true;
83-
removal_pending means removal started but the provider outcome is unknown, so
84-
retry remove_payment_method; recovery_required means either an enrollment
85-
outcome is unknown (remove the method), or the stored card cannot be read
86-
(contact support; removal is refused with 409). Consult status_reason to
87-
distinguish these cases. Absent for other providers.
99+
pending_enrollment means a card_enrollment link or card_ingest URL is open;
100+
active means the card is stored and can pay when
101+
capabilities.single_use_card.eligible is true; removal_pending means removal
102+
started but the provider outcome is unknown, so retry remove_payment_method;
103+
recovery_required means either an enrollment outcome is unknown (remove the
104+
method), or the stored card cannot be read (contact support; removal is refused
105+
with 409). Consult status_reason to distinguish these cases. Absent for other
106+
providers.
88107
"""
89108

90109
status_reason: Optional[str] = None
110+
111+
valid_until: Optional[datetime] = None
112+
"""Kernel wallets only.
113+
114+
When a card pushed with a card_ingest URL stops being usable, as its issuer set.
115+
"""

‎tests/api_resources/vaults/test_items.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1947,6 +1947,17 @@ def test_method_perform_operation_overload_17(self, client: Kernel) -> None:
19471947
)
19481948
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
19491949

1950+
@pytest.mark.skip(reason="Mock server tests are disabled")
1951+
@parametrize
1952+
def test_method_perform_operation_with_all_params_overload_17(self, client: Kernel) -> None:
1953+
item = client.vaults.items.perform_operation(
1954+
key="key",
1955+
id_or_name="id_or_name",
1956+
type="add_payment_method",
1957+
enrollment="hosted",
1958+
)
1959+
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
1960+
19501961
@pytest.mark.skip(reason="Mock server tests are disabled")
19511962
@parametrize
19521963
def test_raw_response_perform_operation_overload_17(self, client: Kernel) -> None:
@@ -4630,6 +4641,17 @@ async def test_method_perform_operation_overload_17(self, async_client: AsyncKer
46304641
)
46314642
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
46324643

4644+
@pytest.mark.skip(reason="Mock server tests are disabled")
4645+
@parametrize
4646+
async def test_method_perform_operation_with_all_params_overload_17(self, async_client: AsyncKernel) -> None:
4647+
item = await async_client.vaults.items.perform_operation(
4648+
key="key",
4649+
id_or_name="id_or_name",
4650+
type="add_payment_method",
4651+
enrollment="hosted",
4652+
)
4653+
assert_matches_type(VaultItemOperationResponse, item, path=["response"])
4654+
46334655
@pytest.mark.skip(reason="Mock server tests are disabled")
46344656
@parametrize
46354657
async def test_raw_response_perform_operation_overload_17(self, async_client: AsyncKernel) -> None:

0 commit comments

Comments
 (0)