Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ Key input field notes:
- `retrieve --interval <seconds>` waits for the initial status to change. Polling starts only for `created`, `pending_approval`, or `requires_action` with `auto_resume`; all other statuses (including `submitted` and unknown future values) return immediately. Any status change returns, even to another waiting state. JSON and interactive retrieve share `shouldPollSpendRequest`. If `--timeout` or `--max-attempts` is reached without a change, JSON polling exits non-zero with `POLLING_TIMEOUT`.
- Both `create` and `retrieve` (including approval polling) can return `status: 'requires_action'` with `status_details.requires_action.next_action` (`type`, `display_message`, `action_url`, `resolution`). With `resolution: 'auto_resume'` (currently only `next_action.type: 'three_d_secure'`), retrieve the same request again to wait for its status to change; interactive create resumes automatically. Any other resolution stops polling immediately; the caller must have the user complete the action, then create a new spend request.
- `cancel <id>` cancels a spend request. Can cancel from `created`, `pending_approval`, or `approved` states. Returns the spend request with `status: "canceled"`.
- `--approval-detail` — optional JSON object (MCP/agent) or JSON string (CLI) with approval details for delegated flows. Required fields: `approved_at` (unix timestamp int), `approval_method` (`click`|`programmatic`|`voice`), `app_name`, `external_user_id`. Optional: `ip_address`, `user_agent`, `device_type` (`mobile`|`web`), `agent_log_id`, `external_user_name`, `external_session_id`, `authentication_method` (`biometric_face`|`biometric_fingerprint`|`passkey`). Sent as `approval_details` in the API request body.
- `--approval-detail` — optional JSON object (MCP/agent) or JSON string (CLI) with approval details for delegated flows. Required fields: `approved_at` (unix timestamp int), `approval_method` (`click`|`programmatic`|`voice`), `app_name`, `external_user_id`. Optional: `ip_address`, `user_agent`, `device_type` (`mobile`|`web`), `agent_log_id`, `external_user_name`, `external_session_id`, `device_id`, `authentication_method` (`biometric_face`|`biometric_fingerprint`|`passkey`). Sent as `approval_details` in the API request body.
- `card` credentials include `billing_address` (name, line1, line2, city, state, postal_code, country) and `valid_until` (ISO date string — when the card expires/stops working)
- `--output-file <path>` on `retrieve` or `create` writes full card credentials to a local file (0600 permissions) and redacts card data in stdout. `--force` allows overwriting an existing file.
- `create` also accepts an undocumented `--expires-at <unix_seconds>` to override the default 12-hour spend request expiration (3 hours to 7 days in the future). It's deliberately excluded from `--schema`/`--llms-full` output and from README/SKILL.md: it's gated to an allow-list of OAuth clients server-side, and most callers get a 400 (`"expires_at is not supported for this client"`) if they try it — don't document or suggest it to general agents.
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/spend-request/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export const createOptions = z.object({
.union([z.string(), z.record(z.string(), z.unknown())])
.optional()
.describe(
'Approval details object (MCP/agent: pass as object; CLI: pass as JSON string). Required fields: approved_at (unix timestamp), approval_method (click|programmatic|voice), app_name, external_user_id. Optional: ip_address, user_agent, device_type (mobile|web), agent_log_id, external_user_name, external_session_id, authentication_method (biometric_face|biometric_fingerprint|passkey).',
'Approval details object (MCP/agent: pass as object; CLI: pass as JSON string). Required fields: approved_at (unix timestamp), approval_method (click|programmatic|voice), app_name, external_user_id. Optional: ip_address, user_agent, device_type (mobile|web), agent_log_id, external_user_name, external_session_id, device_id, authentication_method (biometric_face|biometric_fingerprint|passkey).',
),
metadata: z
.array(z.union([z.string(), z.record(z.string(), z.string())]))
Expand Down
22 changes: 22 additions & 0 deletions packages/sdk-go/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,28 @@ func TestCreateSpendRequestIdempotencyKeyEncoding(t *testing.T) {
}
}

func TestCreateSpendRequestApprovalDetailsDeviceIDEncoding(t *testing.T) {
deviceID := "device_123"
data, err := json.Marshal(CreateSpendRequestParams{
Context: "A sufficiently detailed context for testing approval detail encoding.",
ApprovalDetails: &ApprovalDetail{
ApprovedAt: 123,
ApprovalMethod: ApprovalMethodProgrammatic,
AppName: "Test app",
ExternalUserID: "user_123",
DeviceID: &deviceID,
},
})
assertNoError(t, err)
var fields map[string]json.RawMessage
assertNoError(t, json.Unmarshal(data, &fields))
var approvalDetails map[string]any
assertNoError(t, json.Unmarshal(fields["approval_details"], &approvalDetails))
if approvalDetails["device_id"] != deviceID {
t.Fatalf("got device_id %#v, want %q", approvalDetails["device_id"], deviceID)
}
}

func TestCreateSpendRequestIncompleteIdempotentRequestReturnsAPIError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) {
response.WriteHeader(http.StatusConflict)
Expand Down
1 change: 1 addition & 0 deletions packages/sdk-go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ type ApprovalDetail struct {
AgentLogID *string `json:"agent_log_id,omitempty"`
ExternalUserName *string `json:"external_user_name,omitempty"`
ExternalSessionID *string `json:"external_session_id,omitempty"`
DeviceID *string `json:"device_id,omitempty"`
AuthenticationMethod *AuthenticationMethod `json:"authentication_method,omitempty"`
}

Expand Down
1 change: 1 addition & 0 deletions packages/sdk-python/src/link/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class ApprovalDetail(LinkModel):
agent_log_id: str | None = None
external_user_name: str | None = None
external_session_id: str | None = None
device_id: str | None = None
authentication_method: AuthenticationMethod | str | None = None


Expand Down
1 change: 1 addition & 0 deletions packages/sdk-python/src/link/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class ApprovalDetailParams(TypedDict, total=False):
agent_log_id: str | None
external_user_name: str | None
external_session_id: str | None
device_id: str | None
authentication_method: AuthenticationMethod | None


Expand Down
1 change: 1 addition & 0 deletions packages/sdk-python/tests/test_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ async def test_create_all_fields_and_nested_omission(api: API) -> None:
"agent_log_id": "log",
"external_user_name": "name",
"external_session_id": "session",
"device_id": "device",
"authentication_method": "passkey",
},
}
Expand Down
3 changes: 2 additions & 1 deletion packages/sdk-python/tests/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ def valid(client: Client) -> None:
client.spend_requests.create(
context="Purchase", line_items=[{"name": "Item", "totals": []}],
approval_details={"approved_at": 1, "approval_method": "voice",
"app_name": "app", "external_user_id": "user"},
"app_name": "app", "external_user_id": "user",
"device_id": "device"},
)

