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
5 changes: 5 additions & 0 deletions .changeset/shiny-clowns-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stripe/link-cli": minor
---

Support `metadata` in `spend-request create` command
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ CLI command is `spend-request` (user-facing). Implemented in `packages/cli/src/c
Key input field notes:
- CLI input uses `payment_method_id`; mapped to `payment_details` when calling the SDK
- `context` requires min 100 characters; `amount` is in cents with max 500000
- `--metadata` (create only) is a repeatable `key:value` flag (CLI) or a `{ key: value }` object (MCP/agent), merged into a single `metadata` string→string map. Max 50 keys, key ≤ 40 chars, value ≤ 500 chars. Reuses `parseKvString` from `line-item-parser.ts`.
- `--test` flag creates testmode credentials (real testmode SPT from test card data) instead of livemode ones
- `create --request-approval` and `request-approval` both show an approval URL in interactive mode and poll until approved/denied/expired/failed/canceled. In JSON mode (`--format json`), they return immediately with an `_next.command` for `spend-request retrieve`.
- `retrieve --interval <seconds>` polls until approved/denied/expired/succeeded/failed/canceled. If `--timeout` is reached or `--max-attempts` is exhausted while the request is still non-terminal, it exits non-zero with `POLLING_TIMEOUT`.
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,18 @@ link-cli spend-request create ... \

In MCP/agent mode, pass as a structured object.

#### Metadata

Attach arbitrary string data to a spend request with the repeatable `--metadata` flag (`key:value` format). Max 50 keys, key ≤ 40 chars, value ≤ 500 chars.

```bash
link-cli spend-request create ... \
--metadata "order_id:ord_123" \
--metadata "team:growth"
```

In MCP/agent mode, pass `metadata` as a structured `{ key: value }` object.

#### Credential types

By default, a spend request provisions a virtual card. For merchants that support the [Machine Payments Protocol](https://mpp.dev) (HTTP 402) and the Stripe payment method, instead pass `--credential-type "shared_payment_token"`.
Expand Down
32 changes: 32 additions & 0 deletions packages/cli/src/__tests__/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,38 @@ describe('production mode', () => {
expect(request.network_id).toBe('net_prod_abc');
});

it('merges repeatable --metadata flags into a metadata object in POST body', async () => {
setNextResponse(200, BASE_REQUEST);

const result = await runProdCli(
'spend-request',
'create',
'--payment-method-id',
'pd_prod_test',
'--merchant-name',
'Test Merchant',
'--merchant-url',
'https://example.com',
'--context',
VALID_CONTEXT,
'--amount',
'5000',
'--metadata',
'order_id:ord_123',
'--metadata',
'team:growth',
'--no-request-approval',
'--json',
);

expect(result.exitCode).toBe(0);
const sentBody = JSON.parse(lastRequest.body);
expect(sentBody.metadata).toEqual({
order_id: 'ord_123',
team: 'growth',
});
});

it('sends test flag in POST body when --test is used', async () => {
setNextResponse(200, BASE_REQUEST);

Expand Down
17 changes: 17 additions & 0 deletions packages/cli/src/commands/spend-request/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Cli, z } from 'incur';
import React from 'react';
import { writeCredentialFile } from '../../utils/credential-output';
import {
parseKvString,
parseLineItemFlag,
parseTotalFlag,
} from '../../utils/line-item-parser';
Expand Down Expand Up @@ -154,6 +155,21 @@ export function createSpendRequestCli(
: opts.approvalDetail
: undefined;

// Merge repeatable metadata flags into a single flat object: strings from
// flags are parsed as key:value, objects from MCP pass through.
let metadata: Record<string, string> | undefined;
if (opts.metadata?.length) {
metadata = {};
for (const item of opts.metadata as unknown[]) {
Object.assign(
metadata,
typeof item === 'string'
? parseKvString(item)
: (item as Record<string, string>),
);
}
}

const createParams = {
payment_details: opts.paymentMethodId,
credential_type: credentialType,
Expand All @@ -169,6 +185,7 @@ export function createSpendRequestCli(
test: opts.test ? true : undefined,
approve: opts.approve ? true : undefined,
approval_details: approvalDetails,
metadata,
};

const outputFile = opts.outputFile;
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/commands/spend-request/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ export const createOptions = z.object({
.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).',
),
metadata: z
.array(z.union([z.string(), z.record(z.string(), z.string())]))
.default([])
.describe(
'Metadata key:value pair (repeatable). Attaches arbitrary string data to the spend request. Max 50 keys, key <= 40 chars, value <= 500 chars. Example: "order_id:ord_123"',
),
});

export const listOptions = z.object({
Expand Down
17 changes: 17 additions & 0 deletions packages/sdk/src/resources/__tests__/spend-request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,23 @@ describe('SpendRequestResource', () => {
expect(result.network_id).toBe('net_abc');
});

it('serializes metadata in POST body', async () => {
const paramsWithMetadata: CreateSpendRequestParams = {
...validParams,
metadata: { order_id: 'ord_123', team: 'growth' },
};
mockFetchResponse(200, spendRequestResponse);

await repo.createSpendRequest(paramsWithMetadata);

const [, opts] = mockFetch.mock.calls[0];
const sentBody = JSON.parse(opts.body);
expect(sentBody.metadata).toEqual({
order_id: 'ord_123',
team: 'growth',
});
});

it('serializes test flag in POST body when true', async () => {
const paramsWithTest: CreateSpendRequestParams = {
...validParams,
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/src/resources/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export interface CreateSpendRequestParams {
test?: boolean;
approve?: boolean;
approval_details?: ApprovalDetail;
metadata?: Record<string, string>;
}

export interface UpdateSpendRequestParams {
Expand Down
2 changes: 1 addition & 1 deletion plugins/link/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "link",
"version": "0.9.0",
"version": "0.10.1",
"description": "Authenticate with Link, create spend requests, and retrieve one-time-use card or shared payment token credentials for user-approved purchases.",
"author": {
"name": "Stripe"
Expand Down
2 changes: 1 addition & 1 deletion plugins/link/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "link",
"version": "0.9.0",
"version": "0.10.1",
"description": "Secure, one-time-use payment credentials from Link",
"author": {
"name": "Stripe",
Expand Down
2 changes: 1 addition & 1 deletion plugins/link/.cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "link",
"displayName": "Stripe Link",
"version": "0.9.0",
"version": "0.10.1",
"description": "Get secure, one-time-use payment credentials from a Link wallet so agents can complete purchases on your behalf.",
"author": {
"name": "Stripe"
Expand Down
4 changes: 3 additions & 1 deletion skills/create-payment-credential/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
version: 0.9.0
version: 0.10.1
name: create-payment-credential
description: |
Gets secure, one-time-use payment credentials (cards, tokens) from a Link wallet so agents can complete purchases on behalf of users. Use when the user says "get me a card", "buy something", "pay for X", "make a purchase", "I need to pay", "complete checkout", or asks to transact on any merchant site. Use when the user asks to connect or log in to or sign up for their Link account.
Expand Down Expand Up @@ -173,6 +173,8 @@ Recommend the user approves with the [Link app](https://link.com/download). Show

**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`).

**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"`.

### Step 5: Complete payment

**Card:** Run `link-cli spend-request retrieve <id> --include card` to get the `card` object with `number`, `cvc`, `exp_month`, `exp_year`, `billing_address` (name, line1, line2, city, state, postal_code, country), and `valid_until` (Unix timestamp — the card stops working after this time). Enter these details into the merchant's checkout form.
Expand Down
Loading