PaymentSafe API

Build secure escrow payments into any African marketplace. REST API, JavaScript SDK, and MCP server for AI coding assistants.

What is PaymentSafe?

PaymentSafe is an escrow service for African peer-to-peer marketplaces. When a buyer and seller agree on a deal, the buyer pays into a secure vault. Funds are held safely until the buyer confirms delivery — then the seller is paid. Neither party can be scammed.

PaymentSafe is not a payment gateway — it sits on top of Paystack and adds the escrow trust layer. Your app collects buyer and seller phone numbers, creates an escrow via the API, and PaymentSafe handles the rest.

IntegrationBest for
REST APIAny backend — Node.js, Python, PHP, Go, etc.
JS SDK (paymentsafe-js)Node.js, Next.js, Nuxt, browser apps
MCP Server (paymentsafe-mcp)AI IDEs: Cursor, Claude, Windsurf, Kiro, Cline, Roo Code, Continue, VS Code

Create your first escrow in 2 minutes

Step 1 — Get an API key

Sign up at paymentsafe.business → Developer Portal → My Apps → create a new app. You receive a sandbox key (sk_sandbox_...) immediately.

Step 2 — Create an escrow

cURL
# Create a GHS 6,500 escrow for an iPhone 14
curl -X POST "https://api.paymentsafe.business/v1/transactions" \
  -H "Authorization: Bearer sk_sandbox_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "iPhone 14 Pro Max",
    "price": 6500,
    "currency": "GHS",
    "buyerPhone": "+233540000001",
    "sellerPhone": "+233244000002",
    "inspectionDays": 3
  }'

Step 3 — Response

{
  "id": "tx-api-1234567890-abc123",
  "title": "iPhone 14 Pro Max",
  "status": "CREATED",
  "currency": "GHS",
  "price": 6500,
  "fee": 195,
  "sandbox": true,
  "verificationUrl": "https://paymentsafe.business/verify/tx-api-1234567890-abc123",
  "createdAt": "2026-07-07T12:00:00.000Z"
}

Share the verificationUrl with both buyer and seller so they can track the deal in real time.

Use sk_sandbox_* keys for testing — no real money moves. Switch to sk_live_* when going live.

API Keys

All authenticated requests require an Authorization header:

Authorization: Bearer sk_sandbox_your_key_here
Key prefixTypePermissions
sk_sandbox_Secret sandboxRead + write (test mode, no real money)
sk_live_Secret liveRead + write (real money)
pk_PublicRead-only (safe for frontend use)
⚠️
Never expose your sk_ secret key in client-side code or version control. Store it in environment variables only.

Sandbox vs Live

FeatureSandboxLive
API base URLhttps://api.paymentsafe.business/v1
Key prefixsk_sandbox_sk_live_
Real moneyNoYes
Paystack chargesNoYes
Transaction limitUnlimited50/month (free tier)

How to get your Live API Keys

When you are ready to go to production, you need two keys: a public key (pk_live_...) for your frontend, and a secret key (sk_live_...) for your backend. Here is the exact step-by-step process to obtain them from the portal.

⚠️
Only switch to Live Mode when your integration is fully tested with sandbox keys. Live keys process real money.
1
Go to paymentsafe.business and click “Sign In”

Open paymentsafe.business in your browser. Click the “Sign In” button in the top-right corner of the navigation bar.

PaymentSafe homepage — Sign In button highlighted in the top-right
2
Enter your credentials in the Sign In modal

A sign-in modal will appear. Enter your developer email address and password, then click “Sign In”. If you don’t have an account yet, click “Sign Up” to register a free developer account.

PaymentSafe Sign In modal
3
Click “API Portal” in the top navigation

Once logged in, look for the “API Portal” tab in the top navigation bar and click it. This is the developer hub where all your API credentials and settings live.

PaymentSafe dashboard — API Portal tab in the navigation
4
Click “Keys & Webhooks” in the sidebar

In the Developer Portal sidebar under Developer Tools, click “Keys & Webhooks”. You will land on the API credentials page in Test Mode by default — showing your sandbox keys (pk_test_... / sk_sandbox_...).

Keys and Webhooks page in Test Mode
5
Toggle to “Live Mode”

Click the “Live Mode” toggle at the top of the page. You will see a confirmation message: “Switched Developer Portal to Production (live) mode.”

⚠️
Live keys charge real money through Paystack. Make sure your integration is fully tested in Sandbox before switching.
Keys and Webhooks page switched to Live Mode showing pk_live and sk_live keys
6
Copy your live keys and store them securely

