{"id":31589432,"url":"https://github.com/maseranw/payfast-subscribe","last_synced_at":"2026-01-20T17:01:44.002Z","repository":{"id":315877687,"uuid":"1012581453","full_name":"maseranw/payfast-subscribe","owner":"maseranw","description":"Express.js router for PayFast subscriptions — supports ITN, cancel, pause/unpause, and full billing lifecycle for South African apps.","archived":false,"fork":false,"pushed_at":"2025-09-13T15:46:20.000Z","size":53,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-10-06T02:55:18.047Z","etag":null,"topics":["api","backend","expressjs","nodejs","payfast","payments"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/@ngelekanyo/payfast-subscribe","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/maseranw.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-07-02T14:50:13.000Z","updated_at":"2025-07-06T14:44:47.000Z","dependencies_parsed_at":"2025-09-21T11:36:40.472Z","dependency_job_id":"9103e30f-fb39-4d44-bff6-0249d94033b4","html_url":"https://github.com/maseranw/payfast-subscribe","commit_stats":null,"previous_names":["maseranw/payfast-subscribe"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/maseranw/payfast-subscribe","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maseranw%2Fpayfast-subscribe","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maseranw%2Fpayfast-subscribe/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maseranw%2Fpayfast-subscribe/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maseranw%2Fpayfast-subscribe/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/maseranw","download_url":"https://codeload.github.com/maseranw/payfast-subscribe/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maseranw%2Fpayfast-subscribe/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28607624,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-20T16:10:39.856Z","status":"ssl_error","status_checked_at":"2026-01-20T16:10:39.493Z","response_time":117,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","backend","expressjs","nodejs","payfast","payments"],"created_at":"2025-10-06T02:52:48.956Z","updated_at":"2026-01-20T17:01:43.981Z","avatar_url":"https://github.com/maseranw.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n# PayFast Subscription Integration (Node.js/Express)\n\nThis package provides a modular Express router to integrate with [PayFast](https://www.payfast.co.za/) for managing subscription payments, including:\n\n- Submitting subscription payment forms to PayFast\n- Handling ITN (Instant Transaction Notification) webhooks\n- Cancelling active subscriptions via PayFast's API\n- Callback hooks for custom payment and cancellation handling\n\n## ✅ Features\n\n- 🔐 Signature generation for secure PayFast communication\n- 🔄 Subscription initiation and recurring billing setup\n- 📬 ITN webhook handling with signature + source validation\n- ❌ Cancel PayFast subscriptions with retry logic\n- 🔄 Fetch PayFast subscriptions\n- 🔄 Pause PayFast subscriptions\n- 🔄 UnPause PayFast subscriptions\n- 📦 Clean, pluggable Express router with optional callbacks\n\n## 📋 Requirements\n\n- Node.js 14+\n- Express 4+\n\n## 📦 Installation\n\nInstall from npm:\n\n```bash\nnpm install @ngelekanyo/payfast-subscribe\n```\n\n## ⚙️ Environment Setup\n\nCreate a `.env` file in your root directory:\n\n```env\nPAYFAST_MERCHANT_ID=your_merchant_id\nPAYFAST_MERCHANT_KEY=your_merchant_key\nPAYFAST_PASSPHRASE=your_passphrase\nPAYFAST_API_VERSION=v1\nPAYFAST_RETURN_URL=https://yourdomain.com/payment-success\nPAYFAST_CANCEL_URL=https://yourdomain.com/payment-cancel\nPAYFAST_NOTIFY_URL=https://yourdomain.com/api/payfast/notify\nTESTING_MODE=true\n```\n\n## 🚀 Usage\n\nIn your Express server:\n\n```js\nconst express = require(\"express\");\nconst cors = require(\"cors\");\nconst buildPayfastRouter = require(\"@ngelekanyo/payfast-subscribe\");\n\nconst app = express();\n\nconst handlePaymentUpdate = async (itnData) =\u003e {\n  console.log(\"💰 Payment received:\", itnData);\n  // e.g., update database, activate subscription\n};\n\nconst handleCancel = async ({ token, subscriptionId, status, payload }) =\u003e {\n  console.log(\"❌ Cancel callback called:\", {\n    token,\n    subscriptionId,\n    status,\n    payload,\n  });\n  // e.g., mark subscription as cancelled in your system\n};\n\nconst handlePause = async ({ token, status, payload }) =\u003e {\n  console.log(\"⏸️ Pause callback called:\", {\n    token,\n    status,\n    payload,\n  });\n  // e.g., mark subscription as paused in your system\n};\n\nconst handleUnpause = async ({ token, status, payload }) =\u003e {\n  console.log(\"▶️ Unpause callback called:\", {\n    token,\n    status,\n    payload,\n  });\n  // e.g., resume subscription in your system\n};\n\nconst handleFetch = async ({ token, status, payload }) =\u003e {\n  console.log(\"📄 Fetch callback called:\", {\n    token,\n    status,\n    payload,\n  });\n  // e.g., update subscription status from fetch data\n};\n\napp.use(cors());\napp.use(express.json());\napp.use(express.urlencoded({ extended: true }));\n\napp.use(\n  \"/api/payfast\",\n  buildPayfastRouter(\n    handlePaymentUpdate,\n    handleCancel,\n    handlePause,\n    handleUnpause,\n    handleFetch\n  )\n);\n\napp.listen(6000, () =\u003e console.log(\"Server running on http://localhost:6000\"));\n```\n\n# 🔗 PayFast Integration with External Backend\n\nThis section demonstrates how to integrate your `@ngelekanyo/payfast-subscribe` backend with a client-side or external application using a `PayFastService` class.\n\n\u003e ⚙️ This example assumes your backend is running at `http://localhost:6000` or a configured `VITE_BACKEND_URL`.\n\nYou can copy the code below into a file (e.g., `payfast-service.ts`) and use it in your project.\n\n---\n\n### 🟦 TypeScript Example\n\n```ts\n// PayFast integration with external backend\nconst BACKEND_URL = import.meta.env.VITE_BACKEND_URL || \"http://localhost:6000\";\n\nexport interface PaymentData {\n  amount: string;\n  item_name: string;\n  item_description?: string;\n  name_first?: string;\n  name_last?: string;\n  email_address?: string;\n  m_payment_id: string;\n}\n\nexport interface PayFastResponse {\n  paymentData: Record\u003cstring, string\u003e;\n  payfastUrl: string;\n}\n\nexport class PayFastService {\n  static async initiatePayment(data: PaymentData): Promise\u003cPayFastResponse\u003e {\n    console.log(\"[PayFastService] Initiating payment with data:\", data);\n\n    try {\n      const response = await fetch(`${BACKEND_URL}/api/payfast/initiate`, {\n        method: \"POST\",\n        headers: {\n          \"Content-Type\": \"application/json\",\n        },\n        body: JSON.stringify(data),\n      });\n\n      if (!response.ok) {\n        const error = await response.json();\n        console.error(\"[PayFastService] Error response:\", error);\n        throw new Error(error.error || \"Failed to initiate payment\");\n      }\n\n      const responseData = await response.json();\n      return responseData;\n    } catch (err) {\n      console.error(\"[PayFastService] Failed to initiate payment:\", err);\n      throw err;\n    }\n  }\n\n  static async cancelSubscription(\n    token: string,\n    subscriptionId: string\n  ): Promise\u003cvoid\u003e {\n    try {\n      const response = await fetch(\n        `${BACKEND_URL}/api/payfast/cancel/${token}/${subscriptionId}`,\n        {\n          method: \"POST\",\n          headers: {\n            \"Content-Type\": \"application/json\",\n          },\n        }\n      );\n\n      if (!response.ok) {\n        const error = await response.json();\n        console.error(\"[PayFastService] Cancel error response:\", error);\n        throw new Error(error.error || \"Failed to cancel subscription\");\n      }\n\n      const responseData = await response.json();\n      console.log(\n        \"[PayFastService] Subscription cancelled successfully:\",\n        responseData\n      );\n    } catch (err) {\n      console.error(\"[PayFastService] Failed to cancel subscription:\", err);\n      throw err;\n    }\n  }\n\n  static createPaymentForm(\n    paymentData: Record\u003cstring, string\u003e,\n    actionUrl: string\n  ): HTMLFormElement {\n    const form = document.createElement(\"form\");\n    form.method = \"POST\";\n    form.action = actionUrl;\n    form.style.display = \"none\";\n\n    Object.entries(paymentData).forEach(([key, value]) =\u003e {\n      const input = document.createElement(\"input\");\n      input.type = \"hidden\";\n      input.name = key;\n      input.value = value;\n      form.appendChild(input);\n    });\n\n    return form;\n  }\n\n  static submitPayment(\n    paymentData: Record\u003cstring, string\u003e,\n    actionUrl: string\n  ): void {\n    const form = this.createPaymentForm(paymentData, actionUrl);\n    document.body.appendChild(form);\n    form.submit();\n  }\n}\n```\n\n---\n\n### 🚀 Usage Example\n\n```ts\nconst paymentData: PaymentData = {\n  amount: \"100.00\",\n  item_name: \"Monthly Subscription\",\n  m_payment_id: \"SUB12345\",\n};\n\nPayFastService.initiatePayment(paymentData)\n  .then((response) =\u003e {\n    console.log(\"Redirecting to PayFast:\", response.payfastUrl);\n    PayFastService.submitPayment(response.paymentData, response.payfastUrl);\n  })\n  .catch((err) =\u003e console.error(\"Payment initiation failed:\", err));\n\n// Cancel a subscription\nPayFastService.cancelSubscription(\"token123\", \"SUB12345\").catch((err) =\u003e\n  console.error(\"Cancel failed:\", err)\n);\n```\n\n## 🔌 Exposed Routes\n\n| Method | Route                                        | Description                                   |\n| ------ | -------------------------------------------- | --------------------------------------------- |\n| POST   | `/api/payfast/initiate`                      | Generate PayFast payment data + URL           |\n| POST   | `/api/payfast/notify`                        | Handle ITN (Instant Transaction Notification) |\n| POST   | `/api/payfast/cancel/:token/:subscriptionId` | Cancel an active PayFast subscription         |\n| POST   | `/api/payfast/cancel/:token`                 | Cancel an active PayFast subscription         |\n| POST   | `/api/payfast/pause/:token`                  | Pause an active subscription                   |\n| POST   | `/api/payfast/unpause/:token`                | Unpause a paused subscription                  |\n| GET    | `/api/payfast/fetch/:token`                   | Fetch subscription details                      |\n\n## 🧠 Callbacks\n\n### `onPaymentUpdate(itnPayload)`\n\nTriggered when a valid ITN notification is received from PayFast.\n\n```js\nconst onPaymentUpdate = async (payload) =\u003e {\n  // Called with parsed ITN data\n};\n```\n\n### `onCancel({ token, subscriptionId, status, payload })`\n\nCalled after a cancellation attempt. Includes final result.\n\n```js\nconst onCancel = async ({ token, subscriptionId, status, payload }) =\u003e {\n  if (status !== 200) {\n    console.error(\"Cancel failed:\", payload);\n  }\n};\n```\n\n### `onPause({ token, status, payload })`\n\nCalled after a pause attempt.\n\n```js\nconst onPause = async ({ token, status, payload }) =\u003e {\n  if (status !== 200) {\n    console.error(\"Pause failed:\", payload);\n  }\n};\n```\n\n### `onUnpause({ token, status, payload })`\n\nCalled after an unpause attempt.\n\n```js\nconst onUnpause = async ({ token, status, payload }) =\u003e {\n  if (status !== 200) {\n    console.error(\"Unpause failed:\", payload);\n  }\n};\n```\n\n### `onFetch({ token, status, payload })`\n\nCalled after fetching subscription details.\n\n```js\nconst onFetch = async ({ token, status, payload }) =\u003e {\n  if (status !== 200) {\n    console.error(\"Fetch failed:\", payload);\n  }\n};\n```\n\n## 📌 Subscription ID Explanation\n\nThe `subscriptionId` parameter in the `cancelSubscription` method refers to a unique identifier for a subscription record in **your application’s database** (e.g., a `subscriptions` table).\n\nThis ID is typically generated by your backend when a subscription is created, and is stored **alongside the PayFast `payfast_token`**.\n\n### 🧠 Context\n\nIf you use a service like **Supabase** to manage subscriptions, `subscriptionId` could simply be the `id` column of your `subscriptions` table — uniquely identifying each user’s subscription.\n\n### ✅ Usage\n\nWhen calling `PayFastService.cancelSubscription`, pass:\n\n- the **`payfast_token`** (provided by PayFast), and\n- your **local `subscriptionId`**  \n  to ensure the correct subscription is cancelled **both** on PayFast and in your own system.\n\n---\n\n## 🔧 Example Implementation (React + Supabase)\n\n```ts\nconst handleCancelSubscription = async () =\u003e {\n  if (!subscription || !subscription.payfast_token) {\n    toast.error(\"Cannot cancel subscription: missing PayFast token\");\n    return;\n  }\n\n  setActionLoading(true);\n  try {\n    // Cancel via PayFast API through backend\n    await PayFastService.cancelSubscription(\n      subscription.payfast_token,\n      subscription.id\n    );\n\n    // Mark subscription to cancel at end of period in local DB\n    const { error } = await supabase\n      .from(\"subscriptions\")\n      .update({\n        cancel_at_period_end: true,\n        updated_at: new Date().toISOString(),\n      })\n      .eq(\"id\", subscription.id)\n      .eq(\"user_id\", user!.id);\n\n    if (error) throw error;\n    toast.success(\"Subscription scheduled for cancellation at period end\");\n  } catch (err) {\n    toast.error(\"Failed to cancel subscription\");\n    console.error(\"Cancel error:\", err);\n  } finally {\n    setActionLoading(false);\n  }\n};\n```\n\n## 🛡️ Security\n\n- Validates PayFast's signature on every ITN\n- Verifies source IP matches PayFast domains\n- Uses CSRF/session token for authenticated cancellation\n- Retry logic for expired CSRF/session (e.g., HTTP 419)\n- 👉 [PayFast Developer Docs](https://developers.payfast.co.za/docs)\n\n## ⚠️ Pause/Unpause Disclaimer\n\n\u003e **Important Notice:**  \n\u003e The `pause` and `unpause` subscription features provided by this package rely on PayFast's native subscription behavior. Please review the following carefully before implementing:\n\n- Pausing a subscription **does not cancel it** — it only delays future billing by the number of paused cycles (e.g. `cycles: 1` = 1 billing interval).\n- The **subscription end date is automatically extended** by PayFast for each paused cycle.\n- **Unpausing early** (before the pause period ends) will **not adjust the next billing date** — billing still resumes after the full pause duration.\n- ⚠️ This may result in a user receiving more than a full billing cycle of access without being charged, unless you **enforce access control** on your side.\n- This package does **not automatically manage user access** during pause periods. You must implement that logic in your backend or authorization layer.\n\n📚 For accurate and up-to-date details on PayFast's pause/unpause behavior, refer to the official PayFast documentation:\n\n👉 [PayFast Developer Docs](https://developers.payfast.co.za/docs)\n\n## 🧪 Testing Tips\n\n- Use PayFast Sandbox\n- Set `TESTING_MODE=true` in `.env`\n- Use tools like Postman or Insomnia to test `/initiate`, `/notify`, `/cancel` , `/pause` , `/unpause` , `/fetch`  \n\n## ✅ TODO\n\n- Add support for once-off payments\n- Split Payments\n- Refunds\n- Integrate PayFast subscription query endpoint\n\n## 👥 Maintainers\n\n- [@ngelekanyo](https://github.com/maseranw) (author \u0026 maintainer)\n\n## 🤝 Contributing\n\nContributions, suggestions, and issues welcome!  \nPlease open an issue or submit a pull request.\n\n## 📄 License\n\nThis project is licensed under the MIT License.  \nSee the [LICENSE](./LICENSE) file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaseranw%2Fpayfast-subscribe","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmaseranw%2Fpayfast-subscribe","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaseranw%2Fpayfast-subscribe/lists"}