Hands In
Integration

Hands In Connector API Framework

Get Markdown

This guide walks you through building a payment connector that integrates with the Hands In platform. By the end, you'll have a fully functional connector that can process payments through a unified transaction-based lifecycle.

What is a Connector?

A connector is a service that bridges payment providers with the Hands In platform. It translates between Hands In's standardized API and your payment provider's specific API format, supporting the complete payment lifecycle from initialization to completion.

Why Connectors?

  • Standardization: All payment providers use the same interface with Hands In
  • Unified Lifecycle: Single transaction ID flows through all operations
  • Reliability: Built-in status polling ensures no missed payments
  • Security: Centralized authentication and credential management
  • Flexibility: Support any payment provider, regardless of their API design

New Transaction-Based Architecture

The updated connector framework uses a unified transaction lifecycle where a single transaction_id flows through all operations:

Core Endpoints

Every connector must implement these essential endpoints:

1. /info - Connector Information

Purpose: Tells Hands In about your connector's capabilities and configuration

GET /info
// Returns connector metadata, supported features, and polling configuration

2. /initialize - Initialize Transaction

Purpose: Creates a new transaction with metadata and returns a transaction ID

POST /initialize
// Creates a transaction context and returns transaction_id for subsequent operations

3. /authenticate - Authenticate Payment Method

Purpose: Performs 3D Secure, SCA, or other authentication challenges

POST /authenticate
// Authenticates the payment method (3DS, SCA, etc.)

4. /authorize - Authorize Payment

Purpose: Creates an authorization (hold) on the customer's payment method

POST /authorize
// Authorizes funds using transaction_id from authenticate

5. /capture - Capture Funds

Purpose: Captures (charges) a previously authorized payment

POST /capture
// Captures funds using transaction_id from authorize

6. /void - Cancel Authorization

Purpose: Cancels a previously created authorization

POST /void
// Voids authorization using transaction_id

7. /refund - Process Refunds

Purpose: Creates refunds for completed payments

POST /refund
// Creates a refund using transaction_id

8. /status/{transactionId} - Check Transaction Status

Purpose: Returns comprehensive status for any transaction (replaces individual status endpoints)

GET /status/txn_1234567890
// Returns unified status with captured, authorized, and refunded amounts

How Status Synchronization Works

Hands In uses smart polling with the unified status endpoint to stay synchronized:

Smart Polling Schedule:

  • Initial: Every 2 seconds (for quick payments)
  • Backoff: Every 5 seconds, then 30 seconds
  • Stop: When status reaches "succeeded", "failed", or "cancelled"
  • Timeout: After 5 minutes maximum

Step-by-Step Implementation Guide

Step 1: Set Up Your Project

First, choose your technology stack and create the basic structure:

# Using Node.js/TypeScript (recommended)
npx create-express-app my-connector --typescript
cd my-connector

# Install dependencies
npm install express cors helmet dotenv
npm install -D @types/express @types/node typescript ts-node

Project Structure:

Step 2: Implement Connector Information

Create your connector's information endpoint:

// src/routes/connector.ts
import { Router } from 'express';

const router = Router();

router.get('/info', (req, res) => {
  res.json({
    name: "MyProvider Connector",
    description: "Payment processing connector for MyProvider",
    icon: "https://myprovider.com/logo.png",
    website: "https://myprovider.com",
    
    // Required endpoints with new unified structure
    endpoints: {
      initialize: "/initialize",
      authenticate: "/authenticate", 
      authorize: "/authorize",
      void: "/void",
      capture: "/capture",
      refund: "/refund",
      status: "/status/{transactionId}",
      hooks: "/hooks"
    },
    
    // Authentication configuration
    authConfig: {
      type: "api_key",
      fields: [
        {
          key: "api_key",
          label: "API Key",
          type: "password",
          required: true,
          helpText: "Your API key from MyProvider dashboard"
        }
      ]
    },
    
    // Polling configuration
    polling: {
      enabled: true,
      intervals: {
        initial: 2,    // 2 seconds initially
        backoff: 5,    // then 5 seconds
        maximum: 30,   // max 30 seconds
        timeout: 300   // stop after 5 minutes
      },
      terminal_statuses: ["succeeded", "failed", "cancelled"]
    },
    
    // Supported features
    supports: {
      currencies: ["USD", "EUR", "GBP"],
      countries: ["US", "CA", "GB"],
      cards: ["visa", "mastercard", "amex"],
      refunds: true,
      partial_refunds: true,
      fraud_detection: false
    },
    
    version: "1.0.0"
  });
});

