---
# System prepended metadata

title: Tazapay Provider Implementation Plan

---

# Tazapay Provider Implementation Plan

## Overview

This document provides a detailed implementation plan for integrating Tazapay as a uRamp provider, supporting both onramp and offramp flows for the following currencies:

**Fiat Currencies**: USD, SGD, EUR, INR, BRL, PHP, THB, MXN, VND
**Crypto Currencies**: USDC, USDT (on Ethereum, Polygon, Tron, Solana)

---

## === FILE STRUCTURE ===

### 1. File-by-File Breakdown

```
packages/providers/tazapay/
├── package.json                          # Package configuration
├── tsconfig.json                         # TypeScript configuration
├── manifest.ts                           # Plugin manifest with capabilities
├── index.ts                              # Main exports
│
├── client/                               # API Client Layer
│   ├── tazapay.api.service.ts           # Tazapay API client
│   ├── credential.schema.ts             # Credential form schema
│   ├── interfaces.ts                     # TypeScript interfaces
│   └── config.ts                         # Configuration constants
│
├── api-server/                           # Server-side hooks
│   ├── handlers/
│   │   ├── index.ts                      # Handler exports (collectRequirements, preCheck, check, postCheck)
│   │   ├── tazapay-create-entity.ts      # Entity creation handler (KYB)
│   │   ├── tazapay-verify-entity.ts      # Entity verification handler
│   │   ├── tazapay-create-customer.ts    # Customer creation handler (payins)
│   │   └── tazapay-create-beneficiary.ts # Beneficiary creation handler
│   ├── types.ts                          # Server-side types
│   └── webhook-module.ts                 # Webhook handling
│
├── money-movement/                       # Money movement hooks
│   ├── index.ts                          # Exports createTransaction
│   └── transaction.ts                    # Transaction implementation
│
└── dashboard/                            # Dashboard components
    ├── index.ts                          # Dashboard module export
    └── handlers/
        ├── index.ts                      # Handler exports
        ├── TazapayOnboarding.tsx         # Onboarding UI component
        ├── tazapay-onboarding.ts         # Onboarding server action
        └── tazapay-onboarding.schema.ts  # Form schemas
```

---

### File Details

#### `manifest.ts`
**Purpose**: Define plugin metadata, capabilities, and requirements
**Key Exports**: `manifest: PluginManifest`

```typescript
// Pseudocode
export const manifest: PluginManifest = {
  id: 'TAZAPAY',
  version: '1.0.0',
  name: 'tazapay',
  description: 'Tazapay cross-border payment provider',
  requiredWebhookKeys: ['events'],
  webhookRegistrationMethod: WebhookRegistrationMethod.DASHBOARD,
  credentialSchema: { jsonSchema, uiSchema },
  capabilities: [
    // Onramp: Fiat -> USDC/USDT
    { src: 'USD', dest: 'USDC_ETH', priority: 100 },
    { src: 'USD', dest: 'USDC_POLYGON', priority: 100 },
    // ... all currency pairs
    // Offramp: USDC/USDT -> Fiat
    { src: 'USDC_ETH', dest: 'USD', priority: 100 },
    // ... etc
  ],
  requirementsByEdgeType: {
    KYC_SERVICE_PROVIDER: [
      { code: 'TAZAPAY_CREATE_ENTITY', description: 'Create Tazapay Entity' },
      { code: 'TAZAPAY_VERIFY_ENTITY', description: 'Verify Entity KYB', dependencies: ['TAZAPAY_CREATE_ENTITY'] },
    ],
  },
};
```

---

#### `client/tazapay.api.service.ts`
**Purpose**: HTTP client for Tazapay API
**Key Exports**: `TazapayApiService` class

```typescript
// Pseudocode for key methods
class TazapayApiService {
  constructor(config: TazapayConfig) { ... }

  // Authentication: HTTP Basic Auth
  private _getAuthHeader(): string {
    return `Basic ${Buffer.from(`${apiKey}:${apiSecret}`).toString('base64')}`;
  }

  // Entity (KYB) APIs
  async createEntity(request: EntityCreateRequest): Promise<EntityResponse> { ... }
  async getEntity(entityId: string): Promise<EntityResponse> { ... }
  async submitEntity(entityId: string): Promise<EntityResponse> { ... }

  // Customer APIs
  async createCustomer(request: CustomerCreateRequest): Promise<CustomerResponse> { ... }
  async getCustomer(customerId: string): Promise<CustomerResponse> { ... }

  // Beneficiary APIs
  async createBeneficiary(request: BeneficiaryCreateRequest): Promise<BeneficiaryResponse> { ... }
  async getBeneficiary(beneficiaryId: string): Promise<BeneficiaryResponse> { ... }

  // Payout APIs
  async createPayoutQuote(request: PayoutQuoteRequest): Promise<PayoutQuoteResponse> { ... }
  async createPayout(request: PayoutCreateRequest): Promise<PayoutResponse> { ... }
  async getPayout(payoutId: string): Promise<PayoutResponse> { ... }

  // Checkout/Payin APIs
  async createCheckout(request: CheckoutCreateRequest): Promise<CheckoutResponse> { ... }
  async getCheckout(checkoutId: string): Promise<CheckoutResponse> { ... }

  // Balance/FX APIs
  async getBalance(): Promise<BalanceResponse> { ... }
  async getFxQuote(request: FxQuoteRequest): Promise<FxQuoteResponse> { ... }

  // Collection Account APIs
  async getCollectionAccounts(): Promise<CollectionAccountResponse[]> { ... }
}
```

---

#### `client/interfaces.ts`
**Purpose**: TypeScript type definitions for all API interactions
**Key Exports**: All request/response interfaces