Your live credentials are now visible. Click the “Copy” button next to each key and store them as follows:

🌐 Public Key — Frontend safe
pk_live_...
Add to .env.local as PAYMENTSAFE_PUBLIC_KEY
🔒 Secret Key — Backend only
sk_live_...
Store in Firebase Secrets via CLI — never in source code
.env.local (public key only)
PAYMENTSAFE_PUBLIC_KEY=pk_live_your_key_here
Firebase CLI (secret key — run once)
firebase functions:secrets:set PAYMENTSAFE_SECRET_KEY
# When prompted, paste your sk_live_... key and press Enter
🔐
Your .env.local is already in .gitignore — it will never be committed. The secret key stored via Firebase Secrets is encrypted at rest and only injected into Cloud Functions at runtime.

Base URL & Headers

https://api.paymentsafe.business/v1

Authorization: Bearer <your_api_key>
Content-Type: application/json

Transactions

POST /v1/transactions 🔒 Requires sk_*

Create a new escrow transaction between a buyer and seller.

FieldTypeRequiredDescription
titlestringItem being sold (e.g. "iPhone 14 Pro Max")
pricenumberAmount as a number (e.g. 6500)
currencystringGHS, USD, or EUR
buyerPhonestringBuyer phone in international format (+233...)
sellerPhonestringSeller phone in international format
inspectionDaysnumberNoDays to inspect after delivery (default: 3)
descriptionstringNoOptional deal details
milestonesarrayNoPhased payment milestones
GET /v1/transactions 🔒 Requires sk_* or pk_*

List all transactions for this API key (most recent 50, newest first).

GET /v1/transactions/:id 🔒 Requires sk_* or pk_*

Get a single transaction by ID.

GET /v1/transactions/:id/verify 🌐 Public — no auth needed

Publicly verify any transaction. Safe to call from a frontend — no secret key needed.

POST /v1/transactions/:id/release 🔒 Requires sk_*

Buyer requests fund release to seller. Moves status to AWAITING_PAYOUT. Final payout executed by PaymentSafe staff.

POST /v1/transactions/:id/deliver 🔒 Requires sk_*

Confirm buyer received the item. Moves status to DELIVERED.

POST /v1/transactions/:id/dispute 🔒 Requires sk_*

Open a dispute. Funds are frozen immediately and PaymentSafe staff reviews within 24 hours. Optionally pass { "reason": "..." } in the body.

Exchange Rates

GET /v1/rates 🌐 Public — no auth

Get live GHS/USD/EUR exchange rates. Cached for 10 minutes.

{ "USD_GHS": 15.20, "EUR_GHS": 16.50, "source": "cache" }

Health Check

GET /v1/ping 🌐 Public
{ "status": "ok", "service": "PaymentSafe API", "version": "1.0" }

Errors

HTTP StatusCodeMeaning
400missing_fields, invalid_currencyFix your request body
401missing_auth, invalid_keyMissing or invalid API key
403forbidden, suspendedNot authorised for this resource
404not_foundTransaction not found
409invalid_transitionInvalid status change attempted
429rate_limited100 req/hour limit exceeded
500internal_errorContact support@paymentsafe.business

Rate Limits

100 requests per hour per API key on the free plan. Window resets every 60 minutes. Contact hello@paymentsafe.business to discuss higher limits.


Installation

npm install paymentsafe-js
# or
yarn add paymentsafe-js

Usage

CommonJS (Node.js)
const { PaymentSafe } = require('paymentsafe-js');

const ps = new PaymentSafe({ apiKey: 'sk_sandbox_your_key' });

// Create escrow
const txn = await ps.transactions.create({
  title: 'iPhone 14 Pro Max',
  price: 6500,
  currency: 'GHS',
  buyerPhone: '+233540000001',
  sellerPhone: '+233244000002'
});

console.log(txn.verificationUrl); // share with buyer & seller

// Other methods
await ps.transactions.get('tx-api-xxx');
await ps.transactions.confirmDelivery('tx-api-xxx');
await ps.transactions.release('tx-api-xxx');
await ps.transactions.dispute('tx-api-xxx', 'Item not as described');
await ps.rates.get();
TypeScript / ES Modules
import { PaymentSafe, PaymentSafeError } from 'paymentsafe-js';

const ps = new PaymentSafe({ apiKey: process.env.PAYMENTSAFE_API_KEY! });

try {
  const txn = await ps.transactions.create({ ... });
} catch (err) {
  if (err instanceof PaymentSafeError) {
    console.error(err.code, err.status, err.message);
  }
}

