---
title: "Ko-fi Paper Dynasty Workflow"
description: "Complete setup guide for the n8n workflow that processes Ko-fi shop orders and grants Paper Dynasty packs. Covers webhook config, multi-method team identification, product mapping, API integration, Discord notifications, and node-by-node workflow configuration."
type: guide
domain: productivity
tags: [n8n, ko-fi, paper-dynasty, webhook, discord, automation, e-commerce]
---

# Ko-fi → Paper Dynasty Integration

Automated workflow that processes Ko-fi shop orders and grants Paper Dynasty packs to users.

## Overview

When a user purchases packs on Ko-fi, this workflow:
1. Receives the webhook from Ko-fi
2. Identifies the Paper Dynasty team (via discord_userid or team abbrev)
3. Maps Ko-fi products to Paper Dynasty pack types
4. Grants packs via Paper Dynasty API
5. Sends Discord notifications on success/failure

## Architecture

```
┌──────────────────────────────────────────────────────────────┐
│                  Ko-fi Shop Order Flow                        │
└──────────────────────────────────────────────────────────────┘

Ko-fi Shop Order
    ↓
Ko-fi Webhook POST
    ↓
https://n8n.manticorum.com/webhook/kofi-paperdy
    ↓
┌─────────────────────────────────────────────────────────────┐
│ n8n Workflow: Ko-fi to Paper Dynasty                         │
│                                                              │
│  1. Parse Ko-fi payload                                     │
│  2. Validate verification token                             │
│  3. Identify Paper Dynasty team:                            │
│     • If discord_userid → Lookup by gmid                    │
│     • Else extract team abbrev from message                 │
│  4. Map Ko-fi products → PD packs                           │
│  5. Grant packs via PD API                                  │
│  6. Send Discord notification                               │
└─────────────────────────────────────────────────────────────┘
    ↓
Paper Dynasty Database Updated
    +
Discord Notification Sent
```

## Prerequisites

### 1. Ko-fi Configuration

**Get Verification Token:**
1. Login to Ko-fi: https://ko-fi.com/
2. Navigate to: Settings → API → Webhooks → Advanced
3. Copy your **Verification Token** (UUID format)
4. Save securely for n8n credentials setup

**Configure Webhook URL:**
1. Ko-fi → Settings → API → Webhooks
2. Set Webhook URL: `https://n8n.manticorum.com/webhook/kofi-paperdy`
3. (Initially use `/webhook-test/` for testing)
4. Enable webhook
5. Test using Ko-fi's webhook tester

### 2. Paper Dynasty API

**API Key:**
- Production API: `https://pd.manticorum.com/api/v2/`
- Required header: `Authorization: Bearer YOUR_API_KEY`
- Store API key securely for n8n credentials

**Required Endpoints:**
- `GET /teams?gmid={discord_userid}` - Lookup team by Discord user ID
- `GET /teams?abbrev={team_abbrev}` - Lookup team by abbreviation
- `POST /packs` - Grant packs to team

### 3. Discord Notifications

**Webhook URL:**
1. Discord channel → Settings → Integrations → Webhooks
2. Create webhook for Ko-fi notifications
3. Copy webhook URL
4. Save for n8n credentials setup

## n8n Credentials Setup

Navigate to n8n → Settings → Credentials and create:

### 1. Ko-fi Verification Token

- **Type:** Generic Credential
- **Name:** `Ko-fi Verification Token`
- **Field:** `verification_token`
- **Value:** `[your-kofi-verification-token-uuid]`

### 2. Paper Dynasty API Key

- **Type:** Header Auth
- **Name:** `Paper Dynasty API`
- **Header Name:** `Authorization`
- **Value:** `Bearer [your-pd-api-key]`

### 3. Discord Webhook

- **Type:** Discord Webhook (or HTTP Header Auth)
- **Name:** `Discord Ko-fi Notifications`
- **Webhook URL:** `https://discord.com/api/webhooks/...`

## Product Mapping Configuration

Create n8n custom variable for product mapping.