export default router;

Step 3: Implement Authentication

Create authentication middleware to handle credentials:

// src/utils/auth.ts
export interface Credentials {
  api_key: string;
  // Add other fields as needed
}

export function extractAuth(req: any, res: any, next: any) {
  try {
    // Hands In sends credentials in request body
    const credentials: Credentials = req.body.credentials;
    
    if (!credentials || !credentials.api_key) {
      return res.status(401).json({
        error: {
          type: "authentication_error",
          code: "missing_credentials",
          message: "API key is required"
        }
      });
    }
    
    // Attach credentials to request for use in handlers
    req.credentials = credentials;
    next();
  } catch (error) {
    res.status(401).json({
      error: {
        type: "authentication_error",
        code: "invalid_credentials",
        message: "Invalid authentication format"
      }
    });
  }
}

Step 4: Implement Transaction Lifecycle

4.1 Initialize Transaction

// src/routes/connector.ts (continued)
router.post('/initialize', extractAuth, async (req, res) => {
  try {
    const { amount, payment_method, metadata } = req.body;
    const credentials = req.credentials;
    
    // Create transaction context with your provider
    const result = await paymentService.initializeTransaction({
      amount: amount.amount,
      currency: amount.currency,
      payment_method,
      metadata,
      api_key: credentials.api_key
    });
    
    // Return standardized response with transaction_id
    res.status(201).json({
      transaction_id: result.transaction_id, // This will be used in all subsequent operations
      status: mapStatus(result.status),
      amount: amount,
      created_at: new Date().toISOString(),
      gateway_reference: result.provider_id,
      metadata: metadata
    });
    
  } catch (error) {
    handleError(res, error);
  }
});

4.2 Authenticate Payment Method

// src/routes/connector.ts (continued)
router.post('/authenticate', extractAuth, async (req, res) => {
  try {
    const { amount, payment_method, metadata } = req.body;
    const credentials = req.credentials;
    
    // Perform 3D Secure or SCA authentication
    const result = await paymentService.authenticatePayment({
      amount: amount.amount,
      currency: amount.currency,
      payment_method,
      api_key: credentials.api_key
    });
    
    res.status(201).json({
      id: result.transaction_id,
      status: mapStatus(result.status),
      amount: amount,
      created_at: new Date().toISOString(),
      gateway_reference: result.provider_id,
      authentication_method: result.auth_method,
      liability_shift: result.liability_shift
    });
    
  } catch (error) {
    handleError(res, error);
  }
});

4.3 Authorize Payment

// src/routes/connector.ts (continued)
router.post('/authorize', extractAuth, async (req, res) => {
  try {
    const { transaction_id, amount, metadata } = req.body;
    const credentials = req.credentials;
    
    // Authorize using the transaction_id from previous step
    const result = await paymentService.authorizePayment({
      transaction_id,
      amount: amount?.amount,
      currency: amount?.currency,
      api_key: credentials.api_key
    });
    
    res.status(201).json({
      id: result.authorization_id,
      status: mapStatus(result.status),
      amount: result.amount,
      created_at: new Date().toISOString(),
      original_transaction_id: transaction_id,
      gateway_reference: result.provider_id,
      expires_at: result.expires_at
    });
    
  } catch (error) {
    handleError(res, error);
  }
});