```typescript
// Key interfaces (see INSIGHTS.md Section 11 for full schemas)
interface TazapayCredentials { apiKey, apiSecret, webhookSecret, baseUrl }
interface EntityCreateRequest { name, type, email, ... }
interface EntityResponse { id, approval_status, ... }
interface BeneficiaryCreateRequest { name, type, destination_details, ... }
interface PayoutQuoteRequest { payout_type, payout_info, ... }
interface PayoutCreateRequest { amount, currency, beneficiary, ... }
interface CheckoutCreateRequest { invoice_currency, amount, customer_details, ... }
interface WebhookPayload { type, id, created_at, data }
```

---

#### `api-server/handlers/index.ts`
**Purpose**: Dynamic handler loading and requirement collection
**Key Exports**: `collectRequirements`, `preCheck`, `check`, `postCheck`

```typescript
// Pseudocode
export const collectRequirements = async (intent, edge, context) => {
  const requirements = manifest.requirementsByEdgeType?.[edge.type];
  if (!requirements) return [];
  return requirements.map(req => ({
    code: req.code,
    description: req.description,
    dependencies: req.dependencies,
    complianceEdge: { connect: { id: edge.id } },
    compliancePlan: { connect: { id: intent.compliancePlan!.id } },
  }));
};

// Dynamic handler dispatch
async function _executeHandler(phase, intent, requirement, context) {
  const fileName = requirementCodeToHandlerFileName(requirement.code);
  const functionName = phase + requirementCodeToHandlerFunctionSuffix(requirement.code);
  const handlerModule = await import(`./${fileName}.js`);
  const handler = handlerModule[functionName];
  if (!handler) return intent;
  return handler(intent, requirement, context);
}

export const preCheck = (intent, req, ctx) => _executeHandler('preCheck', intent, req, ctx);
export const check = (intent, req, ctx) => _executeHandler('check', intent, req, ctx);
export const postCheck = (intent, req, ctx) => _executeHandler('postCheck', intent, req, ctx);
```

---

#### `api-server/handlers/tazapay-create-entity.ts`
**Purpose**: Create Tazapay Entity for KYB
**Key Exports**: `checkTazapayCreateEntity`, `postCheckTazapayCreateEntity`

```typescript
// Pseudocode
export async function checkTazapayCreateEntity(intent, requirement, context) {
  const credentials = await context.getCredentials();
  const client = new TazapayApiService(credentials);

  // Check if entity already exists in CustomerProvider
  const customerProvider = await getCustomerProvider(prisma, customerId, 'tazapay');
  if (customerProvider?.providerReferenceId) {
    // Entity exists, verify status
    const entity = await client.getEntity(customerProvider.providerReferenceId);
    if (entity.approval_status === 'approved') return intent; // Done
    if (entity.approval_status === 'rejected') throw new Error('KYB rejected');
  }

  // Create new entity
  const entityRequest = buildEntityRequest(intent);
  const entity = await client.createEntity(entityRequest);

  // Store entity ID
  await upsertCustomerProvider(prisma, customerId, 'tazapay', {
    providerReferenceId: entity.id,
    onboardingData: { entityId: entity.id, kybStatus: entity.approval_status },
  });

  // Store in intent.metadata for downstream handlers
  await context.prisma.intent.update({
    where: { id: intent.id },
    data: { metadata: { ...intent.metadata, tazapay_entity_id: entity.id } },
  });

  return intent;
}

export async function postCheckTazapayCreateEntity(intent, requirement, context) {
  // Propagate entity ID to dependent steps
  const entityId = (requirement.metadata as any)?.entityId;
  // ...propagate to TAZAPAY_VERIFY_ENTITY step
  return intent;
}
```

---

#### `api-server/handlers/tazapay-verify-entity.ts`
**Purpose**: Verify Entity KYB status
**Key Exports**: `checkTazapayVerifyEntity`

```typescript
// Pseudocode
export async function checkTazapayVerifyEntity(intent, requirement, context) {
  const credentials = await context.getCredentials();
  const client = new TazapayApiService(credentials);

  const entityId = (intent.metadata as any)?.tazapay_entity_id;
  if (!entityId) throw new Error('Entity ID not found');

  const entity = await client.getEntity(entityId);

  switch (entity.approval_status) {
    case 'approved':
      // Update CustomerProvider status
      await updateCustomerProviderById(prisma, customerProvider.id, {
        onboardingData: { kybStatus: 'approved', approvedAt: new Date().toISOString() },
      });
      return intent;

    case 'pending':
      throw new Error('KYB pending additional documents');

    case 'rejected':
      throw new Error(`KYB rejected: ${entity.rejection_reason}`);

    case 'submitted':
    case 'initiated':
      throw new Error('KYB still under review');

    default:
      throw new Error(`Unknown KYB status: ${entity.approval_status}`);
  }
}
```

---

#### `api-server/webhook-module.ts`
**Purpose**: Handle incoming webhooks from Tazapay
**Key Exports**: `webhookModule: WebhookModule`