**Navigate to:** n8n → Settings → Variables

**Variable Name:** `KOFI_PRODUCT_MAP`

**Value (JSON):**
```json
{
  "pack-standard-5": {
    "name": "5-Pack Standard",
    "pack_type_id": 1,
    "quantity": 5,
    "pack_cardset_id": null
  },
  "pack-premium-10": {
    "name": "10-Pack Premium",
    "pack_type_id": 3,
    "quantity": 10,
    "pack_cardset_id": null
  },
  "pack-team-choice": {
    "name": "Team Choice Pack",
    "pack_type_id": 8,
    "quantity": 1,
    "pack_cardset_id": 27
  }
}
```

**Product Code Convention:**
- Use Ko-fi's `direct_link_code` field as key
- Set this in Ko-fi product settings
- Format: `pack-{tier}-{quantity}` or `pack-{special-name}`
- Examples: `pack-standard-5`, `pack-premium-10`, `pack-legendary-single`

## Workflow Node Configuration

### Node 1: Webhook Trigger

**Node Type:** Webhook
**Name:** Ko-fi Webhook Trigger

**Settings:**
- **HTTP Method:** POST
- **Path:** `kofi-paperdy`
- **Authentication:** None (we validate token manually)
- **Response Mode:** When Last Node Finishes
- **Response Code:** 200

**Creates URL:** `https://n8n.manticorum.com/webhook/kofi-paperdy`

---

### Node 2: Parse Ko-fi Data

**Node Type:** Code
**Name:** Parse Ko-fi Form Data
**Mode:** Run Once for All Items

**Code:**
```javascript
// Ko-fi sends application/x-www-form-urlencoded
// The 'data' field contains JSON string
const formData = $input.item.json.body;

// Parse JSON from data field
const kofiData = JSON.parse(formData.data);

// Add metadata
return [{
  json: {
    ...kofiData,
    _parsed_at: new Date().toISOString(),
    _execution_id: $execution.id
  }
}];
```

---

### Node 3: Validate Token

**Node Type:** IF
**Name:** Validate Ko-fi Token

**Condition:**
- **Type:** String
- **Value 1:** `{{ $json.verification_token }}`
- **Operation:** Equal
- **Value 2:** `{{ $credentials.kofiVerificationToken.verification_token }}`

**Routing:**
- **True:** Continue to Node 4
- **False:** Connect to "Stop and Error" node

**False Path - Stop and Error Node:**
- **Error Message:** `Invalid Ko-fi verification token`

---

### Node 4: Check Transaction Type

**Node Type:** IF
**Name:** Filter Shop Orders

**Condition:**
- **Type:** String
- **Value 1:** `{{ $json.type }}`
- **Operation:** Equal
- **Value 2:** `Shop Order`

**Routing:**
- **True:** Continue to Node 5
- **False:** Connect to "Respond to Webhook" with message "Not a shop order"

---

### Node 5: Identify Paper Dynasty Team

**Node Type:** Code
**Name:** Lookup Paper Dynasty Team
**Mode:** Run Once for All Items

