8.9 KiB
NOWPayments IPN Listener
A TypeScript Express application for receiving and processing Instant Payment Notifications (IPN) from NOWPayments.
Features
- ✅ Secure IPN signature validation using HMAC SHA512
- ✅ TypeScript for type safety
- ✅ Handles all NOWPayments payment statuses
- ✅ MySQL/MariaDB database integration
- ✅ Automatic payment processing: moves finished/confirmed payments from
pending_orderstosales - ✅ 10-minute reservation mechanism to prevent overselling
- ✅ Expiration checking and automatic cleanup of expired orders
- ✅ Final inventory validation before creating sales
- ✅ Transaction-safe database operations
- ✅ Idempotent IPN processing (handles duplicate callbacks)
- ✅ Error handling and logging
- ✅ Automatic email receipts sent to buyers upon purchase confirmation
Setup
1. Install Dependencies
npm install
2. Configure Environment Variables
Copy .env.example to .env and fill in your NOWPayments IPN Secret Key:
cp .env.example .env
Edit .env and configure:
NOWPayments IPN Secret Key:
- Go to NOWPayments Dashboard → Store Settings → IPN Secret Key
- Generate a new key if you don't have one
- Add it to the
.envfile
Database Configuration:
- Set your MySQL/MariaDB connection details:
DB_HOST- Database host (default: localhost)DB_PORT- Database port (default: 3306)DB_USER- Database usernameDB_PASSWORD- Database passwordDB_NAME- Database name (default: cbd420)
SMTP Configuration (for email receipts):
- Configure SMTP settings for sending order confirmation emails:
SMTP_HOST- SMTP server hostname (e.g., smtp.gmail.com, smtp.mailgun.org)SMTP_PORT- SMTP server port (e.g., 587 for STARTTLS, 465 for SSL, 25 for unencrypted)- Recommended: 587 (STARTTLS) - most reliable and commonly not blocked
- Port 465 (SSL) may be blocked by some firewalls
SMTP_USER- SMTP username/emailSMTP_PASSWORD- SMTP password or app-specific passwordSMTP_FROM_EMAIL- Email address to send from (must match SMTP_USER for most providers)SMTP_FROM_NAME- Display name for sender (optional, default: "CBD420")
SMTP Troubleshooting:
- If you get connection timeouts, try switching from port 465 to 587
- Ensure your firewall allows outbound connections to the SMTP server
- Some providers require app-specific passwords (e.g., Gmail)
- Test SMTP connectivity:
telnet SMTP_HOST SMTP_PORTor usenc -zv SMTP_HOST SMTP_PORT
3. Database Migration
The application requires the expires_at column in the pending_orders table for the 10-minute reservation mechanism. Run the migration:
mysql -u your_user -p your_database < migrations/add_expires_at_to_pending_orders.sql
Or manually add the column:
ALTER TABLE `pending_orders`
ADD COLUMN `expires_at` datetime NOT NULL DEFAULT (DATE_ADD(NOW(), INTERVAL 10 MINUTE))
AFTER `created_at`;
ALTER TABLE `pending_orders`
ADD INDEX `idx_expires_at` (`expires_at`);
4. Build the Project
npm run build
5. Run the Server
Development mode (with auto-reload):
npm run dev
Production mode:
npm start
The server will start on port 3000 (or the port specified in your .env file).
Endpoints
POST /ipn
Receives IPN notifications from NOWPayments. This endpoint:
- Validates the request signature
- Processes payment status updates
- Returns 200 OK to acknowledge receipt
GET /health
Health check endpoint to verify the server is running.
IPN Callback URL Setup
When creating a payment via the NOWPayments API, include the ipn_callback_url parameter:
{
"price_amount": 100,
"price_currency": "usd",
"pay_currency": "btc",
"ipn_callback_url": "https://yourdomain.com/ipn",
// ... other parameters
}
Payment Statuses
The listener handles the following payment statuses:
waiting- Payment is waitingconfirming- Payment is being confirmed on blockchainconfirmed- Payment confirmed on blockchainsending- Payment is being sentpartially_paid- Payment partially receivedfinished- Payment completed successfullyfailed- Payment failedrefunded- Payment refundedexpired- Payment expired
Database Integration
The application implements a 10-minute reservation mechanism to prevent race conditions when multiple buyers attempt to purchase the last available units simultaneously.
Payment Processing Flow
When a payment status is finished or confirmed, the system:
- Find Pending Order - Looks up the pending order by
payment_idorinvoice_id - Check Expiration - Verifies the pending order hasn't expired (10-minute window)
- Validate Payment Status - Processes based on status:
finishedorconfirmed→ Proceed to create salefailedorexpired→ Delete pending orderwaiting,confirming→ Acknowledge and wait
- Final Inventory Check - Validates inventory is still available before creating sale
- Create Sale Record - Inserts into
salestable and deletes frompending_orders - Send Email Receipt - Sends order confirmation email to buyer (after transaction commit)
All operations are performed within a database transaction to ensure data consistency and prevent race conditions.
Database Schema
The application expects the following tables (as defined in cbd420(1).sql + migration):
-
pending_orders- Stores pending payment orders with 10-minute reservationspayment_id(unique) - NOWPayments payment/invoice IDorder_id(unique) - Internal order ID (format: SALE-{timestamp}-{drop_id}-{buyer_id})drop_id- Reference to drops tablebuyer_id- Reference to buyers tablesize- Order size (in grams)price_amount- Payment amountprice_currency- Payment currency (default: 'chf')created_at- Order creation timestampexpires_at- Expiration timestamp (10 minutes from creation) - REQUIRED
-
sales- Stores completed salesdrop_id- Reference to drops tablebuyer_id- Reference to buyers tablesize- Sale size (in grams)payment_id- NOWPayments payment ID (matches pending_orders.payment_id)created_at- Sale creation timestamp
-
drops- Product drop informationid- Drop IDsize- Available sizeunit- Unit of measurement ('g' or 'kg')
Inventory Calculation
Available inventory is calculated as:
Available = drop.size - (SUM(sales.size) + SUM(pending_orders.size WHERE expires_at > NOW()))
The system automatically handles unit conversion (kg to grams) when calculating inventory.
Key Features
- Expiration Handling: Pending orders expire after 10 minutes and are automatically excluded from inventory calculations
- Inventory Validation: Final inventory check ensures no overselling occurs between payment initiation and completion
- Idempotency: IPN callbacks can be processed multiple times safely (checks for existing sales)
- Transaction Safety: All database operations use transactions to ensure atomicity
IPN Callback Processing
The IPN handler follows this flow for each callback:
- Find Pending Order - Searches by
payment_idorinvoice_id(tries both) - Check Expiration - Verifies
expires_at > NOW()- expired orders are rejected - Payment Status Processing:
finishedorconfirmed→ Creates sale (after inventory check)failedorexpired→ Deletes pending order- Other statuses → Acknowledged, waiting for final status
- Final Inventory Check - Re-validates inventory before creating sale (within transaction)
- Create Sale - Atomically creates sale and deletes pending order
Important Notes
- IPN callbacks may be sent multiple times - the system handles this with idempotency checks
- Expired pending orders are automatically excluded from inventory calculations
- Always returns HTTP 200 to NOWPayments (even on errors) to prevent retries
- Inventory is checked within a database transaction to prevent race conditions
Security
- All IPN requests are validated using HMAC SHA512 signature verification
- Invalid signatures are rejected with 400 Bad Request
- The IPN secret key should never be committed to version control
- Database transactions ensure data consistency and prevent race conditions
Testing
You can test the IPN endpoint using tools like:
- Postman
- curl
- NOWPayments test payments
Example curl command (with test signature):
curl -X POST http://localhost:3000/ipn \
-H "Content-Type: application/json" \
-H "x-nowpayments-sig: your_signature_here" \
-d '{"payment_id":"test123","payment_status":"waiting","price_amount":100,"price_currency":"usd"}'
Documentation
For detailed implementation guide and database schema, refer to the IPN Callback Integration Guide provided with this application.
License
ISC