```typescript
// Pseudocode
export const webhookModule: WebhookModule = {
  async authenticate(authContext, pluginContext): Promise<string | null> {
    const { request } = authContext;
    const credentials = await pluginContext.getCredentials();

    // Verify HMAC-SHA256 signature
    const payload = JSON.stringify(request.body);
    const signature = request.headers['x-tazapay-signature'];
    const eventId = request.body.id;
    const createdAt = request.body.created_at;

    if (!_verifySignature(payload, signature, credentials.webhookSecret, eventId, createdAt)) {
      return null;
    }

    // Check timestamp for replay attack
    if (!_isTimestampValid(createdAt, 10 * 60 * 1000)) return null;

    // Find intent by provider transaction ID
    const data = request.body.data;
    const intent = await _findIntentByProviderRef(pluginContext.prisma, data.id);

    return intent?.id ?? null;
  },

  async handle(handleContext, pluginContext): Promise<void> {
    const { intentId, webhookEvent } = handleContext;
    const payload = webhookEvent.eventData;
    const eventType = payload.type;

    // Route by event type
    if (eventType.startsWith('payout.')) {
      await _handlePayoutWebhook(intentId, payload, pluginContext);
    } else if (eventType.startsWith('payin.')) {
      await _handlePayinWebhook(intentId, payload, pluginContext);
    } else if (eventType.startsWith('entity.')) {
      await _handleEntityWebhook(intentId, payload, pluginContext);
    } else if (eventType.startsWith('collect.')) {
      await _handleCollectWebhook(intentId, payload, pluginContext);
    }
  },
};

// Helper: HMAC-SHA256 verification per Tazapay docs
function _verifySignature(payload, signature, secret, eventId, createdAt): boolean {
  const signatureData = `${eventId}${payload}${createdAt}`;
  const expected = crypto.createHmac('sha256', secret).update(signatureData).digest('base64');
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
```

---

#### `money-movement/transaction.ts`
**Purpose**: Execute onramp and offramp transactions
**Key Exports**: `createTransaction`

```typescript
// Pseudocode
export async function createTransaction(
  context: PluginContext,
  transactionContext: TransactionContext,
): Promise<PluginTransactionResult> {
  const credentials = await context.getCredentials();
  const client = new TazapayApiService(credentials);

  const { intent, externalAccounts, amount } = transactionContext;
  const isOfframp = externalAccounts.src.type === ExternalAccountType.CRYPTO_ADDRESS;

  // Check for existing transaction (idempotency)
  const existingTxnId = (intent.metadata as any)?.tazapay_payout_id || (intent.metadata as any)?.tazapay_checkout_id;
  if (existingTxnId) {
    return _handleExistingTransaction(client, existingTxnId, isOfframp);
  }

  if (isOfframp) {
    return _handleOfframp(client, context, transactionContext);
  } else {
    return _handleOnramp(client, context, transactionContext);
  }
}

async function _handleOfframp(client, context, transactionContext): Promise<PluginTransactionResult> {
  const { intent, externalAccounts, amount } = transactionContext;

  // 1. Get or create beneficiary
  const beneficiaryId = await _getOrCreateBeneficiary(client, context, externalAccounts.dest);

  // 2. Create payout quote
  const quote = await client.createPayoutQuote({
    payout_type: _determinePayoutType(externalAccounts.dest),
    payout_info: { currency: amount.currency.code, amount: _toCents(amount.amount) },
    holding_info: { currency: _getHoldingCurrency(externalAccounts.src) },
  });

  // 3. Create payout
  const payout = await client.createPayout({
    amount: _toCents(amount.amount),
    currency: amount.currency.code,
    beneficiary: beneficiaryId,
    quote: quote.id,
    purpose: 'PYR001', // Generic purpose code
    transaction_description: `Offramp ${intent.id}`,
    reference_id: intent.id,
  });

  // 4. Store provider reference
  await context.prisma.intent.update({
    where: { id: intent.id },
    data: { metadata: { ...(intent.metadata || {}), tazapay_payout_id: payout.id, tazapay_beneficiary_id: beneficiaryId } },
  });

  // 5. Return deposit instructions for crypto
  return {
    status: MoneyMovementTransactionStatus.IN_PROGRESS,
    depositInstructions: _buildCryptoDepositInstructions(externalAccounts.src, await _getCollectionWallet(client)),
  };
}

async function _handleOnramp(client, context, transactionContext): Promise<PluginTransactionResult> {
  const { intent, externalAccounts, amount } = transactionContext;

  // 1. Create checkout session
  const checkout = await client.createCheckout({
    invoice_currency: amount.currency.code,
    amount: _toCents(amount.amount),
    customer_details: _buildCustomerDetails(intent),
    success_url: `${process.env.APP_URL}/checkout/success?intent=${intent.id}`,
    cancel_url: `${process.env.APP_URL}/checkout/cancel?intent=${intent.id}`,
    transaction_description: `Onramp ${intent.id}`,
    reference_id: intent.id,
  });

  // 2. Store provider reference
  await context.prisma.intent.update({
    where: { id: intent.id },
    data: { metadata: { ...(intent.metadata || {}), tazapay_checkout_id: checkout.id, tazapay_checkout_url: checkout.url } },
  });

  // 3. Return hosted checkout instructions
  return {
    status: MoneyMovementTransactionStatus.IN_PROGRESS,
    depositInstructions: [{
      kind: 'HOSTED',
      label: `Complete payment via Tazapay`,
      hostedUrl: checkout.url,
      metadata: { checkoutId: checkout.id, expiresAt: checkout.expires_at },
    }],
  };
}
```

---

## === SEQUENCE DIAGRAMS ===

### 2. Flow Diagrams

#### a) Onboarding Flow (KYB)

```mermaid
sequenceDiagram
    participant User
    participant Dashboard
    participant API Server
    participant Tazapay API
    participant Webhook

    User->>Dashboard: Start onboarding
    Dashboard->>API Server: Create Intent
    API Server->>API Server: collectRequirements()
    Note over API Server: Returns TAZAPAY_CREATE_ENTITY, TAZAPAY_VERIFY_ENTITY

    API Server->>API Server: check(TAZAPAY_CREATE_ENTITY)
    API Server->>Tazapay API: POST /v3/entity
    Tazapay API-->>API Server: { id: "ent_xxx", approval_status: "initiated" }
    API Server->>API Server: Store entityId in CustomerProvider
    API Server->>API Server: Update intent.metadata

    API Server->>Tazapay API: POST /v3/entity/{id}/submit
    Tazapay API-->>API Server: { approval_status: "submitted" }

    Note over Tazapay API: Async KYB review...

    Tazapay API->>Webhook: POST entity.approved
    Webhook->>API Server: webhookModule.authenticate()
    Webhook->>API Server: webhookModule.handle()
    API Server->>API Server: Update CustomerProvider.onboardingData

    API Server->>API Server: check(TAZAPAY_VERIFY_ENTITY)
    API Server->>Tazapay API: GET /v3/entity/{id}
    Tazapay API-->>API Server: { approval_status: "approved" }
    API Server-->>Dashboard: KYB Complete
    Dashboard-->>User: Onboarding complete
```

#### b) Quote Flow

```mermaid
sequenceDiagram
    participant User
    participant API Server
    participant Tazapay API

    User->>API Server: Request quote (100 USD -> EUR bank)
    API Server->>Tazapay API: POST /v3/payout/quote
    Note over API Server,Tazapay API: { payout_type: "local", payout_info: { currency: "USD", amount: 10000 }, destination_info: { currency: "EUR" } }
    Tazapay API-->>API Server: PayoutQuoteResponse
    Note over Tazapay API,API Server: { id: "poq_xxx", holding_info: { amount: 10000 }, destination_info: { amount: 9200 }, fee_info: { ... }, exchange_rates: { ... }, valid_until: "..." }
    API Server->>API Server: Store quote details
    API Server-->>User: Quote response
    Note over User: Quote valid ~30 min
```

#### c) Transaction Flow (Offramp: Crypto -> Fiat)

```mermaid
sequenceDiagram
    participant User
    participant API Server
    participant Tazapay API
    participant Blockchain
    participant Webhook

    User->>API Server: Confirm offramp (USDC -> USD bank)
    API Server->>API Server: createTransaction()

    API Server->>Tazapay API: POST /v3/beneficiary (if needed)
    Tazapay API-->>API Server: { id: "bnf_xxx" }

    API Server->>Tazapay API: POST /v3/payout/quote
    Tazapay API-->>API Server: { id: "poq_xxx", valid_until: "..." }

    API Server->>Tazapay API: POST /v3/payout
    Note over API Server,Tazapay API: { amount, currency, beneficiary: "bnf_xxx", quote: "poq_xxx", ... }
    Tazapay API-->>API Server: { id: "pot_xxx", status: "requires_funding" }

    API Server->>API Server: Store pot_xxx in intent.metadata
    API Server-->>User: Deposit instructions (collection wallet address)

    User->>Blockchain: Send USDC to collection wallet
    Blockchain-->>Tazapay API: Funds received

    Tazapay API->>Webhook: POST collect.succeeded
    Webhook->>API Server: Update transaction status

    Tazapay API->>Webhook: POST payout.processing
    Webhook->>API Server: Status: IN_PROGRESS

    Note over Tazapay API: Process payout...

    Tazapay API->>Webhook: POST payout.succeeded
    Webhook->>API Server: Status: SUCCESS
    API Server->>API Server: Update MoneyMovementTransaction
    API Server-->>User: Transaction complete
```

#### d) Transaction Flow (Onramp: Fiat -> Crypto)

```mermaid
sequenceDiagram
    participant User
    participant Dashboard
    participant API Server
    participant Tazapay API
    participant Hosted Checkout
    participant Webhook

    User->>API Server: Initiate onramp (USD card -> USDC)
    API Server->>API Server: createTransaction()

    API Server->>Tazapay API: POST /v3/checkout
    Note over API Server,Tazapay API: { invoice_currency: "USD", amount: 10000, customer_details: {...}, success_url, cancel_url }
    Tazapay API-->>API Server: { id: "chk_xxx", url: "https://checkout.tazapay.com/..." }

    API Server->>API Server: Store chk_xxx in intent.metadata
    API Server-->>Dashboard: Redirect to hosted checkout
    Dashboard-->>User: Show checkout link

    User->>Hosted Checkout: Complete payment
    Hosted Checkout-->>User: Redirect to success_url

    Tazapay API->>Webhook: POST payin.succeeded
    Webhook->>API Server: webhookModule.handle()
    API Server->>API Server: Funds in balance

    Note over API Server: Trigger payout to user's wallet

    API Server->>Tazapay API: POST /v3/payout (type: wallet)
    Tazapay API-->>API Server: { id: "pot_xxx" }

    Tazapay API->>Webhook: POST payout.succeeded
    Webhook->>API Server: Status: SUCCESS
    API Server-->>User: USDC received
```

#### e) Webhook Processing Flow

```mermaid
sequenceDiagram
    participant Tazapay
    participant Webhook Endpoint
    participant webhookModule
    participant Database
    participant Temporal

    Tazapay->>Webhook Endpoint: POST /webhook/tazapay/:projectId/:webhookKey
    Note over Tazapay,Webhook Endpoint: Headers: x-tazapay-signature<br>Body: { type: "payout.succeeded", id: "evt_xxx", created_at, data: {...} }

    Webhook Endpoint->>webhookModule: authenticate(request)

    webhookModule->>webhookModule: Verify HMAC-SHA256 signature
    Note over webhookModule: signature_data = event_id + payload + created_at<br>expected = HMAC-SHA256(secret, signature_data)

    webhookModule->>webhookModule: Check timestamp (< 10 min)

    webhookModule->>Database: Find intent by provider reference
    Note over webhookModule,Database: Query intent.metadata.tazapay_payout_id

    webhookModule-->>Webhook Endpoint: Return intentId

    Webhook Endpoint->>Database: Create WebhookEvent record

    Webhook Endpoint-->>Tazapay: 200 OK

    Note over Webhook Endpoint: Async processing begins

    Webhook Endpoint->>webhookModule: handle(intentId, webhookEvent)

    webhookModule->>Database: Update MoneyMovementTransaction status
    Note over webhookModule,Database: payout.succeeded -> SUCCESS<br>payout.failed -> FAILED

    webhookModule->>Database: Update intent.metadata with tracking details

    webhookModule->>Database: Mark WebhookEvent as COMPLETED

    webhookModule->>Temporal: triggerCheckOnce(intentId)
    Note over Temporal: Workflow advancement
```