**Code:**
```javascript
// Multi-method team identification:
// 1. discord_userid → lookup by gmid
// 2. message field → extract team abbrev

const PD_API_BASE = 'https://pd.manticorum.com/api/v2';
const API_TOKEN = '{{ $credentials.paperDynastyApi.headerAuth.value }}';

let teamId = null;
let teamAbbrev = null;
let identificationMethod = null;

// Method 1: Discord User ID → gmid lookup
if ($json.discord_userid) {
  try {
    const response = await $http.request({
      method: 'GET',
      url: `${PD_API_BASE}/teams?gmid=${$json.discord_userid}`,
      headers: {
        'Authorization': API_TOKEN,
        'Content-Type': 'application/json'
      }
    });

    if (response.body && response.body.length > 0) {
      teamId = response.body[0].id;
      teamAbbrev = response.body[0].abbrev;
      identificationMethod = 'discord_userid';
    }
  } catch (error) {
    console.log('Discord user ID lookup failed:', error.message);
  }
}

// Method 2: Extract team abbrev from message field
if (!teamId && $json.message) {
  // Look for team abbreviations (typically 3 uppercase letters)
  // Match patterns like: "CAR", "SKB", "PPD", etc.
  const abbrevMatch = $json.message.match(/\b([A-Z]{2,4})\b/);

  if (abbrevMatch) {
    const extractedAbbrev = abbrevMatch[1];

    try {
      const response = await $http.request({
        method: 'GET',
        url: `${PD_API_BASE}/teams?abbrev=${extractedAbbrev}`,
        headers: {
          'Authorization': API_TOKEN,
          'Content-Type': 'application/json'
        }
      });

      if (response.body && response.body.length > 0) {
        teamId = response.body[0].id;
        teamAbbrev = response.body[0].abbrev;
        identificationMethod = 'message_abbrev';
      }
    } catch (error) {
      console.log('Team abbrev lookup failed:', error.message);
    }
  }
}

// Return result
return [{
  json: {
    ...$json,
    pd_team_id: teamId,
    pd_team_abbrev: teamAbbrev,
    identification_method: identificationMethod,
    needs_manual_review: !teamId
  }
}];
```

---

### Node 6: Route by Identification Status

**Node Type:** IF
**Name:** Check Team Identified

**Condition:**
- **Type:** Boolean
- **Value 1:** `{{ $json.needs_manual_review }}`
- **Operation:** Equal
- **Value 2:** `false`

**Routing:**
- **True:** Continue to Node 7 (process order)
- **False:** Connect to "Manual Review" Discord notification

**False Path - Discord Notification:**
- **Message:** Manual review needed (see Discord node config below)

---

### Node 7: Map Ko-fi Products

**Node Type:** Code
**Name:** Map Products to PD Packs
**Mode:** Run Once for All Items

**Code:**
```javascript
// Load product mapping
const productMap = JSON.parse($vars.KOFI_PRODUCT_MAP);
const shopItems = $json.shop_items;

// Process each shop item
const packRequests = [];
let unknownProducts = [];

for (const item of shopItems) {
  const productConfig = productMap[item.direct_link_code];

  if (!productConfig) {
    // Unknown product code
    unknownProducts.push({
      code: item.direct_link_code,
      variation: item.variation_name,
      quantity: item.quantity
    });
    continue;
  }

  // Create pack request for each quantity
  for (let i = 0; i < item.quantity; i++) {
    packRequests.push({
      team_id: $json.pd_team_id,
      pack_type_id: productConfig.pack_type_id,
      pack_cardset_id: productConfig.pack_cardset_id
    });
  }
}

return [{
  json: {
    ...$json,
    pack_requests: packRequests,
    unknown_products: unknownProducts,
    has_unknown_products: unknownProducts.length > 0,
    total_packs: packRequests.length
  }
}];
```

---

### Node 8: Grant Packs via API

**Node Type:** HTTP Request
**Name:** Paper Dynasty Grant Packs

**Settings:**
- **Method:** POST
- **URL:** `https://pd.manticorum.com/api/v2/packs`
- **Authentication:** Use Credential → Select "Paper Dynasty API"
- **Body Content Type:** JSON
- **Continue on Fail:** ✅ Enabled
- **Retry on Fail:** ✅ Enabled
- **Max Tries:** 3
- **Wait Between Tries:** 2000ms
- **Timeout:** 10000ms

**Body (JSON):**
```json
{
  "packs": {{ JSON.stringify($json.pack_requests) }}
}
```

**Example:**
```json
{
  "packs": [
    {"team_id": 69, "pack_type_id": 1, "pack_cardset_id": null},
    {"team_id": 69, "pack_type_id": 1, "pack_cardset_id": null},
    {"team_id": 69, "pack_type_id": 3, "pack_cardset_id": null}
  ]
}
```

---

### Node 9: Check API Response

**Node Type:** IF
**Name:** API Success Check

