# Hands In Connector API Framework (/docs/guides/integration/connector) 

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? [#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.

<Mermaid
  chart="graph LR
    HandsIn[Hands In Platform] --> Connector[Your Connector]
    Connector --> Provider[Payment Provider API]
    
    Provider --> Connector
    Connector --> HandsIn
    
    style Connector fill:#e1f5fe
    style HandsIn fill:#f3e5f5
    style Provider fill:#e8f5e8"
/>

### Why Connectors? [#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 [#new-transaction-based-architecture]

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

<Mermaid
  chart="graph TD
    A[Initialize Transaction] --> B[Authenticate Payment Method]
    B --> C[Authorize Payment]
    C --> D{Choose Action}
    D -->|Capture| E[Capture Funds]
    D -->|Cancel| F[Void Authorization]
    E --> G[Refund if Needed]
    
    H[Status Check] -.-> A
    H -.-> B
    H -.-> C
    H -.-> E
    H -.-> F
    H -.-> G
    
    style A fill:#e3f2fd
    style H fill:#fff3e0"
/>

## Core Endpoints [#core-endpoints]

Every connector must implement these essential endpoints:

### 1. `/info` - Connector Information [#1-info---connector-information]

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

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

### 2. `/initialize` - Initialize Transaction [#2-initialize---initialize-transaction]

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

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

### 3. `/authenticate` - Authenticate Payment Method [#3-authenticate---authenticate-payment-method]

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

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

### 4. `/authorize` - Authorize Payment [#4-authorize---authorize-payment]

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

```typescript
POST /authorize
// Authorizes funds using transaction_id from authenticate
```

### 5. `/capture` - Capture Funds [#5-capture---capture-funds]

**Purpose**: Captures (charges) a previously authorized payment

```typescript
POST /capture
// Captures funds using transaction_id from authorize
```

### 6. `/void` - Cancel Authorization [#6-void---cancel-authorization]

**Purpose**: Cancels a previously created authorization

```typescript
POST /void
// Voids authorization using transaction_id
```

### 7. `/refund` - Process Refunds [#7-refund---process-refunds]

**Purpose**: Creates refunds for completed payments

```typescript
POST /refund
// Creates a refund using transaction_id
```

### 8. `/status/{transactionId}` - Check Transaction Status [#8-statustransactionid---check-transaction-status]

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

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

### How Status Synchronization Works [#how-status-synchronization-works]

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

<Mermaid
  chart="sequenceDiagram
    participant HI as Hands In
    participant Connector as Your Connector
    participant Provider as Payment Provider
    
    HI->>Connector: POST /initialize
    Connector->>HI: transaction_id: txn_123
    
    HI->>Connector: POST /authorize {transaction_id}
    Connector->>Provider: Create authorization
    Provider->>Connector: Authorization created (pending)
    Connector->>HI: Status: pending
    
    Note over HI: Start polling with transaction_id
    
    loop Until Final Status
        HI->>Connector: GET /status/txn_123
        Connector->>Provider: Check status
        Provider->>Connector: Current status
        Connector->>HI: Unified status response
        
        alt Still Processing
            Note over HI: Wait 2s, then 5s, then 30s
        else Final Status
            Note over HI: Stop polling
        end
    end"
/>

**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-by-step-implementation-guide]

### Step 1: Set Up Your Project [#step-1-set-up-your-project]

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

```bash
# 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:**

<Mermaid
  chart="graph TD
    Root[my-connector/]
    Root --> Src[src/]
    Root --> Config[.env]
    Root --> Package[package.json]
    
    Src --> Routes[routes/]
    Src --> Services[services/]
    Src --> Types[types/]
    Src --> Utils[utils/]
    
    Routes --> Info[connector.ts]
    Services --> Payment[paymentService.ts]
    Services --> Auth[authService.ts]"
/>

### Step 2: Implement Connector Information [#step-2-implement-connector-information]

Create your connector's information endpoint:

```typescript
// 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 [#step-3-implement-authentication]

Create authentication middleware to handle credentials:

```typescript
// 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 [#step-4-implement-transaction-lifecycle]

#### 4.1 Initialize Transaction [#41-initialize-transaction]

```typescript
// 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 [#42-authenticate-payment-method]

```typescript
// 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 [#43-authorize-payment]

```typescript
// 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 [#44-capture-funds]

```typescript
// 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 [#45-void-authorization]

```typescript
// 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 [#46-process-refunds]

```typescript
// 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 [#step-5-implement-unified-status-checking]

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

```typescript
// 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 [#step-6-implement-lifecycle-hooks]

Handle pre/post operation events:

```typescript
// 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 [#step-7-essential-utility-functions]

#### Status Mapping [#status-mapping]

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

```typescript
// 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 [#error-handling]

```typescript
// 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 [#complete-payment-flow-example]

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

<Mermaid
  chart="sequenceDiagram
    participant HI as Hands In
    participant Connector as Your Connector
    participant Provider as Payment Provider
    
    Note over HI,Provider: 1. Initialize Transaction
    HI->>Connector: POST /initialize
    Connector->>Provider: Create transaction context
    Provider->>Connector: transaction_id: txn_123
    Connector->>HI: { transaction_id: &#x22;txn_123&#x22;, status: &#x22;pending&#x22; }
    
    Note over HI,Provider: 2. Authenticate (optional)
    HI->>Connector: POST /authenticate
    Connector->>Provider: Perform 3DS/SCA
    Provider->>Connector: Authentication success
    Connector->>HI: { status: &#x22;succeeded&#x22;, liability_shift: true }
    
    Note over HI,Provider: 3. Authorize Payment
    HI->>Connector: POST /authorize { transaction_id: &#x22;txn_123&#x22; }
    Connector->>Provider: Create authorization
    Provider->>Connector: Authorization created
    Connector->>HI: { status: &#x22;succeeded&#x22;, expires_at: &#x22;...&#x22; }
    
    Note over HI,Provider: 4. Status Polling
    loop Until Captured
        HI->>Connector: GET /status/txn_123
        Connector->>Provider: Check status
        Provider->>Connector: Current status
        Connector->>HI: { status: &#x22;capturable&#x22;, authorized_money: {...} }
    end
    
    Note over HI,Provider: 5. Capture Funds
    HI->>Connector: POST /capture { transaction_id: &#x22;txn_123&#x22; }
    Connector->>Provider: Capture funds
    Provider->>Connector: Capture successful
    Connector->>HI: { status: &#x22;succeeded&#x22; }
    
    Note over HI,Provider: 6. Final Status Check
    HI->>Connector: GET /status/txn_123
    Connector->>HI: { status: &#x22;succeeded&#x22;, captured_money: {...} }"
/>

## Testing Your Connector [#testing-your-connector]

### 1. Test Transaction Lifecycle [#1-test-transaction-lifecycle]

```bash
# 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 [#2-test-error-scenarios]

```bash
# 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 [#3-test-status-polling]

```bash
# 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 [#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-structure--new-structure]

| Old Endpoint         | New Endpoints                                            | Notes                           |
| -------------------- | -------------------------------------------------------- | ------------------------------- |
| `POST /charge`       | `POST /initialize` → `POST /authorize` → `POST /capture` | Split into lifecycle steps      |
| `GET /payments/{id}` | `GET /status/{transactionId}`                            | Unified status endpoint         |
| `POST /refund`       | `POST /refund`                                           | Same, but uses `transaction_id` |
| `POST /cancel`       | `POST /void`                                             | Renamed for clarity             |

### Migration Steps [#migration-steps]

1. **Update endpoint paths** in your `/info` response
2. **Implement `/initialize`** to create transaction context
3. **Split charge logic** across `initialize` → `authorize` → `capture`
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 [#best-practices]

### 1. Transaction ID Management [#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 [#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 [#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 [#4-security]

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

### 5. Performance [#5-performance]

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

## Deployment Checklist [#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 [#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 [#support]

Need help building your connector?

* 📧 **Email**: [tech@handsin.com](mailto:tech@handsin.com)
* 📖 **API Reference**: [Hands In Merchant API reference](/docs/API/v1/createGroupPayment)
* 💬 **Discord**: [Join our developer community](https://discord.gg/8FpPeeCj6z)
* 🐛 **Issues**: [Contact support](/docs/guides/support/contact-us) with the failing request and response

## System Architecture [#system-architecture]

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

<Mermaid
  chart="graph TB
    subgraph &#x22;Client Applications&#x22;
        Mobile[Mobile App]
        Web[Web Portal]
        Widget[Payment Widget]
    end
    
    subgraph &#x22;Hands In Platform&#x22;
        API[Hands In API]
        Router[Payment Router]
        Auth[Authentication Service]
        DB[(Database)]
        Events[Event System]
    end
    
    subgraph &#x22;Your Connector&#x22;
        YourConn[Your Connector]
        InfoEndpoint[&#x22;/handsin/info&#x22;]
        ChargeEndpoint[&#x22;/handsin/charge&#x22;]
        StatusEndpoint[&#x22;/handsin/payments/*&#x22;]
    end
    
    subgraph &#x22;Payment Providers&#x22;
        YourProvider[Your Payment Provider]
    end
    
    Mobile --> API
    Web --> API
    Widget --> API
    
    API --> Router
    Router --> YourConn
    
    YourConn --> InfoEndpoint
    YourConn --> ChargeEndpoint
    YourConn --> StatusEndpoint
    
    YourConn --> YourProvider
    
    style YourConn fill:#e1f5fe
    style YourProvider fill:#e8f5e8"
/>

## Example: Stripe Connector [#example-stripe-connector]

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

### Main Application [#main-application]

```typescript
// 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 [#stripe-service]

```typescript
// 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 [#hands-in-routes]

```typescript
// 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 [#authentication-utility]

```typescript
// 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"
      }
    });
  }
}
```
