Claude Code Plugins

Community-maintained marketplace

Feedback

stripe-terminal-issuing

@zacharyr0th/next-starter
1
0

Use when implementing in-person payments with Terminal or issuing cards with Stripe Issuing. Invoke for POS terminal setup, card reader integration, in-person payment processing, card issuance, authorization controls, or physical/virtual card management.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name stripe-terminal-issuing
description Use when implementing in-person payments with Terminal or issuing cards with Stripe Issuing. Invoke for POS terminal setup, card reader integration, in-person payment processing, card issuance, authorization controls, or physical/virtual card management.
allowed-tools Read, Grep, Glob

Stripe Terminal & Issuing Expert

Purpose

Expert knowledge of Stripe Terminal for in-person payments and Stripe Issuing for card programs. Covers POS reader setup, Terminal SDK integration, card issuance, authorization controls, and card lifecycle management.

When to Use

Invoke this skill when:

  • Implementing in-person payments
  • Setting up card readers (Terminal)
  • Processing payments at physical locations
  • Creating card programs (Issuing)
  • Issuing physical or virtual cards
  • Setting up spending controls
  • Managing card authorizations
  • Handling card disputes
  • Debugging Terminal or Issuing issues

Documentation Available

Location: /Users/zach/Documents/cc-skills/docs/stripe/

Coverage (~149 files in terminal/ + issuing/):

  • Terminal (~75 files):

    • Reader management (Stripe Reader M2, BBPOS, Verifone)
    • Connection tokens
    • Payment collection
    • Refunds and cancellations
    • Terminal SDK (JS, iOS, Android)
    • Reader locations
  • Issuing (~74 files):

    • Card creation (physical, virtual)
    • Cardholder management
    • Authorization controls
    • Spending limits
    • Authorization holds
    • Card disputes
    • Transaction management

Related Skills

  • stripe-payments: For online payment flows
  • stripe-api-integration: For webhooks and API patterns

Best Practices

Terminal

  • Test with simulated readers - Use test mode readers
  • Handle connection states - Manage reader connections properly
  • Implement offline mode - Store payments when offline
  • Update reader software - Keep firmware updated
  • Handle timeouts - Set appropriate payment timeouts

Issuing

  • Set authorization controls - Prevent fraud
  • Monitor spending - Track card usage
  • Handle real-time authorizations - Approve/decline instantly
  • Implement 3DS - For online card use
  • Manage card lifecycle - Issue, activate, cancel properly

Examples

Example 1: Terminal Reader Setup

Question: "How do I set up Stripe Terminal?"

Response:

import { loadStripeTerminal } from '@stripe/terminal-js';

