Payments API
Create one-time payment links from your own app or website. Customers pay in USDT, USDC or SOL on the Solana network, straight to your wallet. No fees on either side.
Get started
- Open your SOLKEEP wallet and choose Developers.
- Select Generate API key and store the key on your server. It is shown only once.
- Optionally add a webhook endpoint to hear about payments the moment they are confirmed.
Base URL: https://solkeep.app/api/v1. Every request needs your key in the Authorization header. Keep keys on your server; requests from browsers on other sites are refused. Each wallet has one key. Regenerating it stops the old one immediately.
Authorization: Bearer sk_live_...Create a payment link
POST /checkouts with a JSON body.
tokenrequired —USDT,USDCorSOL.amountrequired — decimal string with up to 6 decimals, for example"25.50". Minimum 5 USDT, 5 USDC or 0.001 SOL.labeloptional — note shown to the payer, up to 80 characters. Use it for your order number.
curl -X POST https://solkeep.app/api/v1/checkouts \
-H "Authorization: Bearer $SOLKEEP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"token":"USDC","amount":"25.00","label":"Order 1042"}'Returns 201 with the checkout. Send your customer to checkout.url. The link stays open for 20 minutes.
{
"checkout": {
"id": "7tQm2cY9qHn4WvB1xLkD8e",
"url": "https://solkeep.app/pay/7tQm2cY9qHn4WvB1xLkD8e",
"address": "<your wallet address>",
"token": "USDC",
"amount": "25.000000",
"amountRaw": "25000000",
"reference": "<unique payment reference>",
"label": "Order 1042",
"status": "pending",
"createdAt": 1793000000000,
"expiresAt": 1793001200000,
"source": "api"
},
"usedToday": 3,
"dailyLimit": 50
}Always ask the payer for checkout.amount, not the amount you sent. When another open link from your wallet already asks for the same amount, the new one adds up to 0.000999 so the payments can be told apart.
Retrieve, list and cancel
GET /checkouts/:id— one checkout with its latest status. Checking it also looks for the payment.GET /checkouts— your 50 most recent checkouts, newest first, plususedTodayanddailyLimit.POST /checkouts/:id/cancel— close an open link. Only pending checkouts can be canceled.
Statuses
pending— waiting for payment.paid— the full amount arrived.paidAt,paidSignatureandpayerare set.underpaid— a smaller amount arrived from a wallet that scanned the link;receivedshows how much.expired— no payment within the time limit. Payments sent in time are still picked up for 3 minutes after expiry.canceled— you closed the link.
Webhooks
Add an HTTPS endpoint in Developers. When a checkout leaves pending we send a POST with one of these types: checkout.paid, checkout.underpaid, checkout.expired, checkout.canceled. The test button sends webhook.test.
{
"id": "evt_5f0c...",
"type": "checkout.paid",
"created": 1793000420,
"data": { "checkout": { "id": "7tQm2cY9qHn4WvB1xLkD8e", "status": "paid", ... } }
}Reply with any 2xx within 5 seconds. Failed deliveries are retried after 1, 5 and 15 minutes, then 1, 3 and 6 hours. A retry can repeat an event, so use data.checkout.id and type to ignore duplicates, and confirm with GET /checkouts/:id before shipping an order.
Every delivery carries a Solkeep-Signature header: t=<unix seconds>,v1=<signature>, where the signature is an HMAC-SHA256 of t.body using your signing secret.
import { createHmac, timingSafeEqual } from 'node:crypto'
// rawBody: the request body exactly as received, before JSON parsing.
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(',').map((part) => part.split('=')),
)
const expected = createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex')
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300
return (
fresh &&
expected.length === parts.v1?.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
)
}Limits and errors
Each wallet can create 50 payment links per day (UTC) by default, shared between the wallet and the API. Your current limit is returned as dailyLimit. Need more? Contact us. Requests are limited to 120 per minute. Errors return JSON with error and code.
400 invalid_request— check token, amount or note.401 unauthorized— missing, revoked or wrong key.404 not_found— no such checkout for this key.429 daily_limitorrate_limited— try again later.503 unavailable— temporary problem; retry with backoff.
Questions about fees or payment links? See the FAQ.