---

## === DATA MODEL DESIGN ===

### 3. Persistence Schema

#### a) CustomerProvider.onboardingData Structure

```typescript
interface TazapayOnboardingData {
  // Entity (KYB) tracking
  entityId?: string;              // ent_xxx - Tazapay entity ID
  entityType?: 'individual' | 'sole_proprietorship' | 'company' | 'non_profit' | 'government_entity';
  kybStatus?: 'initiated' | 'submitted' | 'pending' | 'approved' | 'rejected';
  kybSubmittedAt?: string;        // ISO 8601 timestamp
  kybApprovedAt?: string;         // ISO 8601 timestamp
  kybRejectedAt?: string;         // ISO 8601 timestamp
  kybRejectionReason?: string;    // Reason if rejected
  pendingDocuments?: string[];    // Document types still needed

  // Customer (payin) tracking
  customerId?: string;            // cus_xxx - Tazapay customer ID

  // Beneficiary cache
  beneficiaries?: {
    id: string;                   // bnf_xxx
    type: 'bank' | 'wallet' | 'local_payment_network';
    currency: string;             // e.g., "USD", "EUR"
    destinationType: string;      // e.g., "swift", "local", "upi"
    destinationId: string;        // bnk_xxx or wal_xxx
    createdAt: string;
    isVerified: boolean;
  }[];

  // Collection accounts
  collectionAccounts?: {
    id: string;                   // cva_xxx
    paymentMethodType: string;    // e.g., "stablecoin_usdc"
    currency: string;
    chain?: string;               // For crypto: ethereum, polygon, etc.
    depositAddress?: string;      // For crypto collection
    virtualAccount?: {
      accountNumber: string;
      bankName: string;
      routingCodes: Record<string, string>;
    };
    createdAt: string;
  }[];

  // Last webhook event tracking
  lastWebhookEventId?: string;
  lastWebhookEventAt?: string;
}
```

#### b) Intent.metadata Fields

**Fields READ from intent.metadata:**
```typescript
interface TazapayIntentMetadataRead {
  // Customer info (populated by uRamp)
  customer_email?: string;
  customer_name?: string;
  customer_country?: string;
  customer_phone?: { calling_code: string; number: string };

  // Destination account details
  destination_bank?: {
    account_number: string;
    bank_codes: Record<string, string>;
    country: string;
    currency: string;
  };
  destination_wallet?: {
    address: string;
    chain: string;
    currency: string;
  };

  // Source details for onramp
  source_payment_method?: string;  // card, bank_transfer, etc.
}
```

**Fields WRITTEN to intent.metadata:**
```typescript
interface TazapayIntentMetadataWrite {
  // Entity tracking
  tazapay_entity_id?: string;           // ent_xxx

  // Customer tracking
  tazapay_customer_id?: string;         // cus_xxx

  // Beneficiary tracking
  tazapay_beneficiary_id?: string;      // bnf_xxx

  // Transaction tracking - Payout (offramp)
  tazapay_payout_id?: string;           // pot_xxx
  tazapay_payout_quote_id?: string;     // poq_xxx
  tazapay_payout_status?: string;       // Provider status

  // Transaction tracking - Checkout (onramp)
  tazapay_checkout_id?: string;         // chk_xxx
  tazapay_checkout_url?: string;        // Hosted checkout URL
  tazapay_payin_id?: string;            // pay_xxx (linked to checkout)

  // Collection tracking
  tazapay_collect_id?: string;          // col_xxx
  tazapay_collection_account_id?: string;

  // FX tracking
  tazapay_fx_quote_id?: string;         // fx_xxx
  tazapay_exchange_rate?: number;

  // Tracking/settlement info
  tazapay_tracking_number?: string;
  tazapay_tracking_type?: 'uetr' | 'utr' | 'transaction_hash' | 'pix_id';

  // Completion data
  tazapay_completed_at?: string;
  tazapay_settlement_date?: string;
  tazapay_fee_amount?: number;
  tazapay_fee_currency?: string;
}
```

#### c) CompliancePlanStep.metadata Fields

```typescript
interface TazapayStepMetadata {
  // For TAZAPAY_CREATE_ENTITY step
  entityId?: string;
  entityType?: string;
  submittedAt?: string;

  // For TAZAPAY_VERIFY_ENTITY step
  verificationAttempts?: number;
  lastVerificationAt?: string;
  approvalStatus?: string;

  // For TAZAPAY_CREATE_CUSTOMER step
  customerId?: string;

  // For TAZAPAY_CREATE_BENEFICIARY step
  beneficiaryId?: string;
  beneficiaryType?: 'bank' | 'wallet' | 'local_payment_network';
  isVerificationRequired?: boolean;
  verificationStatus?: string;

  // Error tracking
  lastError?: string;
  errorCode?: string;
  retryCount?: number;
}
```

#### d) MoneyMovementTransaction.context Fields