// Backend: Create connection token
export async function createConnectionToken(req, res) {
  try {
    const terminal = require('stripe')(process.env.STRIPE_SECRET_KEY!).terminal;

    const connectionToken = await terminal.connectionTokens.create();

    res.json({ secret: connectionToken.secret });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// Frontend: Initialize Terminal SDK
async function initializeTerminal() {
  const StripeTerminal = await loadStripeTerminal();

  const terminal = StripeTerminal.create({
    onFetchConnectionToken: async () => {
      const response = await fetch('/api/connection-token', {
        method: 'POST',
      }).then(r => r.json());

      return response.secret;
    },
    onUnexpectedReaderDisconnect: () => {
      console.log('Reader disconnected unexpectedly');
    },
  });

  return terminal;
}

// Discover readers
async function discoverReaders(terminal) {
  const discoverResult = await terminal.discoverReaders({
    simulated: false, // Set to true for testing
    location: 'tml_xxxxx', // Optional: filter by location
  });

  if (discoverResult.error) {
    console.error('Discovery failed:', discoverResult.error);
  } else {
    console.log('Discovered readers:', discoverResult.discoveredReaders);
    return discoverResult.discoveredReaders;
  }
}

// Connect to reader
async function connectToReader(terminal, reader) {
  const connectResult = await terminal.connectReader(reader);

  if (connectResult.error) {
    console.error('Connect failed:', connectResult.error);
  } else {
    console.log('Connected to reader:', connectResult.reader);
    return connectResult.reader;
  }
}

// Complete Terminal setup
async function setupTerminal() {
  const terminal = await initializeTerminal();
  const readers = await discoverReaders(terminal);

  if (readers && readers.length > 0) {
    await connectToReader(terminal, readers[0]);
  }

  return terminal;
}

References:

  • See: docs/stripe/terminal/

Example 2: Collect Payment with Terminal

Question: "How do I collect an in-person payment?"

Response:

// Backend: Create PaymentIntent for Terminal
export async function createTerminalPaymentIntent(req, res) {
  try {
    const { amount } = req.body;

    const paymentIntent = await stripe.paymentIntents.create({
      amount: amount * 100,
      currency: 'usd',
      payment_method_types: ['card_present'],
      capture_method: 'automatic', // or 'manual' for auth only
      metadata: {
        orderId: 'order_123',
      },
    });

    res.json({ clientSecret: paymentIntent.client_secret });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// Frontend: Collect payment
async function collectPayment(terminal: any, amount: number) {
  try {
    // Create PaymentIntent
    const response = await fetch('/api/create-terminal-payment-intent', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ amount }),
    }).then(r => r.json());

    // Collect payment method
    const collectResult = await terminal.collectPaymentMethod(
      response.clientSecret
    );

    if (collectResult.error) {
      throw new Error(collectResult.error.message);
    }

    console.log('Payment method collected:', collectResult.paymentIntent);

    // Process payment
    const processResult = await terminal.processPayment(
      collectResult.paymentIntent
    );

    if (processResult.error) {
      throw new Error(processResult.error.message);
    }

    console.log('Payment successful:', processResult.paymentIntent);

    // Optionally confirm on backend
    await fetch('/api/capture-payment-intent', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        paymentIntentId: processResult.paymentIntent.id,
      }),
    });

    return processResult.paymentIntent;
  } catch (error) {
    console.error('Payment failed:', error);
    throw error;
  }
}

// Cancel payment collection
async function cancelPayment(terminal: any) {
  const cancelResult = await terminal.cancelCollectPaymentMethod();

  if (cancelResult.error) {
    console.error('Cancel failed:', cancelResult.error);
  } else {
    console.log('Payment collection canceled');
  }
}

// Handle reader events
terminal.on('connection_status', (status) => {
  console.log('Connection status:', status);
});

terminal.on('payment_status', (status) => {
  console.log('Payment status:', status);
});

References:

  • See: docs/stripe/terminal/payments/

Example 3: Create Location and Register Reader

Question: "How do I register a physical reader?"

Response:

// Create location
export async function createLocation(req, res) {
  try {
    const { displayName, address } = req.body;

    const location = await stripe.terminal.locations.create({
      display_name: displayName,
      address: {
        line1: address.line1,
        city: address.city,
        state: address.state,
        postal_code: address.postalCode,
        country: address.country,
      },
    });

    res.json({ location });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// List locations
export async function listLocations(req, res) {
  try {
    const locations = await stripe.terminal.locations.list({
      limit: 10,
    });

    res.json({ locations: locations.data });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// Register reader (done via Stripe Dashboard or mobile app)
// Readers are automatically registered when first connected

// List readers
export async function listReaders(req, res) {
  try {
    const { locationId } = req.query;

    const readers = await stripe.terminal.readers.list({
      location: locationId as string,
      limit: 10,
    });

    res.json({ readers: readers.data });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// Update reader
export async function updateReader(req, res) {
  try {
    const { readerId, label, metadata } = req.body;

    const reader = await stripe.terminal.readers.update(readerId, {
      label,
      metadata,
    });

    res.json({ reader });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// Delete reader
export async function deleteReader(req, res) {
  try {
    const { readerId } = req.body;

    const deleted = await stripe.terminal.readers.del(readerId);

    res.json({ deleted });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

References:

  • See: docs/stripe/terminal/locations/
  • See: docs/stripe/terminal/readers/

Example 4: Issue Cards

Question: "How do I issue cards with Stripe Issuing?"

Response:

// Create cardholder
export async function createCardholder(req, res) {
  try {
    const { name, email, phone, address } = req.body;

    const cardholder = await stripe.issuing.cardholders.create({
      name,
      email,
      phone_number: phone,
      billing: {
        address: {
          line1: address.line1,
          city: address.city,
          state: address.state,
          postal_code: address.postalCode,
          country: address.country,
        },
      },
      type: 'individual', // or 'company'
      metadata: {
        userId: req.user.id,
      },
    });

    res.json({ cardholder });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// Issue virtual card
export async function issueVirtualCard(req, res) {
  try {
    const { cardholderId } = req.body;

    const card = await stripe.issuing.cards.create({
      cardholder: cardholderId,
      currency: 'usd',
      type: 'virtual',
      status: 'active',
      spending_controls: {
        spending_limits: [
          {
            amount: 50000, // $500 per month
            interval: 'per_month',
          },
        ],
        allowed_categories: ['restaurants', 'gas_stations'],
      },
      metadata: {
        purpose: 'Employee expenses',
      },
    });

    res.json({ card });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// Issue physical card
export async function issuePhysicalCard(req, res) {
  try {
    const { cardholderId, shippingAddress } = req.body;

    const card = await stripe.issuing.cards.create({
      cardholder: cardholderId,
      currency: 'usd',
      type: 'physical',
      status: 'inactive', // Activate after cardholder receives it
      shipping: {
        name: 'John Doe',
        address: {
          line1: shippingAddress.line1,
          city: shippingAddress.city,
          state: shippingAddress.state,
          postal_code: shippingAddress.postalCode,
          country: shippingAddress.country,
        },
        service: 'standard', // or 'express', 'priority'
      },
    });

    res.json({ card });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// Get card details (including sensitive data)
export async function getCardDetails(req, res) {
  try {
    const { cardId } = req.query;

    const card = await stripe.issuing.cards.retrieve(cardId as string, {
      expand: ['number', 'cvc'], // Get full card number and CVC
    });

    res.json({
      number: card.number,
      exp_month: card.exp_month,
      exp_year: card.exp_year,
      cvc: card.cvc,
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// Update card status
export async function updateCardStatus(req, res) {
  try {
    const { cardId, status } = req.body;
    // status: 'active', 'inactive', 'canceled'

    const card = await stripe.issuing.cards.update(cardId, {
      status,
    });

    res.json({ card });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

Card Types:

  • Virtual: Instant issuance, online use
  • Physical: 3-7 day delivery, in-person use

References:

  • See: docs/stripe/issuing/cards/

Example 5: Authorization Controls

Question: "How do I control card authorizations?"

Response:

// Set spending controls on card
export async function setSpendingControls(req, res) {
  try {
    const { cardId, limits } = req.body;

    const card = await stripe.issuing.cards.update(cardId, {
      spending_controls: {
        spending_limits: [
          {
            amount: 100000, // $1000
            interval: 'per_month',
            categories: ['restaurants'],
          },
          {
            amount: 5000, // $50
            interval: 'per_transaction',
          },
        ],
        allowed_categories: [
          'restaurants',
          'gas_stations',
          'grocery_stores',
        ],
        blocked_categories: [
          'gambling',
          'adult_digital_content',
        ],
      },
    });

    res.json({ card });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// Real-time authorization webhook
export async function handleAuthorizationWebhook(req, res) {
  const event = req.body;

  if (event.type === 'issuing_authorization.request') {
    const authorization = event.data.object;

    // Custom authorization logic
    const shouldApprove = await checkAuthorizationRules(authorization);

    if (shouldApprove) {
      // Approve authorization
      await stripe.issuing.authorizations.approve(authorization.id);
    } else {
      // Decline authorization
      await stripe.issuing.authorizations.decline(authorization.id, {
        reason: 'insufficient_funds', // or 'spending_controls'
      });
    }
  }

  res.json({ received: true });
}

async function checkAuthorizationRules(authorization: any): Promise<boolean> {
  // Custom business logic
  const cardholder = await db.cardholders.findUnique({
    where: { stripeId: authorization.cardholder },
  });

  // Check balance
  if (cardholder.balance < authorization.amount) {
    return false;
  }

  // Check merchant category
  if (BLOCKED_MCCS.includes(authorization.merchant_data.category)) {
    return false;
  }

  // Check time of day
  const hour = new Date().getHours();
  if (hour < 6 || hour > 22) {
    return false; // Block late-night transactions
  }

  return true;
}

// List authorizations
export async function listAuthorizations(req, res) {
  try {
    const { cardId } = req.query;

    const authorizations = await stripe.issuing.authorizations.list({
      card: cardId as string,
      limit: 10,
    });

    res.json({ authorizations: authorizations.data });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

// Capture authorization
export async function captureAuthorization(req, res) {
  try {
    const { authorizationId, captureAmount } = req.body;

    const authorization = await stripe.issuing.authorizations.update(
      authorizationId,
      {
        metadata: { capture_amount: captureAmount },
      }
    );

    res.json({ authorization });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
}

Authorization States:

  • pending: Awaiting approval
  • approved: Approved
  • declined: Declined

References:

  • See: docs/stripe/issuing/controls/

Common Patterns

Terminal Payment Flow

// 1. Create PaymentIntent (backend)
// 2. Collect payment method (Terminal SDK)
// 3. Process payment (Terminal SDK)
// 4. Confirm payment (webhook)

Card Lifecycle

// 1. Create cardholder
// 2. Issue card (virtual or physical)
// 3. Activate card (physical only)
// 4. Monitor transactions
// 5. Update/cancel card as needed

Real-time Authorization

// Webhook: issuing_authorization.request
// → Check rules
// → Approve or decline within 5 seconds

Search Helpers

# Find Terminal docs
grep -r "Terminal\|reader\|in-person" /Users/zach/Documents/cc-skills/docs/stripe/terminal/

# Find Issuing docs
grep -r "Issuing\|card\|authorization" /Users/zach/Documents/cc-skills/docs/stripe/issuing/

# List Terminal files
ls /Users/zach/Documents/cc-skills/docs/stripe/terminal/

# List Issuing files
ls /Users/zach/Documents/cc-skills/docs/stripe/issuing/

Common Errors

Terminal

  • Reader not found: Reader not registered or offline

    • Solution: Check reader power and connection
  • Connection token expired: Token older than 60 seconds

    • Solution: Fetch new connection token
  • Payment timeout: Reader timeout waiting for card

    • Solution: Set appropriate timeout or cancel

Issuing

  • Cardholder required: Missing cardholder

    • Solution: Create cardholder first
  • Spending limit exceeded: Transaction over limit

    • Solution: Adjust spending controls
  • Authorization declined: Custom logic declined

    • Solution: Check authorization rules

Security Notes

Terminal

  • Secure connection tokens - Use HTTPS
  • Validate reader ID - Ensure reader belongs to your account
  • Monitor offline mode - Reconcile offline payments

Issuing

  • Protect card details - Never log full card numbers
  • Implement fraud detection - Monitor unusual patterns
  • Use real-time authorizations - Control spending instantly
  • Secure cardholder data - Follow PCI compliance

Notes

  • Documentation covers latest Stripe API (2023+)
  • Terminal requires physical hardware (readers)
  • Issuing requires approval from Stripe
  • Real-time authorization webhook must respond within 5 seconds
  • Virtual cards are instant, physical cards take 3-7 days
  • File paths reference local documentation cache
  • For latest updates, check: