---
name: symph-pay
description: "SymphPay payment integration for Symph Agency apps. Use when: payment integration, checkout, Stripe, PayMongo, price errors, hydration issues with payment buttons, checkout sessions, webhooks, merchant creation. Keywords: price ×100, meta object, STRIPE integration, payment redirect, checkout flow."
metadata:
  openclaw:
    emoji: "💳"
---

# SymphPay Integration

Payment processing for Symph Agency apps via the unified SymphPay gateway.

## Overview

SymphPay is Symph's internal payment gateway that wraps Stripe and PayMongo with a unified API.

| Environment | Base URL | Admin API Key |
|-------------|----------|---------------|
| Staging/Test | `https://symph-pay-test.df.r.appspot.com/` | `INTERNAL_ADMIN` |
| Production | `https://symph-pay.df.r.appspot.com/` | `R227Nnt5U` |

**Wiki:** https://github.com/symphco/symph-pay/wiki

## Subscription API (Staging Only)

⚠️ **Currently only available in staging** (`symph-pay-test.df.r.appspot.com`)

SymphPay supports recurring subscriptions. See wiki for full details:
https://github.com/symphco/symph-pay/wiki/Subscription-API

*TODO: Ralph to share full Subscription API docs for integration here*

---

## Integration Flow

### 1. Create Merchant (One-time per app)

```bash
curl -X POST "${BASE_URL}/api/admin/merchant" \
  -H "Content-Type: application/json" \
  -H "x-api-key: ${ADMIN_API_KEY}" \
  -d '{
    "name": "My Business",
    "brandLogo": "https://example.com/logo.png",
    "brandColorScheme": ["#000000", "#FFFFFF"],
    "successUrl": "https://example.com/success",
    "failureUrl": "https://example.com/failure",
    "cancelUrl": "https://example.com/cancel",
    "webhookUrl": "https://example.com/webhooks/symph-pay"
  }'
```

| Field | Required | Description |
|-------|----------|-------------|
| `name` | Yes | Display name of your business |
| `brandLogo` | Yes | URL to your brand logo |
| `brandColorScheme` | Yes | Array of hex color codes |
| `successUrl` | Yes | Redirect after successful payment |
| `failureUrl` | Yes | Redirect after failed payment |
| `cancelUrl` | Yes | Redirect after cancelled payment |
| `webhookUrl` | Yes | URL to receive payment webhooks |

Response contains `apiKey` and `secretKey` — save these for the app.

### 2. Configure App Environment

```bash
# .env.local
SYMPH_PAY_BASE_URL=https://symph-pay-test.df.r.appspot.com/
SYMPH_PAY_API_KEY={from merchant creation}
SYMPH_PAY_SECRET_KEY={from merchant creation}
```

### 3. Create Product

**Endpoint:** `POST /api/admin/product`
**Auth:** Merchant API key (not admin key)

```bash
curl -X POST "${BASE_URL}/api/admin/product" \
  -H "Content-Type: application/json" \
  -H "x-api-key: ${MERCHANT_API_KEY}" \
  -d '{
    "title": "Premium Plan",
    "description": "Product description",
    "price": 150,
    "currency": "PHP",
    "active": true,
    "productType": "one_time",
    "meta": {}
  }'
```

| Field | Required | Description |
|-------|----------|-------------|
| `title` | Yes | Display name of the product |
| `description` | Yes | Detailed description |
| `price` | Yes | Price in whole currency units (150 = ₱150) |
| `currency` | Yes | Three-letter code (PHP, USD) |
| `active` | Yes | Whether product is available |
| `productType` | Yes | `one_time` or `subscription` |
| `meta` | No | Custom metadata object |
| `interval` | No | For subscriptions: `day`, `week`, `month`, `year` |
| `cycleCount` | No | For subscriptions: number of billing cycles |

**Response includes:** `id`, `stripeProductId` — use `id` for checkout.

## CRITICAL Learnings

### Price Units

**Use whole currency units (not centavos/cents).**