```typescript
interface TazapayTransactionContext {
  // Payout tracking
  payoutId?: string;                    // pot_xxx
  payoutQuoteId?: string;               // poq_xxx
  payoutType?: 'local' | 'swift' | 'wallet' | 'local_payment_network';
  payoutStatus?: string;                // Provider status string

  // Payin tracking
  payinId?: string;                     // pay_xxx
  checkoutId?: string;                  // chk_xxx
  checkoutUrl?: string;
  paymentMethod?: string;               // card, bank_transfer, etc.

  // Collect tracking
  collectId?: string;                   // col_xxx
  collectionAccountId?: string;         // cva_xxx

  // Beneficiary info
  beneficiaryId?: string;               // bnf_xxx
  beneficiaryType?: 'bank' | 'wallet' | 'local_payment_network';

  // FX info
  fxQuoteId?: string;                   // fx_xxx
  holdingCurrency?: string;
  destinationCurrency?: string;
  exchangeRate?: number;

  // Fee breakdown
  fees?: {
    fixed?: { amount: number; currency: string };
    variable?: { percentage: number; amount: number; currency: string };
    total?: number;
  };

  // Tracking details
  trackingDetails?: {
    trackingNumber: string;
    trackingType: 'uetr' | 'utr' | 'transaction_hash' | 'pix_id';
  };

  // Balance info
  balanceTransactionId?: string;        // btr_xxx
  availableBalance?: number;
  isBalanceSufficient?: boolean;

  // Timestamps
  providerCreatedAt?: string;
  providerCompletedAt?: string;
  settlementDate?: string;

  // Error info (if failed)
  failureReason?: string;
  failureCode?: string;
  isRetryable?: boolean;
}
```

---

## === EDGE CASES & ERROR HANDLING ===

### 4. Edge Case Catalog

