Coastal Pay API
Accept crypto payments with a single API call. Send us a payment request and we return a unique wallet address and QR code. We monitor the blockchain, confirm the payment, and notify you via webhook.
2. We return a wallet address + QR code to show your customer
3. Customer sends crypto to the address
4. We detect and confirm the payment on-chain
5. We POST a webhook to your server with the result
Authentication
All API requests require a Bearer token in the Authorization header. You can generate API keys from your dashboard.
Endpoints
POST /api/v1/payments
Creates a new crypto payment and returns a unique wallet address + QR code.
Request Body
Supported Currencies
Example Request
{
"merchant_id": "merch_abc123",
"customer_id": "cust_789",
"transaction_id": "txn_456",
"amount_usd": 50.00,
"currency": "BTC",
"callback_url": "https://your-server.com/webhooks/crypto"
}
201 Created
{
"payment_id": "cpay_a1b2c3d4e5f6g7h8i9j0",
"status": "awaiting_payment",
"wallet_address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"crypto_amount": "0.000587230000000000",
"currency": "BTC",
"exchange_rate": "85150.23",
"qr_code": "data:image/png;base64,iVBOR...",
"expires_at": "2026-03-12T15:30:00.000Z",
"amount_usd": "50.00"
}
GET /api/v1/payments/:id
Returns full payment details including current status, timestamps, and wallet information.
200 OK{
"payment_id": "cpay_a1b2c3d4e5f6g7h8i9j0",
"status": "confirmed",
"merchant_id": "merch_abc123",
"transaction_id": "txn_456",
"amount_usd": "50.00",
"currency": "BTC",
"crypto_amount": "0.000587230000000000",
"exchange_rate": "85150.23",
"wallet_address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"failure_code": null,
"expires_at": "2026-03-12T15:30:00.000Z",
"confirmed_at": "2026-03-12T15:12:34.000Z",
"created_at": "2026-03-12T15:00:00.000Z"
}
GET /api/v1/payments/:id/status
Lightweight endpoint for polling payment status. Returns only essential status fields.
200 OK{
"payment_id": "cpay_a1b2c3d4e5f6g7h8i9j0",
"status": "confirmed",
"failure_code": null,
"confirmed_at": "2026-03-12T15:12:34.000Z"
}
GET /health
Health check endpoint. No authentication required.
200 OK{
"status": "ok",
"timestamp": "2026-03-12T15:00:00.000Z"
}
Payment Statuses
Every payment moves through a lifecycle. Here are the possible statuses:
| Status | Description |
|---|---|
| awaiting_payment | Payment created, waiting for customer to send crypto |
| confirmed | Payment received and confirmed on blockchain |
| expired | Payment window expired (30 minutes), no payment received |
| refunded | Payment was refunded (underpayment, error, etc.) |
| failed | Payment failed — see failure_code for reason |
Failure Codes
When a payment has status failed, the failure_code field indicates the reason:
| Code | Description |
|---|---|
| UNDERPAYMENT | Customer sent less than required amount (>2% below) |
| OVERPAYMENT | Customer sent more than required amount |
| PAYMENT_AFTER_EXPIRY | Payment received after the 30-minute window |
| NETWORK_ERROR | Blockchain network issue |
Webhooks (Callbacks)
When a payment status changes, we send a POST request to your callback_url with the payment details:
{
"payment_id": "cpay_a1b2c3d4e5f6g7h8i9j0",
"status": "confirmed",
"merchant_id": "merch_abc123",
"transaction_id": "txn_456",
"amount_usd": "50.00",
"currency": "BTC",
"crypto_amount": "0.000587230000000000",
"wallet_address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"confirmed_at": "2026-03-12T15:12:34.000Z"
}
Verifying Webhook Signatures
Every callback includes an X-Coastal-Signature header containing an HMAC-SHA256 signature of the request body. Verify it against your shared secret to confirm the webhook is authentic and hasn't been tampered with.
Retry Policy
If your server doesn't return a 2xx status code, we retry with exponential backoff:
7 attempts total before the webhook is marked as failed.
Code Examples
Complete, working examples for creating payments, checking status, and verifying webhook signatures.
const axios = require('axios'); const crypto = require('crypto'); const API_KEY = 'cpay_live_xxxxxxxxxxxxx'; const BASE_URL = 'https://api-pay.coastalai.dev'; const WEBHOOK_SECRET = 'your_webhook_secret'; const client = axios.create({ baseURL: BASE_URL, headers: { 'Authorization': `Bearer ${API_KEY}` } }); // --- Create a payment --- async function createPayment() { const { data } = await client.post('/api/v1/payments', { merchant_id: 'merch_abc123', customer_id: 'cust_789', transaction_id: 'txn_456', amount_usd: 50.00, currency: 'BTC', callback_url: 'https://your-server.com/webhooks/crypto' }); console.log('Payment created:', data.payment_id); console.log('Send', data.crypto_amount, 'BTC to:', data.wallet_address); return data; } // --- Check payment status --- async function checkStatus(paymentId) { const { data } = await client.get(`/api/v1/payments/${paymentId}/status`); console.log('Status:', data.status); return data; } // --- Verify webhook signature --- function verifyWebhook(rawBody, signature) { const expected = crypto .createHmac('sha256', WEBHOOK_SECRET) .update(rawBody) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature) ); } // Express webhook handler example app.post('/webhooks/crypto', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-coastal-signature']; if (!verifyWebhook(req.body, signature)) { return res.status(401).json({ error: 'Invalid signature' }); } const event = JSON.parse(req.body); console.log('Payment', event.payment_id, 'is now', event.status); res.status(200).json({ received: true }); });
import requests import hmac import hashlib API_KEY = "cpay_live_xxxxxxxxxxxxx" BASE_URL = "https://api-pay.coastalai.dev" WEBHOOK_SECRET = "your_webhook_secret" headers = {"Authorization": f"Bearer {API_KEY}"} # --- Create a payment --- def create_payment(): payload = { "merchant_id": "merch_abc123", "customer_id": "cust_789", "transaction_id": "txn_456", "amount_usd": 50.00, "currency": "BTC", "callback_url": "https://your-server.com/webhooks/crypto" } response = requests.post( f"{BASE_URL}/api/v1/payments", json=payload, headers=headers ) response.raise_for_status() data = response.json() print(f"Payment created: {data['payment_id']}") print(f"Send {data['crypto_amount']} BTC to: {data['wallet_address']}") return data # --- Check payment status --- def check_status(payment_id): response = requests.get( f"{BASE_URL}/api/v1/payments/{payment_id}/status", headers=headers ) response.raise_for_status() data = response.json() print(f"Status: {data['status']}") return data # --- Verify webhook signature --- def verify_webhook(raw_body: bytes, signature: str) -> bool: expected = hmac.new( WEBHOOK_SECRET.encode(), raw_body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature) # Flask webhook handler example from flask import Flask, request, jsonify app = Flask(__name__) @app.route("/webhooks/crypto", methods=["POST"]) def handle_webhook(): signature = request.headers.get("X-Coastal-Signature", "") if not verify_webhook(request.data, signature): return jsonify({"error": "Invalid signature"}), 401 event = request.get_json() print(f"Payment {event['payment_id']} is now {event['status']}") return jsonify({"received": True}), 200
<?php $apiKey = 'cpay_live_xxxxxxxxxxxxx'; $baseUrl = 'https://api-pay.coastalai.dev'; $webhookSecret = 'your_webhook_secret'; // --- Create a payment --- function createPayment() { global $apiKey, $baseUrl; $payload = json_encode([ 'merchant_id' => 'merch_abc123', 'customer_id' => 'cust_789', 'transaction_id' => 'txn_456', 'amount_usd' => 50.00, 'currency' => 'BTC', 'callback_url' => 'https://your-server.com/webhooks/crypto', ]); $ch = curl_init("$baseUrl/api/v1/payments"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer $apiKey", 'Content-Type: application/json', ], ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode !== 201) { throw new \Exception("API error: $response"); } $data = json_decode($response, true); echo "Payment created: " . $data['payment_id'] . "\n"; echo "Send " . $data['crypto_amount'] . " BTC to: " . $data['wallet_address'] . "\n"; return $data; } // --- Check payment status --- function checkStatus($paymentId) { global $apiKey, $baseUrl; $ch = curl_init("$baseUrl/api/v1/payments/$paymentId/status"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer $apiKey", ], ]); $response = curl_exec($ch); curl_close($ch); $data = json_decode($response, true); echo "Status: " . $data['status'] . "\n"; return $data; } // --- Verify webhook signature --- function verifyWebhook($rawBody, $signature) { global $webhookSecret; $expected = hash_hmac('sha256', $rawBody, $webhookSecret); return hash_equals($expected, $signature); } // Webhook handler $rawBody = file_get_contents('php://input'); $signature = $_SERVER['HTTP_X_COASTAL_SIGNATURE'] ?? ''; if (!verifyWebhook($rawBody, $signature)) { http_response_code(401); echo json_encode(['error' => 'Invalid signature']); exit; } $event = json_decode($rawBody, true); echo "Payment " . $event['payment_id'] . " is now " . $event['status'] . "\n"; http_response_code(200); echo json_encode(['received' => true]); ?>
Rate Limits
The API is rate-limited to 100 requests per minute per API key.
Each response includes rate limit headers:
When the limit is exceeded, the API returns a 429 Too Many Requests response. Wait for the window to reset before retrying.
Errors
All errors return a consistent JSON structure:
{
"error": "error_code",
"message": "Human readable description"
}
| Status | Error Code | Description |
|---|---|---|
400 |
validation_error |
Invalid request body |
401 |
unauthorized |
Missing or invalid API key |
404 |
not_found |
Payment not found |
429 |
rate_limit_exceeded |
Too many requests |
503 |
price_unavailable |
Price feed temporarily unavailable |
Idempotency
If you send the same transaction_id twice, the API returns the original payment instead of creating a duplicate. The response will include "idempotent": true to indicate this.
transaction_id.