**Condition:**
- **Type:** Number
- **Value 1:** `{{ $json.statusCode }}`
- **Operation:** Between
- **Value 2:** `200`
- **Value 3:** `299`

**Routing:**
- **True:** Success Discord notification
- **False:** Error Discord notification

---

### Node 10a: Success Notification

**Node Type:** Discord
**Name:** Success Notification

**Credential:** Discord Ko-fi Notifications

**Message (JSON):**
```json
{
  "content": "✅ Ko-fi Order Processed",
  "embeds": [{
    "title": "Paper Dynasty Packs Granted",
    "color": 3066993,
    "fields": [
      {
        "name": "Customer",
        "value": "{{ $('Parse Ko-fi Form Data').item.json.from_name }}",
        "inline": true
      },
      {
        "name": "Team",
        "value": "{{ $('Lookup Paper Dynasty Team').item.json.pd_team_abbrev }}",
        "inline": true
      },
      {
        "name": "Amount",
        "value": "${{ $('Parse Ko-fi Form Data').item.json.amount }} {{ $('Parse Ko-fi Form Data').item.json.currency }}",
        "inline": true
      },
      {
        "name": "Packs Granted",
        "value": "{{ $('Map Products to PD Packs').item.json.total_packs }} packs",
        "inline": false
      },
      {
        "name": "Products",
        "value": "{{ $('Parse Ko-fi Form Data').item.json.shop_items.map(i => `${i.quantity}x ${i.variation_name}`).join('\\n') }}",
        "inline": false
      },
      {
        "name": "Transaction ID",
        "value": "`{{ $('Parse Ko-fi Form Data').item.json.kofi_transaction_id }}`",
        "inline": false
      }
    ],
    "timestamp": "{{ new Date().toISOString() }}"
  }]
}
```

---

### Node 10b: Error Notification

**Node Type:** Discord
**Name:** Error Notification

**Credential:** Discord Ko-fi Notifications

**Message (JSON):**
```json
{
  "content": "@here ⚠️ Ko-fi Order Processing Error",
  "embeds": [{
    "title": "Paper Dynasty API Error",
    "color": 15158332,
    "fields": [
      {
        "name": "Customer",
        "value": "{{ $('Parse Ko-fi Form Data').item.json.from_name }} ({{ $('Parse Ko-fi Form Data').item.json.email }})",
        "inline": false
      },
      {
        "name": "Amount",
        "value": "${{ $('Parse Ko-fi Form Data').item.json.amount }} {{ $('Parse Ko-fi Form Data').item.json.currency }}",
        "inline": true
      },
      {
        "name": "Team ID",
        "value": "{{ $('Lookup Paper Dynasty Team').item.json.pd_team_id || 'Unknown' }}",
        "inline": true
      },
      {
        "name": "Products",
        "value": "{{ $('Parse Ko-fi Form Data').item.json.shop_items.map(i => `${i.quantity}x ${i.variation_name}`).join('\\n') }}",
        "inline": false
      },
      {
        "name": "API Status",
        "value": "{{ $('Paper Dynasty Grant Packs').item.json.statusCode || 'Request Failed' }}",
        "inline": true
      },
      {
        "name": "Error Message",
        "value": "```{{ $('Paper Dynasty Grant Packs').item.json.body?.error || 'Unknown error' }}```",
        "inline": false
      },
      {
        "name": "Transaction ID",
        "value": "`{{ $('Parse Ko-fi Form Data').item.json.kofi_transaction_id }}`",
        "inline": false
      },
      {
        "name": "Action Required",
        "value": "Manual pack distribution needed. Use transaction ID to process manually.",
        "inline": false
      }
    ],
    "timestamp": "{{ new Date().toISOString() }}"
  }]
}
```

---

### Node 10c: Manual Review Notification

**Node Type:** Discord
**Name:** Manual Review Needed

**Credential:** Discord Ko-fi Notifications

**Message (JSON):**
```json
{
  "content": "@here 🔍 Ko-fi Order Needs Manual Review",
  "embeds": [{
    "title": "Unable to Identify Paper Dynasty Team",
    "color": 16776960,
    "fields": [
      {
        "name": "Customer",
        "value": "{{ $json.from_name }} ({{ $json.email }})",
        "inline": false
      },
      {
        "name": "Amount",
        "value": "${{ $json.amount }} {{ $json.currency }}",
        "inline": true
      },
      {
        "name": "Discord User ID",
        "value": "{{ $json.discord_userid || 'Not provided' }}",
        "inline": true
      },
      {
        "name": "Message",
        "value": "{{ $json.message || 'No message' }}",
        "inline": false
      },
      {
        "name": "Products",
        "value": "{{ $json.shop_items.map(i => `${i.quantity}x ${i.variation_name} (${i.direct_link_code})`).join('\\n') }}",
        "inline": false
      },
      {
        "name": "Transaction ID",
        "value": "`{{ $json.kofi_transaction_id }}`",
        "inline": false
      },
      {
        "name": "Action Required",
        "value": "1. Identify user's Paper Dynasty team\n2. Manually grant packs using Paper Dynasty API\n3. Reply to this message when resolved",
        "inline": false
      }
    ],
    "timestamp": "{{ new Date().toISOString() }}"
  }]
}
```

---

### Node 11: Respond to Webhook

**Node Type:** Respond to Webhook
**Name:** Return 200 OK

**Settings:**
- **Response Code:** 200
- **Response Body:** JSON

**Body:**
```json
{
  "status": "success",
  "message": "Order received and processed",
  "execution_id": "{{ $execution.id }}"
}
```

**Note:** This node must execute after all processing to ensure Ko-fi receives the 200 status code.

## Workflow Visual Structure

```
[Webhook Trigger]
    ↓
[Parse Ko-fi Data]
    ↓
[Validate Token] → (false) → [Stop and Error]
    ↓ (true)
[Filter Shop Orders] → (false) → [Respond: Not shop order]
    ↓ (true)
[Lookup PD Team]
    ↓
[Check Team Identified]
    ├─ (false) → [Discord: Manual Review] → [Respond to Webhook]
    ↓ (true)
[Map Products to PD Packs]
    ↓
[PD API: Grant Packs]
    ↓
[API Success Check]
    ├─ (true) → [Discord: Success]
    └─ (false) → [Discord: Error]
    ↓
[Respond to Webhook: 200 OK]
```

## Testing

### Phase 1: Development Testing

**Use Test Webhook Path:**
- Test URL: `https://n8n.manticorum.com/webhook-test/kofi-paperdy`
- This won't trigger production workflows
- View test executions in n8n UI

**Test with cURL:**
```bash
curl -X POST 'https://n8n.manticorum.com/webhook-test/kofi-paperdy' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'data={
    "verification_token": "YOUR_TOKEN_HERE",
    "message_id": "test-001",
    "timestamp": "2025-11-13T12:00:00Z",
    "type": "Shop Order",
    "is_public": true,
    "from_name": "Test Customer",
    "message": "Team: SKB",
    "amount": "10.00",
    "url": "https://ko-fi.com/test",
    "email": "[email protected]",
    "currency": "USD",
    "kofi_transaction_id": "test-txn-001",
    "shop_items": [
      {
        "direct_link_code": "pack-standard-5",
        "variation_name": "5-Pack Standard",
        "quantity": 1
      }
    ],
    "discord_userid": "012345678901234567"
  }'
```

### Phase 2: Ko-fi Test Webhook

**Configure Ko-fi:**
1. Ko-fi → API → Webhooks
2. Set URL: `https://n8n.manticorum.com/webhook-test/kofi-paperdy`
3. Use Ko-fi's "Test Webhook" button
4. Verify n8n receives and processes correctly

### Phase 3: Test Purchases

**Create Test Products:**
1. Ko-fi → Shop → Add Product
2. Set product code (e.g., `pack-test-1`)
3. Price: $0.01 (minimum)
4. Make test purchase
5. Verify workflow processes correctly
6. Check Discord for notification
7. Verify packs granted in Paper Dynasty