4.4 Capture Funds

// src/routes/connector.ts (continued)
router.post('/capture', extractAuth, async (req, res) => {
  try {
    const { transaction_id, amount, metadata } = req.body;
    const credentials = req.credentials;
    
    // Capture using the transaction_id
    const result = await paymentService.capturePayment({
      transaction_id,
      amount: amount?.amount, // Optional for partial capture
      currency: amount?.currency,
      api_key: credentials.api_key
    });
    
    res.status(201).json({
      id: result.capture_id,
      status: mapStatus(result.status),
      amount: result.amount,
      created_at: new Date().toISOString(),
      original_transaction_id: transaction_id,
      gateway_reference: result.provider_id
    });
    
  } catch (error) {
    handleError(res, error);
  }
});

4.5 Void Authorization

// src/routes/connector.ts (continued)
router.post('/void', extractAuth, async (req, res) => {
  try {
    const { transaction_id, reason, metadata } = req.body;
    const credentials = req.credentials;
    
    // Void the authorization
    const result = await paymentService.voidPayment({
      transaction_id,
      reason,
      api_key: credentials.api_key
    });
    
    res.status(201).json({
      id: result.void_id,
      status: mapStatus(result.status),
      created_at: new Date().toISOString(),
      original_transaction_id: transaction_id,
      gateway_reference: result.provider_id,
      reason: reason
    });
    
  } catch (error) {
    handleError(res, error);
  }
});

4.6 Process Refunds

// src/routes/connector.ts (continued)
router.post('/refund', extractAuth, async (req, res) => {
  try {
    const { transaction_id, amount, reason } = req.body;
    const credentials = req.credentials;
    
    // Create refund using transaction_id
    const result = await paymentService.refundPayment({
      transaction_id,
      amount: amount?.amount, // Optional for partial refund
      currency: amount?.currency,
      reason,
      api_key: credentials.api_key
    });
    
    res.status(201).json({
      id: result.refund_id,
      status: mapStatus(result.status),
      amount: result.amount,
      created_at: new Date().toISOString(),
      original_transaction_id: transaction_id,
      gateway_reference: result.provider_id
    });
    
  } catch (error) {
    handleError(res, error);
  }
});

Step 5: Implement Unified Status Checking

The most important endpoint - provides comprehensive status for any transaction:

// src/routes/connector.ts (continued)
router.get('/status/:transactionId', extractAuth, async (req, res) => {
  try {
    const { transactionId } = req.params;
    const credentials = req.credentials;
    
    // Get comprehensive status from provider
    const transaction = await paymentService.getTransactionStatus(transactionId, {
      api_key: credentials.api_key
    });
    
    // Return unified status response
    res.json({
      transaction_id: transactionId,
      status: mapStatus(transaction.status), // One of: capturable, requires_action, processing, cancelled, succeeded
      
      // Money tracking - all amounts for this transaction
      authorized_money: {
        amount: transaction.authorized_amount || 0,
        currency: transaction.currency
      },
      captured_money: {
        amount: transaction.captured_amount || 0,
        currency: transaction.currency
      },
      refunded_money: {
        amount: transaction.refunded_amount || 0,
        currency: transaction.currency
      },
      
      // Next action details (for redirects, app opens, etc.)
      next_action: transaction.requires_action ? {
        type: transaction.action_type, // "redirect", "app_redirect", "webhook_wait", "none"
        redirect_url: transaction.redirect_url,
        app_url: transaction.app_url
      } : null,
      
      // Timestamps and metadata
      created_at: transaction.created_at,
      updated_at: transaction.updated_at,
      expiry_date: transaction.expires_at,
      gateway_reference: transaction.provider_id,
      metadata: transaction.metadata
    });
    
  } catch (error) {
    handleError(res, error);
  }
});

Step 6: Implement Lifecycle Hooks

Handle pre/post operation events:

// src/routes/connector.ts (continued)
router.post('/hooks', extractAuth, async (req, res) => {
  try {
    const { event, transaction_id, data } = req.body;
    const credentials = req.credentials;
    
    let result;
    
    switch (event) {
      case 'pre_authenticate':
        result = await handlePreAuthenticate(transaction_id, data, credentials);
        break;
      case 'post_authenticate':
        result = await handlePostAuthenticate(transaction_id, data, credentials);
        break;
      case 'pre_authorize':
        result = await handlePreAuthorize(transaction_id, data, credentials);
        break;
      case 'post_authorize':
        result = await handlePostAuthorize(transaction_id, data, credentials);
        break;
      case 'pre_capture':
        result = await handlePreCapture(transaction_id, data, credentials);
        break;
      case 'post_capture':
        result = await handlePostCapture(transaction_id, data, credentials);
        break;
      case 'pre_void':
        result = await handlePreVoid(transaction_id, data, credentials);
        break;
      case 'post_void':
        result = await handlePostVoid(transaction_id, data, credentials);
        break;
      case 'pre_refund':
        result = await handlePreRefund(transaction_id, data, credentials);
        break;
      case 'post_refund':
        result = await handlePostRefund(transaction_id, data, credentials);
        break;
      default:
        return res.status(400).json({
          success: false,
          error: {
            type: "validation_error",
            code: "unsupported_event",
            message: `Event type '${event}' is not supported`
          }
        });
    }
    
    res.json({
      success: true,
      data: result
    });
    
  } catch (error) {
    res.status(500).json({
      success: false,
      error: {
        type: "server_error",
        code: "hook_execution_failed",
        message: error.message
      }
    });
  }
});

Step 7: Essential Utility Functions

Status Mapping

Map your provider's statuses to Hands In standard statuses:

// src/utils/status.ts
export function mapStatus(providerStatus: string): string {
  const statusMap: Record<string, string> = {
    // Provider statuses -> Hands In statuses
    'pending': 'processing',
    'authorized': 'capturable',
    'requires_payment_method': 'requires_action',
    'requires_action': 'requires_action',
    'processing': 'processing',
    'succeeded': 'succeeded',
    'paid': 'succeeded',
    'completed': 'succeeded',
    'failed': 'cancelled',
    'canceled': 'cancelled',
    'voided': 'cancelled'
  };
  
  return statusMap[providerStatus.toLowerCase()] || 'processing';
}

Error Handling

// src/utils/errors.ts
export function handleError(res: any, error: any) {
  console.error('Payment operation failed:', error);
  
  // Map provider errors to standard format
  if (error.code === 'card_declined') {
    return res.status(402).json({
      error: {
        type: "payment_error",
        code: "card_declined",
        message: "Your card was declined",
        decline_code: error.decline_code
      }
    });
  }
  
  if (error.code === 'insufficient_funds') {
    return res.status(402).json({
      error: {
        type: "payment_error", 
        code: "card_declined",
        message: "Insufficient funds",
        decline_code: "insufficient_funds"
      }
    });
  }
  
  // Generic error
  res.status(500).json({
    error: {
      type: "server_error",
      code: "processing_error",
      message: "Payment processing failed"
    }
  });
}

Complete Payment Flow Example

Here's how a complete payment flows through your connector:

Testing Your Connector

1. Test Transaction Lifecycle

# 1. Initialize transaction
curl -X POST http://localhost:3000/initialize \
  -H "Content-Type: application/json" \
  -d '{
    "amount": { "amount": 1000, "currency": "USD" },
    "payment_method": { "type": "card", "token": "tok_visa_4242" },
    "credentials": { "api_key": "your_test_key" }
  }'

# Response: { "transaction_id": "txn_123", "status": "succeeded" }

# 2. Authorize using transaction_id
curl -X POST http://localhost:3000/authorize \
  -H "Content-Type: application/json" \
  -d '{
    "transaction_id": "txn_123",
    "credentials": { "api_key": "your_test_key" }
  }'