Webhook Verification

Express.js
const { verifyWebhook } = require('paymentsafe-js/webhooks');

app.post('/webhooks/escrow', express.raw({ type: 'application/json' }), (req, res) => {
  const event = verifyWebhook(
    req.body,
    req.headers['x-paymentsafe-signature'],
    process.env.PAYMENTSAFE_WEBHOOK_SECRET
  );

  if (event.type === 'escrow.funded') {
    // Safe to ship — payment is secured in escrow
  }
  res.sendStatus(200);
});
🔐
Webhooks are signed with HMAC-SHA256. Always verify the x-paymentsafe-signature header before processing events.

PaymentSafe for AI IDEs

The paymentsafe-mcp package is a Model Context Protocol (MCP) server that lets AI coding assistants create and manage escrow transactions using natural language — no API boilerplate required.

Just tell your AI: "Create an escrow for a GHS 6,500 iPhone deal. Buyer: +233540000001, Seller: +233244000002" — and it handles everything.

IDE Setup

🖱️
Cursor
~/.cursor/mcp.json
🤖
Claude Desktop
claude_desktop_config.json
⌨️
Claude Code
claude mcp add ...
🌊
Windsurf
mcp_settings.json
☁️
Kiro
.kiro/settings/mcp.json
🦾
Cline
cline_mcp_settings.json
🦘
Roo Code
.roo/mcp.json
➡️
Continue
~/.continue/config.json
🔷
VS Code Copilot
settings.json

All IDEs use the same JSON config block:

{
  "mcpServers": {
    "paymentsafe": {
      "command": "npx",
      "args": ["-y", "paymentsafe-mcp"],
      "env": {
        "PAYMENTSAFE_API_KEY": "sk_sandbox_your_key_here"
      }
    }
  }
}

Full per-IDE instructions: npmjs.com/package/paymentsafe-mcp

Available MCP Tools

ToolDescription
create_escrowCreate a new escrow transaction
get_transactionGet status and details by ID
list_transactionsList all transactions (last 50)
release_fundsRequest fund release to seller
open_disputeFreeze funds and open a dispute
confirm_deliveryConfirm buyer received item
get_ratesGet live GHS/USD/EUR exchange rates

Transaction Statuses

StatusMeaningNext step
CREATEDAwaiting buyer paymentShare verificationUrl
PART_FUNDEDPartial/down payment receivedAwait balance
FUNDEDFully funded — seller can shipSeller ships
SHIPPEDSeller dispatched orderAwait delivery
DELIVEREDBuyer confirmed receiptRelease funds
AWAITING_PAYOUTRelease requested, staff processingAwait staff approval
RELEASEDFunds sent to seller✅ Deal complete
DISPUTEDDispute opened, funds frozenStaff review (24h)
REFUNDEDFunds returned to buyerDeal closed

Webhook Events

PaymentSafe sends a signed POST to your webhookUrl on every status change. Retried up to 5 times with exponential backoff.

escrow.createdTransaction created
escrow.fundedPayment received in vault
escrow.shippedSeller dispatched order
escrow.deliveredBuyer confirmed receipt
escrow.completedFunds released to seller
escrow.disputedDispute opened, funds frozen
// Example payload
{
  "event": "escrow.funded",
  "timestamp": 1751886000,
  "data": {
    "id": "tx-api-1234567890-abc123",
    "status": "FUNDED",
    "currency": "GHS",
    "price": 6500,
    "fee": 195,
    "sandbox": false
  }
}

Signature header: x-paymentsafe-signature (sha256=HMAC hex)

Currencies & Countries

CurrencyCodeCountries
Ghanaian CediGHS🇬🇭 Ghana (primary market)
US DollarUSD🇳🇬 Nigeria · 🇰🇪 Kenya · 🇺🇬 Uganda · 🇹🇿 Tanzania · 🇷🇼 Rwanda · 🇿🇲 Zambia · 🇸🇳 Senegal · and more
EuroEURCross-border & diaspora payments

Payment methods (via Paystack): Mobile Money (MTN, Vodafone, AirtelTigo), Debit/Credit Cards (Visa, Mastercard), Bank Transfer, USSD. No smartphone required for USSD access.

Fees & Pricing

Total Default
3%
GHS 1,000 deal = GHS 30 fee
Sandbox
Free
Unlimited test transactions
Free Tier Limit
50
Live transactions/month
Rate Limit
100
API requests per hour
💬
Need higher limits or a custom fee arrangement? Contact hello@paymentsafe.business

