This is the core merchant flow — create an invoice, wait for payment, fulfill when the webhook fires.
Install the SDK
npm install gwop-checkout
TypeScript types included. Zero runtime dependencies.
Set your environment variables
GWOP_CHECKOUT_API_KEY=sk_m_... # from Dashboard → Settings
GWOP_WEBHOOK_SECRET=whsec_... # from Dashboard → Settings
Initialize the client
import { GwopCheckout } from 'gwop-checkout';
const gwop = new GwopCheckout({
merchantApiKey: process.env.GWOP_CHECKOUT_API_KEY,
});
Create an invoice
const invoice = await gwop.invoices.create(
{
amount_usdc: 5_000_000, // $5.00 USDC (6 decimal places)
description: 'Pro plan — 1 month',
metadata: { customer_id: 'cust_123' },
},
{ idempotencyKey: 'order_abc_v1' }, // safe to retry
);
console.log(invoice.id); // 'inv_...'
console.log(invoice.public_invoice_id); // public-facing ID
console.log(invoice.status); // 'OPEN'
console.log(invoice.expires_at); // ISO 8601 timestamp
What happens behind the scenes
When you create an invoice, Gwop:
- Generates x402 payment endpoints on Base and Solana. Any x402-compatible agent can pay without custom integration.
- Starts the expiry clock. Default TTL is 15 minutes. Configurable via
expires_in_seconds (60–86400).
- Returns the invoice with a status of
OPEN, ready for payment.
Create options
| Field | Type | Required | Description |
|---|
amount_usdc | number | Yes | Amount in USDC minor units. 1_000_000 = $1.00. |
description | string | No | Human-readable label (max 500 chars). |
metadata | object | No | Arbitrary key-value pairs (max 1024 bytes). |
metadata_public | boolean | No | Expose metadata in the public invoice view. |
expires_in_seconds | number | No | Invoice TTL in seconds (60–86400). Default: 900. |
Idempotency
Pass an idempotencyKey to safely retry failed requests. If the same key is sent again, Gwop returns the original invoice instead of creating a duplicate.
const invoice = await gwop.invoices.create(
{ amount_usdc: 10_000_000 },
{ idempotencyKey: 'order_xyz_v1' },
);
Handle the webhook
When payment settles on-chain, Gwop sends a signed invoice.paid event to your webhook URL.
import express from 'express';
import { GwopCheckout } from 'gwop-checkout';
const gwop = new GwopCheckout({
merchantApiKey: process.env.GWOP_CHECKOUT_API_KEY,
});
const app = express();
app.post(
'/webhooks/gwop',
express.raw({ type: 'application/json' }),
async (req, res) => {
const event = gwop.webhooks.constructEvent(
req.body.toString('utf8'),
req.header('x-gwop-signature'),
process.env.GWOP_WEBHOOK_SECRET!, // whsec_...
);
await gwop.webhooks.dispatch(event, {
invoicePaid: async (evt) => {
// evt.data.invoice_id — the invoice that was paid
// evt.data.tx_hash — on-chain transaction hash
// evt.data.payment_chain — 'solana' | 'base'
await fulfillOrder(evt.data.invoice_id);
},
invoiceExpired: async (evt) => {
await cancelPendingOrder(evt.data.invoice_id);
},
});
res.json({ ok: true });
},
);
Pass the raw body bytes to constructEvent. If JSON parsing runs before signature verification, the signature check will fail. Use express.raw() as shown above.
Invoice lifecycle
Once created, an invoice moves through these statuses:
| Status | Terminal | Meaning |
|---|
OPEN | No | Awaiting payment. |
PAYING | No | Payment in progress — do not create a new invoice. |
PAID | Yes | Settlement confirmed on-chain. |
EXPIRED | Yes | TTL reached before payment. |
CANCELED | Yes | Canceled by merchant via gwop.invoices.cancel(). |
Retrieve an invoice
Check the current state of any invoice. This is a public endpoint — no API key required.
const invoice = await gwop.invoices.retrieve('inv_abc123');
console.log(invoice.status); // 'OPEN' | 'PAYING' | 'PAID' | ...
console.log(invoice.payment_methods); // x402 payment options per chain
Poll for payment (alternative to webhooks)
If you can’t receive webhooks, poll with waitForStatus:
const paid = await gwop.invoices.waitForStatus('inv_abc123', 'PAID', {
timeoutMs: 60_000,
intervalMs: 1_000,
});
console.log(paid.paid_tx_hash); // on-chain transaction hash
Uses exponential backoff with jitter. Throws WAIT_TIMEOUT if the timeout is reached.
Next steps