Accept your first payment
500000. Every currency uses 1/100 units.1Get your API keys
Sign in and open Dashboard → Developers. You will see a test secret key (sk_test_…) and, once your account is approved for live payments, a live one (sk_live_…). Test keys never move real money, so build everything with them first.
Secret keys belong on your server only. Never put them in a browser, a mobile app or a public repository. Keep them in environment variables:
DOTTSPAY_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxxxx
DOTTSPAY_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxIf you use Node, install the SDK. It has no dependencies and works on Node 18 and later.
npm install @dottspay/node2Create a checkout session
When your customer is ready to pay, create a checkout session from your server. Send an Idempotency-Key so that a retry after a timeout never creates a second session.
curl https://dottspay.hiddekellabs.com/api/v1/checkout/sessions \
-H "Authorization: Bearer $DOTTSPAY_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042" \
-d '{
"amount": 500000,
"email": "ada@example.com",
"callback_url": "https://yourshop.com/payment/callback",
"metadata": { "order_id": "ord_1042" }
}'import { DottsPay } from '@dottspay/node'
const dottspay = new DottsPay(process.env.DOTTSPAY_SECRET_KEY)
const session = await dottspay.checkout.create(
{
amount: 500000, // ₦5,000 in kobo
email: 'ada@example.com',
callback_url: 'https://yourshop.com/payment/callback',
metadata: { order_id: 'ord_1042' },
},
{ idempotencyKey: 'order-1042' },
)
console.log(session.checkout_url)DottsPay returns the session. The field you need now is checkout_url.
{
"status": true,
"message": "Checkout session created",
"data": {
"id": "cs_8Kq2LmT4xPz9WbN3vR6hYd",
"checkout_url": "https://dottspay.hiddekellabs.com/pay/c/cs_8Kq2LmT4xPz9WbN3vR6hYd",
"status": "open",
"mode": "test",
"amount": 500000,
"currency": "NGN",
"email": "ada@example.com",
"metadata": {
"order_id": "ord_1042"
},
"callback_url": "https://yourshop.com/payment/callback",
"expires_at": "2026-10-13T09:30:12.000Z",
"created_at": "2026-10-12T09:30:12.000Z"
}
}3Redirect your customer
Send your customer to checkout_url. They pick a payment method and pay on a page hosted by DottsPay, so card details never touch your server.
app.post('/checkout', async (req, res) => {
const order = await orders.create(req.body) // your own order record
const session = await dottspay.checkout.create(
{
amount: order.totalKobo,
email: order.email,
callback_url: 'https://yourshop.com/payment/callback',
metadata: { order_id: order.id },
},
{ idempotencyKey: `order-${order.id}` },
)
res.redirect(303, session.checkout_url)
})After the payment, DottsPay sends the customer back to your callback_url with two query parameters:
https://yourshop.com/payment/callback?reference=dp_Hk2pQx9LmT4sWb7NcY&status=success4Verify the payment
Call GET /transactions/{reference} with the reference from the callback. If the payment is still pending, DottsPay checks with the payment network before it answers.
curl https://dottspay.hiddekellabs.com/api/v1/transactions/dp_Hk2pQx9LmT4sWb7NcY \
-H "Authorization: Bearer $DOTTSPAY_SECRET_KEY"app.get('/payment/callback', async (req, res) => {
const { reference } = req.query
// Never trust the status in the URL. Ask DottsPay.
const tx = await dottspay.transactions.verify(reference)
const order = await orders.find(tx.metadata?.order_id)
if (tx.status === 'success' && tx.amount === order.totalKobo && tx.currency === 'NGN') {
await orders.markPaid(order.id, tx.reference) // safe to call twice
return res.redirect('/thank-you')
}
res.redirect('/payment-failed')
})Only give value when all of these are true:
statusissuccess.amountandcurrencymatch what you expected for this order.- You have not already fulfilled this reference. The callback and the webhook can both arrive, so make fulfilment safe to run twice.
See the transaction object for every field.
5Handle the webhook
Customers sometimes close the tab before the redirect finishes. A webhook makes sure you still hear about the payment. Add your endpoint URL in Dashboard → Developers → Webhooks and copy the signing secret (it starts with whsec_ and is shown once).
DottsPay sends a POST with an X-DottsPay-Signature header. Check it against the raw body before you trust anything in the payload.
import express from 'express'
import { DottsPay } from '@dottspay/node'
const app = express()
// Use the raw body for this route. The signature is computed over the exact bytes.
app.post('/webhooks/dottspay', express.raw({ type: 'application/json' }), async (req, res) => {
let event
try {
event = DottsPay.webhooks.constructEvent(
req.body,
req.get('X-DottsPay-Signature'),
process.env.DOTTSPAY_WEBHOOK_SECRET,
)
} catch {
return res.sendStatus(400)
}
res.sendStatus(200) // reply first, then do the work
if (event.event === 'charge.success') {
await orders.markPaid(event.data.metadata?.order_id, event.data.reference)
}
})The webhooks guide covers retries, every event type, and verification in PHP and plain Node.
Node SDK
The examples above use @dottspay/node. It wraps every endpoint with full TypeScript types, turns API errors into a typed DottsPayError, and verifies webhook signatures. Prefer another language? Every endpoint is plain HTTPS and JSON, so cURL examples translate directly.
Read the SDK guide.
Go live
- Finish account verification in the dashboard so live mode is switched on.
- Swap
sk_test_…forsk_live_…in your production environment. - Add a live webhook URL. Test and live have separate URLs and separate secrets.
- Make one small real payment end to end, then refund it.