```javascript
// CORRECT
{ "price": 150 }   // ₱150.00
{ "price": 599 }   // ₱599.00

// WRONG
{ "price": 15000 } // Would charge ₱15,000!
```

> **Note:** Wiki says "smallest currency unit" but in practice, use whole PHP amounts.
> Always verify with a test transaction before going live.

### `meta` Must Be an Object

```javascript
// CORRECT
{ "meta": {} }

// WRONG  
{ "meta": null }  // API error
// Also wrong:
// (omitting meta entirely)
```

### Use `integration: "STRIPE"` for Checkouts

Per Ralph's instruction — always specify STRIPE for checkout sessions:

```javascript
const response = await fetch(`${BASE_URL}/api/merchant/checkout`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': API_KEY
  },
  body: JSON.stringify({
    productId: product.id,
    successUrl: `${APP_URL}/success`,
    cancelUrl: `${APP_URL}/cancel`,
    integration: "STRIPE"  // Always specify
  })
});
```

### Native `<button>` for Payment CTAs

**shadcn Button has hydration issues on static pages.**

```tsx
// WRONG - causes hydration mismatch
<Button onClick={handleCheckout}>Pay Now</Button>

// CORRECT - native button works
<button 
  onClick={handleCheckout}
  className="bg-primary text-white px-4 py-2 rounded"
>
  Pay Now
</button>
```

### Payment Redirects

Use `window.location.assign()` instead of `.href =` for cross-origin redirects:

```javascript
// CORRECT
window.location.assign(checkoutUrl);

// Less reliable
window.location.href = checkoutUrl;
```

### Auto-Expire Sessions

Checkout sessions should auto-expire after 15 minutes:

```bash
curl -X POST "${BASE_URL}/api/admin/expire-session/${txCode}" \
  -H "x-api-key: ${ADMIN_API_KEY}"
```

### One Merchant Per App

**Never share SymphPay credentials between apps.** Each app gets its own merchant for:
- Revenue tracking
- Webhook isolation
- Credential rotation

## Webhook Setup

```typescript
// app/api/webhooks/symph-pay/route.ts
import { NextResponse } from 'next/server';
import crypto from 'crypto';

export async function POST(request: Request) {
  const body = await request.text();
  const signature = request.headers.get('x-symph-signature');
  
  // Verify signature
  const expected = crypto
    .createHmac('sha256', process.env.SYMPH_PAY_SECRET_KEY!)
    .update(body)
    .digest('hex');
    
  if (signature !== expected) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }
  
  const event = JSON.parse(body);
  
  switch (event.type) {
    case 'checkout.completed':
      // Handle successful payment
      await handlePaymentSuccess(event.data);
      break;
    case 'checkout.failed':
      // Handle failed payment
      await handlePaymentFailed(event.data);
      break;
  }
  
  return NextResponse.json({ received: true });
}
```

## Payment Methods

| Gateway | Methods |
|---------|---------|
| PayMongo | card, gcash, paymaya, grab_pay |
| Stripe | card, link |

## Test Cards

- **Stripe:** https://docs.stripe.com/testing
- **PayMongo:** https://developers.paymongo.com/docs/testing

## DEV vs PROD Isolation

| Aspect | DEV | PROD |
|--------|-----|------|
| Base URL | `symph-pay-test` | `symph-pay` |
| Merchant | Test merchant | Production merchant |
| Cards | Test cards only | Real cards |
| Webhooks | Dev URL | Prod URL |

**Never mix DEV and PROD credentials!**

## Full Integration Guide

See: `agency-app-starter/docs/symph-pay-integration.md`

## Quick Checklist

- [ ] Create merchant via admin API
- [ ] Add env vars to `.env.local`
- [ ] Create products via merchant API
- [ ] Implement checkout endpoint
- [ ] Set up webhook handler
- [ ] Use native `<button>` for CTAs
- [ ] Use `integration: "STRIPE"`
- [ ] Test with test cards
- [ ] Verify webhook signatures
