npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

payload-plugin-bold

v0.1.0-beta.1

Published

Bold payment button integration for Payload CMS

Readme

payload-plugin-bold

Bold payment button integration for Payload CMS. Adds a transactions collection, three endpoints, and headless React components, so a host app only supplies its Bold keys, a pricing function, and a button.

Built for Bold (Colombia). Requires Payload 3.

Install

pnpm add payload-plugin-bold
import { boldPlugin } from 'payload-plugin-bold'

export default buildConfig({
  plugins: [
    boldPlugin({
      identityKey: process.env.BOLD_IDENTITY_KEY!,
      secretKey: process.env.BOLD_SECRET_KEY!,
      testMode: process.env.NODE_ENV !== 'production',
      redirectionUrl: 'https://mytienda.com/resultado',
      relatedCollections: ['orders'],
      resolveAmount: async ({ input, req }) => {
        const order = await req.payload.findByID({
          collection: 'orders',
          id: input.reference!,
        })

        return { amount: order.total, description: `Orden ${order.reference}` }
      },
      statusMapping: [
        {
          collection: 'orders',
          field: 'status',
          values: {
            APPROVED: 'paid',
            REJECTED: 'payment_failed',
            VOIDED: 'refunded',
          },
        },
      ],
    }),
  ],
})
'use client'
import { BoldPayButton } from 'payload-plugin-bold/client'

export const Checkout = ({ orderId }: { orderId: string }) => (
  <BoldPayButton
    checkout={{ reference: orderId, relationTo: 'orders' }}
    className="rounded-md bg-black px-5 py-3 text-white"
  >
    Pagar con Bold
  </BoldPayButton>
)
'use client'
import { useBoldReturn } from 'payload-plugin-bold/client'

export const Resultado = () => {
  const { status, final, loading } = useBoldReturn()

  if (loading) return <p>Confirmando el pago…</p>

  return <p>{final && status === 'APPROVED' ? '¡Pago aprobado!' : status}</p>
}

Then register https://<your-site>/api/bold/webhooks in the Bold panel under Integraciones → Webhooks.

What the plugin adds

A bold-transactions collection. Every checkout creates a row and every Bold event updates it. All fields are read-only in the admin, and writes are denied through the REST/GraphQL API — only the plugin's own endpoints mutate it.

Three endpoints:

| Endpoint | Purpose | |---|---| | POST /api/bold/checkout | Prices the order server-side, creates the transaction, signs it, returns the BoldCheckout config | | POST /api/bold/webhooks | Verifies x-bold-signature, deduplicates, updates the transaction and the linked document | | GET /api/bold/status/:orderId | Reconciles against Bold's voucher API and returns a normalized status |

Client exports from payload-plugin-bold/client: BoldPayButton, useBoldCheckout, useBoldReturn, loadBoldScript. They are unstyled and framework-agnostic — the script tag is injected on demand, so there is no Next.js dependency and nothing to add to your layout.

How orders are linked

The plugin never adds a column to your collections. The transaction owns a polymorphic relatedDocument relationship pointing at whatever you list in relatedCollections, and your collections get a virtual join field (boldTransactions by default) so the payments show up on the order's admin page and in its API response.

relatedCollections: ['orders', 'subscriptions'],
joinField: 'boldTransactions',

Set joinField: false to skip the join entirely.

Because bold-transactions defaults to authenticated-read, the join field is omitted from unauthenticated API responses. That is intentional. Widen it with collectionOverrides if your storefront needs it.

Configuration

| Option | Type | Default | Description | |---|---|---|---| | identityKey * | string | — | Bold identity key (public) | | secretKey * | string | — | Bold secret key (server-only) | | testMode | boolean | false | Uses Bold's test environment semantics | | resolveAmount | function | — | Prices the order on the server. Required unless allowClientAmount | | allowClientAmount | boolean | false | Trusts a client-supplied amount. See the security note | | relatedCollections | string[] | [] | Collections a transaction may link to | | joinField | string \| false | 'boldTransactions' | Name of the virtual join field | | statusMapping | array | — | Maps Bold statuses onto a field of a linked document | | onApproved / onRejected / onVoided / onFailed | function | — | Fired once per real status change | | onStatusChange | function | — | Fired on every real status change | | redirectionUrl | string | — | Where Bold returns the customer | | renderMode | 'embedded' \| 'redirect' | 'redirect' | embedded opens Bold in a modal | | defaultCurrency | 'COP' \| 'USD' | 'COP' | | | collectionSlug | string | 'bold-transactions' | | | collectionOverrides | function | — | Transforms the generated collection | | fields | function | — | Transforms the generated fields | | generateOrderId | function | — | Custom order id generator | | access | object | — | Guards the checkout and status endpoints | | routes | object | — | Overrides endpoint paths | | webhookSecret | string | derived | Overrides the webhook HMAC secret | | logs | boolean | false | Logs lifecycle transitions | | disabled | boolean | false | Registers the collection but no endpoints |

* required

Security notes

Amounts are priced on the server. The client sends a reference, never a price. resolveAmount looks up the real amount and only then is the integrity signature generated. allowClientAmount: true exists for donation-style flows where any amount is acceptable — do not enable it for fixed-price carts, because a signature over a client-supplied number certifies whatever the client chose to send.

The secret key never reaches the browser. Only the identity key and the finished signature are returned to the client. The signature is SHA256(orderId + amount + currency + secretKey), computed in the endpoint.

Webhooks are verified before they are trusted, using HMAC-SHA256(base64(rawBody), secretKey) compared against x-bold-signature in constant time. In test mode Bold signs with an empty secret, which the plugin handles automatically.

Order ids are unguessable. GET /api/bold/status/:orderId is public by default so a returning customer can confirm their payment before logging in; the order id embeds 10 random characters from a 62-character alphabet (~59 bits) and acts as a bearer credential. Pass access.status to require authentication instead.

Status handling

Bold reports outcomes through two independent channels, and the plugin treats both as first-class:

  • Webhooks arrive server-to-server and are authoritative.
  • The voucher API is polled when the customer returns, because Bold does not fire webhooks for simulated test-mode transactions. Without this path an integration appears to work in test and silently does nothing in production.

Both funnel through the same transition table, so they cannot disagree:

| From | May move to | |---|---| | PENDING | PROCESSING, APPROVED, REJECTED, FAILED, VOIDED | | PROCESSING | APPROVED, REJECTED, FAILED, VOIDED | | APPROVED | VOIDED | | REJECTED, FAILED, VOIDED | nothing |

This makes the integration safe against Bold's retry policy (up to 5 redeliveries over 24 hours) and against out-of-order delivery: a replayed event is ignored by its CloudEvents id, a late SALE_REJECTED cannot undo an approved sale, and NO_TRANSACTION_FOUND never overwrites a real status.

Note that VOID_REJECTED means the void failed, so the sale stays APPROVED.

Bold constraints enforced

  • COP and USD only; minimum 1000 COP
  • Amounts must be integers with no decimal separators
  • Order ids are at most 60 characters of A-Za-z0-9_-
  • Taxes accept vat-5, vat-19, iac-8 or a numeric value, and must already be included in the amount

Local development

pnpm install
cp .env.example dev/.env   # add your Bold test keys
pnpm dev                   # sandbox at http://localhost:3000
pnpm test

The sandbox seeds an order, renders a checkout page, and handles the Bold return at /resultado. Bold's test cards: 4111111111111111 approves, 4970110000000062 rejects.

Because Bold does not deliver webhooks for simulated transactions, use the panel's "Probar el webhook" button, or post a signed event yourself — in test mode the HMAC secret is the empty string.

License

MIT