### Phase 4: Production Deployment

**Switch to Production:**
1. Update webhook path in workflow from `/webhook-test/` to `/webhook/`
2. Update Ko-fi webhook URL to production path
3. Activate workflow in n8n
4. Monitor first 10 real transactions closely
5. Verify all Discord notifications working
6. Check Paper Dynasty pack grants are correct

## Troubleshooting

### Webhook Not Triggering

**Check:**
- Workflow is Active (toggle in n8n)
- Webhook path matches Ko-fi configuration
- n8n is accessible from internet
- NPM proxy host is running

**Test:**
```bash
curl -I https://n8n.manticorum.com/webhook/kofi-paperdy
# Should return: HTTP/1.1 200 OK or 404 (workflow not active)
```

### Invalid Token Error

**Check:**
- Token in n8n credentials matches Ko-fi dashboard
- No extra whitespace in token
- Credential name referenced correctly in IF node

**Fix:**
- Re-copy token from Ko-fi
- Update n8n credential
- Test workflow again

### Team Not Found

**Causes:**
- Discord user ID not linked to Paper Dynasty account
- Team abbrev not in message field
- Typo in team abbreviation

**Resolution:**
- Manual review Discord notification sent
- Admin manually grants packs
- Update user's PD profile with correct gmid

### API Call Failing

**Check:**
- API key is valid
- Paper Dynasty API is online
- Request payload format is correct
- Team ID exists in database

**Debug:**
- View full API response in n8n execution log
- Test API call manually with curl
- Check Paper Dynasty API logs

### Packs Not Granted

**Verify:**
- API returned 200 status
- pack_type_id is valid
- team_id exists
- Correct number of packs in request

**Check in Paper Dynasty:**
```bash
# Using Paper Dynasty API
curl -H "Authorization: Bearer YOUR_KEY" \
  "https://pd.manticorum.com/api/v2/packs?team_id=69&opened=false&limit=20"
```

## Monitoring

### n8n Execution Logs

**View Executions:**
1. n8n → Executions tab
2. Filter by workflow: "Ko-fi to Paper Dynasty"
3. Review successful and failed executions
4. Click execution to see node-by-node data flow

### Discord Notifications

**Monitor Channel:**
- All successful orders → success notification
- All errors → error notification with @here mention
- Manual reviews → notification with customer details

### Paper Dynasty Verification

**Verify Packs Granted:**
```python
from api_client import PaperDynastyAPI

api = PaperDynastyAPI(environment='prod')

# Check recent packs
recent_packs = api.list_packs(opened=False, new_to_old=True, limit=50)

# Filter for Ko-fi source (if transaction_id stored)
kofi_packs = [p for p in recent_packs if 'kofi' in str(p.get('metadata', {}))]
```

## Maintenance

### Weekly Tasks

- [ ] Review failed executions in n8n
- [ ] Check for unknown product codes
- [ ] Verify all manual review items resolved
- [ ] Monitor API response times

### Monthly Tasks

- [ ] Update product mapping for new Ko-fi products
- [ ] Review and optimize workflow performance
- [ ] Backup n8n database
- [ ] Test disaster recovery procedure

### Quarterly Tasks

- [ ] Rotate Ko-fi verification token
- [ ] Rotate Paper Dynasty API key
- [ ] Review workflow for optimization opportunities
- [ ] Update documentation with lessons learned

## References

- **Ko-fi Webhook Docs:** https://help.ko-fi.com/hc/en-us/articles/360004162298
- **Paper Dynasty API:** `/home/cal/.claude/skills/paper-dynasty/SKILL.md`
- **n8n Documentation:** https://docs.n8n.io/
- **n8n Webhook Node:** https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/

## Change Log

### 2025-11-13 - Initial Design
- Created workflow specification
- Defined multi-method team identification
- Configured product mapping structure
- Designed error handling and manual review process