Common Setup Issues

Running into a problem? These are the most frequent issues developers encounter when integrating PaymentSafe.

Cloud Functions show “Needs Blaze Plan” Most Common
SMS, USSD notifications, and Paystack webhooks are not working

Firebase Cloud Functions can only make outbound network requests (to Paystack, SMS providers, etc.) on the Blaze pay-as-you-go plan. The free Spark plan blocks all external calls from functions.

💰
Cost reality: Blaze is still free within Firebase’s generous quota. For most new apps the monthly bill is $0. You only pay if you process millions of requests.

How to upgrade to Blaze:

  1. Go to console.firebase.google.com and select your project
  2. Click “Upgrade” in the bottom-left of the Firebase console sidebar
  3. Select Blaze (Pay as you go) and click “Continue”
  4. Add a Google Cloud billing account (credit or debit card required)
  5. Return to your terminal and redeploy your functions:
firebase deploy --only functions

After deploying, return to the PaymentSafe API Portal and click “↻ Refresh Status” — Cloud Functions should show Active.

Features that require Blaze:

FeatureWorks on Spark (free)?Works on Blaze?
Firestore reads / writes✅ Yes✅ Yes
Firebase Authentication✅ Yes✅ Yes
Static Hosting✅ Yes✅ Yes
Paystack webhook processing❌ No✅ Yes
SMS / USSD notifications❌ No✅ Yes
Live exchange rates❌ No✅ Yes
Escrow fund releases❌ No✅ Yes
🔒
API returns 401 Unauthorized
Every request fails with an authentication error

This means your API key is missing, malformed, or revoked. Check the following:

  1. Make sure the Authorization header is formatted exactly as: Bearer sk_sandbox_your_key
  2. Confirm you are using the correct key for the environment — sk_sandbox_* for test, sk_live_* for production
  3. Go to paymentsafe.business → API Portal → Keys & Webhooks and verify your key is still active
  4. If the key was rotated or deleted, generate a new one and update your environment variables
📡
Webhooks are not being received
Status changes happen but your server never gets notified

Webhooks require a publicly accessible HTTPS URL. If you are developing locally, your localhost cannot receive them directly.

  1. Use ngrok to expose your local server:

    ngrok http 3000
  2. Copy the forwarding URL (e.g. https://xxxx.ngrok-free.app)
  3. Paste it into the Webhook URL field in your API Portal settings
  4. Always verify the x-paymentsafe-signature header before processing any event — see the Webhook Verification section
🔐
PaymentSafe retries failed webhook deliveries up to 5 times with exponential backoff. Check your server logs before assuming webhooks are not being sent.
🔄
Transactions created in sandbox don’t appear in live — or vice versa
Data seems to be missing or in the wrong place

Sandbox and live environments are completely isolated. Transactions, webhooks, and data do not cross between them.

Key prefixEnvironmentReal money?
sk_sandbox_Test / SandboxNo
sk_live_Production / LiveYes

If you are not seeing your data, verify which key your application is currently using and switch to the correct environment in the API Portal.

🌐
CORS errors when calling the API from the browser
Browser console shows a blocked cross-origin request

The PaymentSafe REST API is designed to be called from a server-side backend, not directly from a browser. Calling it from client-side JavaScript exposes your secret key.

⚠️
Never call /v1/transactions with your sk_* secret key from browser code. Your secret key will be visible to anyone who inspects the network tab.

Correct architecture:

  1. Your frontend sends a request to your own backend (Node.js, Firebase Function, etc.)
  2. Your backend holds the sk_* secret key in an environment variable
  3. Your backend calls the PaymentSafe API and returns the result to the frontend

The only PaymentSafe endpoint safe to call from a browser is the public GET /v1/transactions/:id/verify endpoint, which requires no secret key.

Requests returning 429 Too Many Requests
API calls are being rate-limited

The free plan allows 100 API requests per hour per key. If you exceed this, requests are rejected with a 429 status until the window resets.

  1. Add retry logic with exponential backoff in your integration
  2. Cache responses where possible (e.g. exchange rates are cached for 10 minutes)
  3. If you need higher limits, contact hello@paymentsafe.business to discuss an upgraded plan
💬
Still stuck? Email us at hello@paymentsafe.business or open an issue on GitHub. Include your API key prefix (e.g. sk_sandbox_abc...) and the full error response so we can help faster.

Made with ❤️ by Transoft Technologies · Accra, Ghana

Home · paymentsafe-js · paymentsafe-mcp · hello@paymentsafe.business