# 3. Check unified status
curl -X GET http://localhost:3000/status/txn_123 \
  -H "Authorization: Bearer <your-api-key>"

# 4. Capture funds
curl -X POST http://localhost:3000/capture \
  -H "Content-Type: application/json" \
  -d '{
    "transaction_id": "txn_123", 
    "credentials": { "api_key": "your_test_key" }
  }'

2. Test Error Scenarios

# Test declined card
curl -X POST http://localhost:3000/authorize \
  -H "Content-Type: application/json" \
  -d '{
    "transaction_id": "txn_decline_test",
    "credentials": { "api_key": "your_test_key" }
  }'

# Expected: 402 Payment Error response

3. Test Status Polling

# Check status every few seconds
watch -n 2 'curl -s http://localhost:3000/status/txn_123 -H "Authorization: Bearer <your-api-key>" | jq'

Migration from Old API

If you have an existing connector using the old /charge and /payments/{id} structure, here's how to migrate:

Old Structure → New Structure

Old EndpointNew EndpointsNotes
POST /chargePOST /initializePOST /authorizePOST /captureSplit into lifecycle steps
GET /payments/{id}GET /status/{transactionId}Unified status endpoint
POST /refundPOST /refundSame, but uses transaction_id
POST /cancelPOST /voidRenamed for clarity

Migration Steps

  1. Update endpoint paths in your /info response
  2. Implement /initialize to create transaction context
  3. Split charge logic across initializeauthorizecapture
  4. Update status endpoint to return unified format with money tracking
  5. Change ID parameter from payment_id to transaction_id
  6. Test thoroughly with the new flow

Best Practices

1. Transaction ID Management

  • Always use the transaction_id provided by Hands In across all operations
  • Store mappings between Hands In transaction_ids and your provider's IDs
  • Handle idempotency by checking if transaction_id already exists

2. Status Accuracy

  • Map statuses precisely - incorrect status mapping breaks polling
  • Include all money amounts in status responses (authorized, captured, refunded)
  • Set proper next_action when customer interaction is required

3. Error Handling

  • Use specific error codes to help merchants understand issues
  • Include decline codes for payment failures
  • Log errors thoroughly for debugging

4. Security

  • Validate all credentials on every request
  • Use HTTPS only in production
  • Never log sensitive data (API keys, card details)

5. Performance

  • Cache status responses when appropriate
  • Use connection pooling for provider API calls
  • Handle rate limits gracefully

Deployment Checklist

Before deploying your connector:

  • All 8 core endpoints implemented and tested
  • Status mapping covers all provider states
  • Error handling for common scenarios
  • Logging configured (no sensitive data)
  • HTTPS configured with valid certificates
  • Health check endpoint (/health) implemented
  • Environment variables configured
  • Rate limiting implemented
  • Monitoring and alerting set up
  • Documentation updated with your provider details

Next Steps

  1. Register your connector in the Hands In dashboard
  2. Configure authentication with your provider credentials
  3. Test in sandbox environment first
  4. Monitor performance and error rates
  5. Scale as needed based on transaction volume

Support

Need help building your connector?

System Architecture

Here's how your connector fits into the overall Hands In ecosystem:

Example: Stripe Connector

Here's a complete example implementing a Stripe connector using Node Express:

Main Application

// src/index.ts
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import handsInRoutes from './routes/handsin';

const app = express();

app.use(helmet());
app.use(cors());
app.use(express.json());