async def valid_async(client: AsyncClient) -> None:
Expand Down
20 changes: 20 additions & 0 deletions packages/sdk/src/resources/__tests__/spend-request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,26 @@ describe('SpendRequestResource', () => {
expect(result).toEqual(spendRequestResponse);
});

it('includes device_id in approval details', async () => {
mockFetchResponse(200, spendRequestResponse);

await repo.create({
...validParams,
approval_details: {
approved_at: 123,
approval_method: 'programmatic',
app_name: 'Test app',
external_user_id: 'user_123',
device_id: 'device_123',
},
});

const [, opts] = mockFetch.mock.calls[0]!;
expect(JSON.parse(opts.body).approval_details.device_id).toBe(
'device_123',
);
});

it('accepts omitted optional fields and a null shared payment token', async () => {
mockFetchResponse(200, sparseSpendRequestResponse);

Expand Down
1 change: 1 addition & 0 deletions packages/sdk/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export interface ApprovalDetail {
agent_log_id?: string;
external_user_name?: string;
external_session_id?: string;
device_id?: string;
authentication_method?:
| 'biometric_face'
| 'biometric_fingerprint'
Expand Down
2 changes: 1 addition & 1 deletion skills/create-payment-credential/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ Recommend the user approves with the [Link app](https://link.com/download). Show

**Test mode:** Add `--test` to create testmode credentials instead of real ones. Useful for development and integration testing. Link Pay Token does not support test mode.

**Approval details:** For delegated/pre-approved flows, pass `--approval-detail` as a JSON object (MCP/agent) or JSON string (CLI). Required fields: `approved_at` (unix timestamp), `approval_method` (`click`|`programmatic`|`voice`), `app_name`, `external_user_id`. Optional: `ip_address`, `user_agent`, `device_type` (`mobile`|`web`), `agent_log_id`, `external_user_name`, `external_session_id`, `authentication_method` (`biometric_face`|`biometric_fingerprint`|`passkey`).
**Approval details:** For delegated/pre-approved flows, pass `--approval-detail` as a JSON object (MCP/agent) or JSON string (CLI). Required fields: `approved_at` (unix timestamp), `approval_method` (`click`|`programmatic`|`voice`), `app_name`, `external_user_id`. Optional: `ip_address`, `user_agent`, `device_type` (`mobile`|`web`), `agent_log_id`, `external_user_name`, `external_session_id`, `device_id`, `authentication_method` (`biometric_face`|`biometric_fingerprint`|`passkey`).

**Metadata:** Attach arbitrary string data with the repeatable `--metadata "key:value"` flag (CLI) or a `{ key: value }` object (MCP/agent). Max 50 keys, key ≤ 40 chars, value ≤ 500 chars. Example: `--metadata "order_id:ord_123" --metadata "team:growth"`.

Expand Down
Loading