| Scenario | Detection | Handling | Recovery |
|----------|-----------|----------|----------|
| **API timeout** | HTTP timeout (>30s) | Retry with exponential backoff | Max 3 retries, then fail with PROVIDER_TIMEOUT |
| **Rate limited (429)** | HTTP 429 response | Exponential backoff starting at 1s | Max delay 30s, max 5 retries |
| **Server error (5xx)** | HTTP 500/502/503/504 | Retry with backoff | Max 3 retries, log for ops investigation |
| **Invalid credentials** | HTTP 401 | No retry, fail immediately | Alert ops, require credential update |
| **Insufficient permissions** | HTTP 403 | No retry | Check API key permissions in dashboard |
| **Entity not found** | HTTP 404 on GET /v3/entity | Check CustomerProvider for stale ID | Re-create entity if needed |
| **Duplicate entity** | Error code on POST /v3/entity | Extract existing entity ID from error | Use existing entity, update CustomerProvider |
| **KYB rejected** | `entity.rejected` webhook or approval_status | Mark step failed, notify user | Manual review, allow re-submission |
| **KYB pending docs** | `entity.pending` webhook or approval_status | Mark step ACTION_REQUIRED | Prompt user for additional documents |
| **Quote expired** | HTTP 422 with code 20299 | Create new quote | Auto-refresh quote on retry |
| **Insufficient balance** | Error code 20199 or `requires_funding` status | Wait for deposit | Poll collection account, retry payout |
| **Amount outside limits** | Error code 20298 | Fail with clear message | Inform user of min/max limits |
| **Invalid bank details** | Error codes 3524, 3529 | Fail with validation error | Prompt user to correct details |
| **Beneficiary verification required** | `is_doc_verification_required: true` | Mark step ACTION_REQUIRED | Prompt user for wallet verification docs |
| **Webhook signature invalid** | HMAC mismatch | Reject webhook, log warning | Alert ops if persistent |
| **Webhook replay attack** | Timestamp > 10 minutes old | Reject webhook | Log for investigation |
| **Duplicate webhook** | Same event_id already processed | Idempotent skip | Return 200 OK, no action |
| **Transaction already exists** | provider_transaction_id in intent.metadata | Return existing transaction status | Idempotent response |
| **Payout reversed** | `payout.reversed` webhook | Mark FAILED, store reason | Manual ops intervention |
| **Partial completion** | N/A (Tazapay doesn't support partial) | Full amount or failure | Standard retry/fail flow |
| **Collection wallet mismatch** | Deposit to wrong address | Cannot auto-detect | Manual reconciliation |
| **Wrong amount deposited** | Amount differs from expected | Depends on provider handling | May need manual intervention |

### 5. Idempotency Strategy

#### Transaction Creation Idempotency

```typescript
// Before creating any provider resource, check if it already exists
async function createTransaction(...) {
  // Check for existing payout
  const existingPayoutId = (intent.metadata as any)?.tazapay_payout_id;
  if (existingPayoutId) {
    const payout = await client.getPayout(existingPayoutId);
    return _mapPayoutToResult(payout);
  }

  // Check for existing checkout
  const existingCheckoutId = (intent.metadata as any)?.tazapay_checkout_id;
  if (existingCheckoutId) {
    const checkout = await client.getCheckout(existingCheckoutId);
    return _mapCheckoutToResult(checkout);
  }

  // Create new transaction...
}
```

#### API Request Idempotency

```typescript
// Use Idempotency-Key header for all POST requests
async function _post<T>(endpoint: string, body: any, idempotencyKey?: string): Promise<T> {
  const headers = {
    'Authorization': this._getAuthHeader(),
    'Content-Type': 'application/json',
    ...(idempotencyKey && { 'Idempotency-Key': idempotencyKey }),
  };

  return this._request('POST', endpoint, body, headers);
}

// Generate idempotency keys from intent + operation
function _generateIdempotencyKey(intentId: string, operation: string): string {
  return `${intentId}-${operation}-${Date.now()}`.slice(0, 255);
}

// Usage in createPayout
const idempotencyKey = _generateIdempotencyKey(intent.id, 'create-payout');
const payout = await client.createPayout(request, idempotencyKey);
```

#### Webhook Idempotency

```typescript
async function handle(handleContext, pluginContext): Promise<void> {
  const { webhookEvent } = handleContext;
  const eventId = webhookEvent.eventData?.id;

  // Check if already processed (Tazapay event IDs are unique)
  const existing = await pluginContext.prisma.webhookEvent.findFirst({
    where: {
      eventType: { startsWith: 'tazapay.' },
      eventData: { path: ['id'], equals: eventId },
      status: 'COMPLETED',
    },
  });

  if (existing) {
    pluginContext.logger.info('Duplicate webhook event, skipping', { eventId });
    return;
  }

  // Process webhook...
}
```

#### Retry Safety

| Operation | Idempotency Method | Key Format |
|-----------|-------------------|------------|
| Create Entity | Check CustomerProvider.providerReferenceId | N/A (check before create) |
| Create Beneficiary | Check intent.metadata.tazapay_beneficiary_id | N/A (check before create) |
| Create Payout | Idempotency-Key header | `{intentId}-payout-{timestamp}` |
| Create Checkout | Idempotency-Key header | `{intentId}-checkout-{timestamp}` |
| Create Quote | No idempotency needed | N/A (quotes are cheap, short-lived) |
| Webhook Handle | Check webhookEvent by eventId | Provider event ID |

---

## === TEST SPECIFICATIONS ===

### 6. Mock Server Test Scenarios

#### Endpoints to Mock

| Endpoint | Method | Mock Scenarios |
|----------|--------|----------------|
| `/v3/entity` | POST | Success (201), Duplicate (409), Validation error (422) |
| `/v3/entity/{id}` | GET | Found (200), Not found (404) |
| `/v3/entity/{id}/submit` | POST | Success (200), Already submitted (400) |
| `/v3/customer` | POST | Success (201), Duplicate (409) |
| `/v3/customer/{id}` | GET | Found (200), Not found (404) |
| `/v3/beneficiary` | POST | Success (201), Validation error (422), Verification required |
| `/v3/beneficiary/{id}` | GET | Found (200), Not found (404) |
| `/v3/payout/quote` | POST | Success (200), Invalid params (400), Amount limits (422) |
| `/v3/payout` | POST | Success (201), Quote expired (422), Insufficient balance (422) |
| `/v3/payout/{id}` | GET | All status variants |
| `/v3/checkout` | POST | Success (201), Validation error (422) |
| `/v3/checkout/{id}` | GET | All status variants |
| `/v3/balance` | GET | Success with balances (200) |
| `/v3/collection_account` | GET | Success with accounts (200) |

#### Mock Response Examples

```typescript
// mock-server/fixtures/entity-responses.ts
export const entityApproved = {
  status: 'success',
  data: {
    id: 'ent_test_123',
    name: 'Test Company',
    type: 'company',
    approval_status: 'approved',
    created_at: '2024-01-01T00:00:00Z',
  },
};

export const entityPending = {
  status: 'success',
  data: {
    id: 'ent_test_456',
    approval_status: 'pending',
    pending_requirements: ['business_registration', 'id_document'],
  },
};

export const payoutSucceeded = {
  id: 'pot_test_789',
  object: 'payout',
  amount: 10000,
  currency: 'USD',
  status: 'succeeded',
  beneficiary: 'bnf_test_001',
  tracking_details: {
    tracking_number: 'UETR123456',
    tracking_type: 'uetr',
  },
  created_at: '2024-01-01T00:00:00Z',
};
```

### 7. Plugin Test Scenarios

#### Unit Tests per Handler

**tazapay-create-entity.ts**
- [ ] Creates entity when CustomerProvider doesn't exist
- [ ] Returns existing entity when CustomerProvider has entityId and status is approved
- [ ] Throws error when existing entity is rejected
- [ ] Handles API timeout with retry
- [ ] Handles duplicate entity error gracefully
- [ ] Stores entityId in intent.metadata and CustomerProvider

**tazapay-verify-entity.ts**
- [ ] Returns success when entity is approved
- [ ] Throws error when entity is pending (ACTION_REQUIRED)
- [ ] Throws error when entity is rejected with reason
- [ ] Throws error when entity is still under review
- [ ] Updates CustomerProvider.onboardingData on status change

**webhook-module.ts (authenticate)**
- [ ] Returns intentId for valid signature
- [ ] Returns null for invalid signature
- [ ] Returns null for missing signature header
- [ ] Returns null for expired timestamp (>10 min)
- [ ] Returns null when intent not found

**webhook-module.ts (handle)**
- [ ] Updates MoneyMovementTransaction to SUCCESS on payout.succeeded
- [ ] Updates MoneyMovementTransaction to FAILED on payout.failed
- [ ] Updates CustomerProvider.onboardingData on entity.approved
- [ ] Handles collect.succeeded and triggers payout
- [ ] Skips duplicate events (idempotent)

**transaction.ts (offramp)**
- [ ] Creates beneficiary if not exists
- [ ] Reuses existing beneficiary from intent.metadata
- [ ] Creates quote and payout
- [ ] Returns crypto deposit instructions
- [ ] Handles quote expiry by creating new quote
- [ ] Handles insufficient balance gracefully
- [ ] Idempotent: returns existing payout if already created

**transaction.ts (onramp)**
- [ ] Creates checkout session
- [ ] Returns hosted checkout URL
- [ ] Handles checkout expiry
- [ ] Idempotent: returns existing checkout if already created

#### Integration Tests for Full Flows

**Offramp Flow (USDC -> USD bank)**
1. Create intent with crypto source, bank destination
2. Run compliance flow (entity creation, verification)
3. Execute createTransaction
4. Verify deposit instructions returned
5. Simulate collect.succeeded webhook
6. Simulate payout.succeeded webhook
7. Verify MoneyMovementTransaction status is SUCCESS

**Onramp Flow (USD card -> USDC wallet)**
1. Create intent with fiat source, crypto destination
2. Run compliance flow (customer creation)
3. Execute createTransaction
4. Verify checkout URL returned
5. Simulate payin.succeeded webhook
6. Verify wallet payout triggered
7. Simulate payout.succeeded webhook
8. Verify MoneyMovementTransaction status is SUCCESS

**KYB Rejection Flow**
1. Create intent
2. Run compliance flow
3. Simulate entity.rejected webhook
4. Verify step marked as FAILED
5. Verify CustomerProvider.onboardingData updated with rejection reason

---

## === IMPLEMENTATION ORDER ===

### 8. Dependency Graph

```
                    ┌──────────────────┐
                    │     types.ts     │
                    │   interfaces.ts  │
                    └────────┬─────────┘
                             │
                    ┌────────▼─────────┐
                    │    config.ts     │
                    │credential.schema │
                    └────────┬─────────┘
                             │
                    ┌────────▼─────────┐
                    │  tazapay.api.    │
                    │    service.ts    │
                    └────────┬─────────┘
                             │
              ┌──────────────┼──────────────┐
              │              │              │
     ┌────────▼───────┐ ┌────▼────┐ ┌───────▼───────┐
     │   handlers/    │ │manifest │ │  webhook-     │
     │   *.ts         │ │  .ts    │ │  module.ts    │
     └────────┬───────┘ └────┬────┘ └───────┬───────┘
              │              │              │
              └──────────────┼──────────────┘
                             │
                    ┌────────▼─────────┐
                    │   transaction.ts │
                    │ money-movement/  │
                    └────────┬─────────┘
                             │
                    ┌────────▼─────────┐
                    │   dashboard/     │
                    │   components     │
                    └────────┬─────────┘
                             │
                    ┌────────▼─────────┐
                    │     index.ts     │
                    │   (main export)  │
                    └──────────────────┘
```

### Implementation Phases

#### Phase 1: Foundation (client layer)
1. `client/interfaces.ts` - All TypeScript types
2. `client/config.ts` - Constants, base URLs
3. `client/credential.schema.ts` - Credential form
4. `client/tazapay.api.service.ts` - API client

#### Phase 2: Compliance (server handlers)
5. `api-server/types.ts` - Server-side types
6. `api-server/handlers/index.ts` - Handler infrastructure
7. `api-server/handlers/tazapay-create-entity.ts`
8. `api-server/handlers/tazapay-verify-entity.ts`
9. `api-server/handlers/tazapay-create-customer.ts`
10. `api-server/handlers/tazapay-create-beneficiary.ts`

#### Phase 3: Webhooks
11. `api-server/webhook-module.ts` - Full webhook handling

#### Phase 4: Money Movement
12. `money-movement/index.ts` - Capability matching
13. `money-movement/transaction.ts` - Transaction execution

#### Phase 5: Integration
14. `manifest.ts` - Plugin manifest
15. `index.ts` - Main exports

#### Phase 6: Dashboard (optional, can be deferred)
16. `dashboard/handlers/index.ts`
17. `dashboard/handlers/TazapayOnboarding.tsx`
18. `dashboard/handlers/tazapay-onboarding.ts`
19. `dashboard/index.ts`

#### Phase 7: Testing
20. Mock server setup
21. Unit tests
22. Integration tests

---

## Appendix: Currency Pairs to Implement

Based on USER_INPUT.md, the following currency pairs will be implemented:

### Onramp (Fiat -> Crypto)

| Source (Fiat) | Destination (Crypto) | Transfer Type |
|---------------|---------------------|---------------|
| USD | USDC_ETH, USDC_POLYGON, USDC_TRON, USDC_SOLANA | Card, ACH |
| USD | USDT_ETH, USDT_POLYGON, USDT_TRON, USDT_SOLANA | Card, ACH |
| SGD | USDC_*, USDT_* | Card, PayNow |
| EUR | USDC_*, USDT_* | Card, SEPA |
| INR | USDC_*, USDT_* | Card, UPI, IMPS |
| BRL | USDC_*, USDT_* | PIX |
| PHP | USDC_*, USDT_* | InstaPay |
| THB | USDC_*, USDT_* | PromptPay |
| MXN | USDC_*, USDT_* | SPEI |
| VND | USDC_*, USDT_* | Bank push |

### Offramp (Crypto -> Fiat)

| Source (Crypto) | Destination (Fiat) | Transfer Type |
|-----------------|-------------------|---------------|
| USDC_ETH, USDC_POLYGON, USDC_TRON, USDC_SOLANA | USD | ACH, SWIFT |
| USDT_ETH, USDT_POLYGON, USDT_TRON, USDT_SOLANA | USD | ACH, SWIFT |
| USDC_*, USDT_* | SGD | FAST, SWIFT |
| USDC_*, USDT_* | EUR | SEPA, SWIFT |
| USDC_*, USDT_* | INR | RTGS, IMPS |
| USDC_*, USDT_* | BRL | PIX |
| USDC_*, USDT_* | PHP | InstaPay, PESONet |
| USDC_*, USDT_* | THB | PromptPay |
| USDC_*, USDT_* | MXN | SPEI |
| USDC_*, USDT_* | VND | NAPAS |

Total: ~80 currency pair combinations (9 fiat x 8 crypto + 8 crypto x 9 fiat)

---

PHASE_3_COMPLETE