// Mount Hands In routes
app.use('/handsin', handsInRoutes);

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Stripe connector running on port ${PORT}`);
});

export default app;

Stripe Service

// src/services/stripeService.ts
import Stripe from 'stripe';

export class StripeService {
  private stripe: Stripe;
  
  constructor(apiKey: string) {
    this.stripe = new Stripe(apiKey, { apiVersion: '2023-10-16' });
  }
  
  async createPayment(params: {
    amount: number;
    currency: string;
    payment_method: any;
    metadata?: any;
  }) {
    const paymentIntent = await this.stripe.paymentIntents.create({
      amount: params.amount,
      currency: params.currency,
      payment_method: params.payment_method.token,
      confirmation_method: 'manual',
      confirm: true,
      metadata: params.metadata || {}
    });
    
    return {
      id: paymentIntent.id,
      status: paymentIntent.status,
      amount: paymentIntent.amount,
      currency: paymentIntent.currency,
      created_at: new Date(paymentIntent.created * 1000).toISOString()
    };
  }
  
  async getPayment(paymentId: string) {
    try {
      const paymentIntent = await this.stripe.paymentIntents.retrieve(paymentId);
      
      return {
        id: paymentIntent.id,
        status: paymentIntent.status,
        amount: paymentIntent.amount,
        currency: paymentIntent.currency,
        created_at: new Date(paymentIntent.created * 1000).toISOString(),
        failure_reason: paymentIntent.last_payment_error?.message
      };
    } catch (error) {
      if (error.code === 'resource_missing') {
        const notFoundError = new Error('Payment not found');
        (notFoundError as any).code = 'NOT_FOUND';
        throw notFoundError;
      }
      throw error;
    }
  }
  
  async refundPayment(paymentId: string, amount?: number) {
    const refund = await this.stripe.refunds.create({
      payment_intent: paymentId,
      amount: amount
    });
    
    return {
      id: refund.id,
      status: refund.status,
      amount: refund.amount,
      created_at: new Date(refund.created * 1000).toISOString()
    };
  }
}

Hands In Routes

// src/routes/handsin.ts
import { Router } from 'express';
import { StripeService } from '../services/stripeService';
import { extractAuth } from '../utils/auth';

const router = Router();

// Connector information
router.get('/info', (req, res) => {
  res.json({
    name: "Stripe Connector",
    description: "Official Stripe payment processing connector for Hands In",
    icon: "https://images.ctfassets.net/fzn2n1nzq965/3AGidihOJl4nH9D1vDjM80/7ca5e0d94956e353c81bbcf9b67b9bc9/stripe-logo-white.png",
    website: "https://stripe.com",
    
    endpoints: {
      charge: "/handsin/charge",
      get_payment: "/handsin/payments/{payment_id}",
      refund: "/handsin/refund",
      cancel: "/handsin/cancel"
    },
    
    authConfig: {
      type: "api_key",
      fields: [
        {
          key: "secret_key",
          label: "Secret Key",
          type: "password",
          required: true,
          helpText: "Your Stripe secret key (starts with sk_)",
          placeholder: "sk_test_... or sk_live_..."
        }
      ]
    },
    
    polling: {
      enabled: true,
      intervals: {
        initial: 2,
        backoff: 5,
        maximum: 30,
        timeout: 300
      },
      terminal_statuses: ["succeeded", "failed", "canceled"]
    },
    
    supports: {
      currencies: ["USD", "EUR", "GBP", "CAD", "AUD", "JPY"],
      countries: ["US", "CA", "GB", "AU", "DE", "FR", "IT", "ES", "NL", "SE"],
      cards: ["visa", "mastercard", "amex", "discover", "diners", "jcb"],
      apms: ["apple_pay", "google_pay", "link", "klarna", "afterpay"],
      refunds: true,
      partial_refunds: true,
      fraud_detection: true,
      three_d_secure: true
    },
    
    version: process.env.CONNECTOR_VERSION || "1.0.0"
  });
});

// Process payment
router.post('/charge', extractAuth, async (req, res) => {
  try {
    const { amount, payment_method, metadata } = req.body;
    const { secret_key } = req.credentials;
    
    const stripeService = new StripeService(secret_key);
    const result = await stripeService.createPayment({
      amount: amount.amount,
      currency: amount.currency,
      payment_method,
      metadata
    });
    
    res.status(201).json({
      id: result.id,
      status: mapStripeStatus(result.status),
      amount: { amount: result.amount, currency: result.currency },
      created_at: result.created_at,
      gateway_reference: result.id,
      metadata
    });
    
  } catch (error) {
    handleStripeError(res, error);
  }
});

// Check payment status
router.get('/payments/:payment_id', extractAuth, async (req, res) => {
  try {
    const { payment_id } = req.params;
    const { secret_key } = req.credentials;
    
    const stripeService = new StripeService(secret_key);
    const payment = await stripeService.getPayment(payment_id);
    
    res.json({
      id: payment.id,
      status: mapStripeStatus(payment.status),
      amount: { amount: payment.amount, currency: payment.currency },
      created_at: payment.created_at,
      gateway_reference: payment.id,
      failure_reason: payment.failure_reason
    });
    
  } catch (error) {
    if (error.code === 'NOT_FOUND') {
      return res.status(404).json({
        error: {
          type: "not_found",
          code: "payment_not_found",
          message: `Payment ${req.params.payment_id} not found`
        }
      });
    }
    handleStripeError(res, error);
  }
});

// Process refund
router.post('/refund', extractAuth, async (req, res) => {
  try {
    const { payment_id, amount } = req.body;
    const { secret_key } = req.credentials;
    
    const stripeService = new StripeService(secret_key);
    const result = await stripeService.refundPayment(payment_id, amount?.amount);
    
    res.status(201).json({
      id: result.id,
      status: mapStripeStatus(result.status),
      amount: { amount: result.amount, currency: 'usd' }, // Stripe doesn't return currency
      created_at: result.created_at,
      original_payment_id: payment_id
    });
    
  } catch (error) {
    handleStripeError(res, error);
  }
});

// Map Stripe statuses to standard statuses
function mapStripeStatus(stripeStatus: string): string {
  const statusMap = {
    'succeeded': 'completed',
    'failed': 'failed',
    'canceled': 'cancelled',
    'requires_payment_method': 'failed',
    'requires_confirmation': 'pending',
    'requires_action': 'pending',
    'processing': 'pending',
    'requires_capture': 'pending'
  };
  
  return statusMap[stripeStatus] || 'pending';
}

// Handle Stripe-specific errors
function handleStripeError(res: any, error: any) {
  console.error('Stripe error:', error);
  
  if (error.type === 'StripeCardError') {
    return res.status(402).json({
      error: {
        type: "card_error",
        code: "card_declined",
        message: error.message,
        details: {
          decline_code: error.decline_code,
          param: error.param
        }
      }
    });
  }
  
  if (error.type === 'StripeInvalidRequestError') {
    return res.status(400).json({
      error: {
        type: "validation_error",
        code: "invalid_request",
        message: error.message,
        details: {
          param: error.param
        }
      }
    });
  }
  
  // Default error
  res.status(500).json({
    error: {
      type: "api_error",
      code: "internal_error",
      message: "An error occurred processing your request"
    }
  });
}

export default router;

Authentication Utility

// src/utils/auth.ts
export interface StripeCredentials {
  secret_key: string;
}

export function extractAuth(req: any, res: any, next: any) {
  try {
    const credentials: StripeCredentials = req.body.credentials;
    
    if (!credentials || !credentials.secret_key) {
      return res.status(401).json({
        error: {
          type: "authentication_error",
          code: "missing_credentials",
          message: "Stripe secret key is required"
        }
      });
    }
    
    if (!credentials.secret_key.startsWith('sk_')) {
      return res.status(401).json({
        error: {
          type: "authentication_error",
          code: "invalid_credentials", 
          message: "Invalid Stripe secret key format"
        }
      });
    }
    
    req.credentials = credentials;
    next();
  } catch (error) {
    res.status(401).json({
      error: {
        type: "authentication_error",
        code: "invalid_credentials",
        message: "Invalid authentication format"
      }
    });
  }
}

On this page