Overview

The Valescrow API provides programmatic access to conditional fund allocation and escrow, cross-border stablecoin payments, refunds, payroll disbursement, and withdrawal to local bank accounts across 15 Mento stablecoin currencies. Specify your currency per request (NGNm, KESm, GHSm, USDm, GBPm, EURm, and more). All responses are JSON.

Base URL & Environments

Production:  https://api.usetappay.app/v1
Sandbox:     https://sandbox.usetappay.app/v1

Two key prefixes determine environment:

  • tap_live_sk_xxx — production (mainnet Celo, real money)
  • tap_test_sk_xxx — sandbox (Celo Sepolia testnet, no real money)

Authentication

All requests require a Bearer token:

Authorization: Bearer tap_live_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Never expose your secret key in client-side code, mobile apps, or public repositories. Generate keys in the Valescrow dashboard at valescrow.com or in the TapPay mobile app under Settings → Developer.

API Keys

Generate API keys from the Valescrow dashboard (Settings → API Keys) or the TapPay mobile app (Settings → Developer → Generate API Key). Keys are shown once at creation — store them securely.

Errors

All errors return a consistent shape:

Error response
{
  "error": "Human-readable description",
  "code": "MACHINE_READABLE_CODE"
}
CodeMeaning
<code>400</code>Validation error &mdash; check error and code fields
<code>401</code>Invalid or missing API key
<code>403</code>Forbidden &mdash; account frozen or insufficient permissions
<code>404</code>Resource not found
<code>409</code>Conflict &mdash; duplicate request or concurrent operation
<code>422</code>On-chain verification failed
<code>429</code>Rate limited &mdash; see Retry-After header
<code>503</code>Service unavailable &mdash; insufficient wallet balance

Allocation & Escrow

The Allocation API is TapPay's most powerful primitive. It enables conditional fund disbursement to unlimited recipients with multi-signature approvals, time locks, and milestone triggers — with an immutable on-chain audit trail. Used by NGOs, governments, and trade counterparties across 15 Mento stablecoin currencies. Allocation endpoints are served via Supabase Edge Functions and accessed through the SDK or directly at your project's Supabase URL.

Lifecycle:

Allocation lifecycle
CREATE ──> FUND ──> [ALL CONDITIONS MET] ──> RELEASE ──> RECIPIENTS PAID
  |           |              ^
draft      funded     conditions checked on:
                       - each approval submitted
                       - each milestone triggered
                       - time locks expiring
                       - on funding (for 'immediate' type)

Status values:
  draft        created, not yet funded
  funded       full amount escrowed, conditions pending
  releasing    disbursement in progress (multicall batches)
  released     all recipients paid
  partial      some transfers failed - review required
  cancelled    cancelled, refund issued if funded
  expired      funding deadline passed

Conditions Reference

TypeTriggered byFields requiredUse case
<code>immediate</code>Automatically on fundingnoneSimple disbursement
<code>manual_approval</code>N wallet signatures<code>required_approvals</code>, <code>approver_wallets</code>Board sign-off, multi-sig governance
<code>time_lock</code>Datetime passes<code>release_after</code>Quarterly budget release
<code>milestone</code>External webhook + HMAC<code>milestone_key</code>, optional: <code>milestone_tiers</code>, <code>inspector_email</code>, <code>metric_inspector</code>Human inspector confirmation, programmatic auto-evaluation via external system metrics, or both
<code>all_of</code>All listed sub-conditions met<code>sub_condition_indices</code>Composite AND logic
<code>any_of</code>Any one sub-condition met<code>sub_condition_indices</code>Flexible OR logic

Conditions are evaluated every time an approval, milestone, or time event fires. When ALL root-level conditions are met, disbursement begins automatically.

Tiered Milestones

Milestone conditions can optionally define quality/quantity tiers, each mapped to a release percentage. When an inspector confirms a milestone, they select a tier — and only that percentage of linked recipient amounts is released. Unreleased funds are held for a configurable dispute window (default 7 days), then automatically returned to the allocation creator.

Milestone with tiers
{
  "type": "milestone",
  "label": "Goods inspection",
  "milestone_key": "inspection_PO-441",
  "inspector_email": "inspector@example.com",
  "milestone_tiers": [
    { "label": "Below minimum", "release_percent": 0 },
    { "label": "Partially delivered", "release_percent": 50 },
    { "label": "Substantially delivered", "release_percent": 75 },
    { "label": "Fully delivered", "release_percent": 100 }
  ]
}

Tier constraints:

  • Minimum 2 tiers when milestone_tiers is provided
  • Must include at least one tier with release_percent: 0 (rejection) and one with release_percent > 0
  • Tiers must be sorted ascending by release_percent
  • Each tier: label (non-empty string), release_percent (0–100)
  • inspector_email is optional — receives email notification with inspection link when allocated

POST/v1/allocations

Create a new conditional allocation. Recipients receive funds only when all conditions are met.

TypeScript SDK
const alloc = await tappay.allocations.create({
  title: 'Q1 2026 Education Budget',
  reference: 'LSG-EDU-Q1-2026',
  currency: 'NGNm',
  total_amount: 50_000_000,
  acknowledge_large_allocation: true,
  recipients: [
    { wallet: '0xPrimarySchools...', amount: 20_000_000, label: 'Primary Schools' },
    { wallet: '0xSecondarySchools...', amount: 25_000_000, label: 'Secondary Schools' },
    { wallet: '0xAdmin...', amount: 5_000_000, label: 'Administration', condition_index: 1 },
  ],
  conditions: [
    {
      type: 'manual_approval',
      label: 'Board approval - 2 of 3 required',
      required_approvals: 2,
      approver_wallets: ['0xAuditor1...', '0xAuditor2...', '0xMinister...'],
    },
    {
      type: 'time_lock',
      label: 'Not before Q2 2026',
      release_after: '2026-04-01T08:00:00Z',
    },
  ],
});
console.log(alloc.allocation_id); // "alc_01JXYZ..."

Field constraints:

  • title: 1–200 characters, required
  • total_amount: must be > 0
  • recipients: 1–1,000 items. Sum of amounts must equal total_amount
  • conditions: 1–20 items
  • wallet: valid Celo address (0x + 40 hex chars)
  • milestone_key: globally unique across all allocations
  • milestone_tiers: optional array of tiers (see Tiered Milestones)
  • inspector_email: optional, receives email notification with inspection link
  • acknowledge_large_allocation: required when total_amount > 500,000,000

Trade escrow with tiered inspection:

TypeScript SDK — Trade escrow with tiers
const escrow = await tappay.allocations.create({
  title: 'PO-441 - 500 bags of maize',
  reference: 'PO-441',
  category: 'trade_escrow',
  currency: 'KESm',
  total_amount: 250_000,
  recipients: [
    { wallet: '0xVendorWallet...', amount: 250_000, label: 'Vendor Ochieng' },
  ],
  conditions: [
    {
      type: 'milestone',
      label: 'Delivery confirmed by inspector',
      milestone_key: 'delivery_PO-441',
      inspector_email: 'inspector@logistics.co',
      milestone_tiers: [
        { label: 'Below minimum', release_percent: 0 },
        { label: 'Partial delivery', release_percent: 70 },
        { label: 'Full delivery', release_percent: 100 },
      ],
    },
  ],
});
// Inspector receives email notification with inspection link
// Fund via escrow.payment_url, then inspector confirms on delivery

POST/v1/allocations/fund

Called after the funder sends stablecoin to the Valescrow service wallet. Verifies the on-chain transaction, deducts the platform fee, and locks the remaining amount for recipients.

A 0.5% platform fee is deducted at funding time before ring-fencing recipient amounts. Minimum fee varies by currency (e.g. ₦2,000 NGNm, $2 USDm, KSh250 KESm). This fee is non-refundable and applies to all allocation categories.
TypeScript SDK
const result = await tappay.allocations.fund({
  allocation_id: 'alc_01JXYZ...',
  tx_hash: '0xfunding_transaction_hash...',
});
console.log(result.status);              // "funded"
console.log(result.platform_fee);   // 125000
console.log(result.escrowed_for_recipients); // 49875000

If all conditions are type immediate, status will be releasing immediately and funds begin disbursing.

POST/v1/allocations/approve

Submit an approval for a manual_approval condition. Returns 403 if approver_wallet is not in the condition's approver_wallets list.

TypeScript SDK
const result = await tappay.allocations.approve({
  allocation_id: 'alc_01JXYZ...',
  condition_id: 'cond_uuid_1',
  approver_wallet: '0xAuditor1...',
  note: 'Reviewed and approved',
});
console.log(result.approval_count);     // 1
console.log(result.condition_met);      // false
console.log(result.all_conditions_met); // false

When condition_met becomes true, all conditions are re-evaluated. If all_conditions_met is true, disbursement begins immediately.

POST/v1/allocations/milestone

Trigger a milestone condition from your external system. Authenticated by HMAC-SHA256 — not by API key. Supports three actions: confirm (default), reject, and pending.

Signature algorithm
HMAC-SHA256(milestone_key + ":" + timestamp.toString(), milestone_secret)

Timestamp must be within 300 seconds of server time (replay protection).

Request body fields:

  • milestone_key (required): the milestone's unique key
  • timestamp (required): Unix timestamp (seconds)
  • signature (required): HMAC-SHA256 hex signature
  • action (optional): "confirm" (default), "reject", or "pending"
  • tier_index (optional): index into milestone_tiers array — required when action is "confirm" and tiers exist
  • note (optional): inspector note — required when action is "reject"
Confirm (full)
import { AllocationResource } from '@tappay/sdk';

const { signature, timestamp } = AllocationResource.signMilestone(
  'delivery_PO-441',
  milestoneSecret
);

// Simple confirmation (no tiers)
await tappay.allocations.triggerMilestone({
  milestone_key: 'delivery_PO-441',
  timestamp,
  signature,
  note: 'Confirmed delivery at warehouse',
});

// Tiered confirmation — select tier by index
await tappay.allocations.triggerMilestone({
  milestone_key: 'delivery_PO-441',
  timestamp,
  signature,
  action: 'confirm',
  tier_index: 2,    // "Fully delivered" (100%)
  note: 'All items verified',
});

Reject and Pending actions:

Reject
// Reject — requires a reason in note field
await tappay.allocations.triggerMilestone({
  milestone_key: 'delivery_PO-441',
  timestamp,
  signature,
  action: 'reject',
  note: 'Goods did not match specification',
});
The milestone_secret is returned once at allocation creation. Store it securely — it cannot be retrieved again. Anyone holding this secret can trigger the milestone.
When a tiered milestone confirms at less than 100%, unreleased funds are held for a configurable dispute window (default 7 days), then automatically returned to the allocation creator.

POST/v1/allocations/metric

Push metric data to a milestone with a metric inspector. Instead of a human confirming a milestone, any external system — IoT sensors, ERPs, logistics platforms, monitoring tools, CI/CD pipelines — can push data points via this endpoint. When the metric value crosses a configured threshold, the milestone auto-confirms and funds release without human intervention.

Uses the same HMAC-SHA256 authentication as POST /allocations/milestone.

Metric types:

  • counter — each push increments the running total (e.g. deliveries completed, units manufactured, vaccinations administered)
  • keyword_count — increments a count each time a tracked keyword is detected
  • percentage — latest reading replaces previous value (e.g. inspection score, completion percentage, quality rating)
  • boolean — pass 1 for true, 0 for false (e.g. compliance check passed, temperature within range)

Comparison operators:

  • gte (default) — auto-confirm when value ≥ threshold (e.g. "release when 20 deliveries completed")
  • lte — auto-confirm when value ≤ threshold (e.g. "release when defect rate drops to 2% or below")
  • eq — auto-confirm when value equals threshold exactly

Request body fields:

  • milestone_key (required): the milestone's unique key
  • timestamp (required): Unix timestamp (seconds)
  • signature (required): HMAC-SHA256 hex signature (same as milestone trigger)
  • value (required): the metric value to push
  • metadata (optional): JSON object with context (e.g. sensor ID, batch number, GPS coordinates)
Push metric
import { AllocationResource } from '@tappay/sdk';

const { signature, timestamp } = AllocationResource.signMilestone(
  'warehouse-deliveries',
  milestoneSecret
);

// Push a counter increment — e.g. 4.5 tonnes delivered
await tappay.allocations.pushMetric({
  milestone_key: 'warehouse-deliveries',
  timestamp,
  signature,
  value: 4.5,
  metadata: { truck_id: 'LG-4821', location: 'Apapa Warehouse' },
});

Setting up a metric inspector

Add a metric_inspector field when creating a milestone condition. The metric inspector defines what data to track, how to accumulate it, and at what threshold to auto-confirm.

// Counter — release when 20 tonnes delivered
{
  "type": "milestone",
  "label": "Commodity delivery to warehouse",
  "metric_inspector": {
    "metric_key": "tonnes-delivered",
    "metric_type": "counter",
    "threshold": 20,
    "comparison": "gte"
  }
}

// Percentage — release when inspection score reaches 85%
{
  "type": "milestone",
  "label": "Building inspection score",
  "metric_inspector": {
    "metric_key": "inspection-score",
    "metric_type": "percentage",
    "threshold": 85,
    "comparison": "gte"
  }
}

// Boolean — release when compliance check passes
{
  "type": "milestone",
  "label": "Regulatory compliance verified",
  "metric_inspector": {
    "metric_key": "compliance-check",
    "metric_type": "boolean",
    "threshold": 1,
    "comparison": "eq"
  }
}

Tiered release with metric thresholds

Combine milestone_tiers with tier_thresholds to release funds progressively as metrics accumulate. Each tier threshold maps a metric value to a tier index — when the metric crosses a threshold, the corresponding tier activates and its release percentage applies.

{
  "type": "milestone",
  "label": "Vaccination campaign progress",
  "milestone_tiers": [
    { "label": "Phase 1 — 1,000 vaccinations", "release_percent": 30 },
    { "label": "Phase 2 — 5,000 vaccinations", "release_percent": 70 },
    { "label": "Phase 3 — 10,000 vaccinations", "release_percent": 100 }
  ],
  "metric_inspector": {
    "metric_key": "vaccination-count",
    "metric_type": "counter",
    "threshold": 10000,
    "comparison": "gte",
    "tier_thresholds": [
      { "min_value": 1000, "tier_index": 0 },
      { "min_value": 5000, "tier_index": 1 },
      { "min_value": 10000, "tier_index": 2 }
    ]
  }
}

When the counter reaches 1,000 → tier 0 activates (30% released). At 5,000 → tier 1 (70%). At 10,000 → tier 2 (100%). Each tier upgrade triggers a webhook and audit log entry.

Connecting external systems

Any system that can make an HTTP POST request can push metrics to Valescrow. Here are common integration patterns:

  • IoT & sensors — warehouse scales, temperature monitors, GPS trackers push readings on a schedule or event trigger
  • ERP & supply chain — SAP, Odoo, or custom systems push delivery confirmations, invoice counts, or inventory levels via webhook
  • Project management — Jira, Asana, or custom dashboards push task completion counts or sprint progress
  • CI/CD pipelines — GitHub Actions or Jenkins push build pass counts or deployment success flags
  • Healthcare systems — clinic software pushes patient counts, vaccination records, or treatment completions
  • Financial systems — accounting software pushes revenue milestones, collection totals, or audit completion flags

The integration is a single HTTP call. Use signMilestone() from the SDK for HMAC signing, or compute HMAC-SHA256(milestone_key:timestamp, milestone_secret) in any language.

A milestone can have both an inspector_email (human) and a metric_inspector (programmatic). Either can trigger confirmation — whichever happens first. This lets you set up automatic confirmation with a human fallback, or vice versa.

GET /v1/allocations/milestone/[token] — No Auth Required

For field officers who need to confirm a milestone without an account. Each milestone condition has a unique URL:

https://valescrow.com/milestone/[token]

GET returns allocation title, condition label, current status, and milestone tiers (if defined). POST with action, tier_index, and note triggers the milestone. No API key required — the token is the authentication.

Token page actions
// POST body for field officer confirmation
{ "confirm": true, "action": "confirm", "tier_index": 2, "note": "All items verified" }

// POST body for rejection
{ "confirm": true, "action": "reject", "note": "Items damaged in transit" }

// POST body for pending/in-progress
{ "confirm": true, "action": "pending", "note": "Inspection underway" }

The designated inspector receives an email notification with the token link when the allocation is created (if inspector_email is provided).

POST/v1/allocations/cancel

Cancels an allocation and refunds escrowed funds on-chain. Cannot cancel if status is releasing or released.

The platform fee is non-refundable. The refund covers escrowed_for_recipients, not the original gross amount.
TypeScript SDK
const result = await tappay.allocations.cancel({
  allocation_id: 'alc_01JXYZ...',
  reason: 'Project scope changed',
});
console.log(result.refund_tx_hash); // "0xrefund_hash..."

GET/v1/allocations/:id

Full allocation detail including recipients, conditions, approvals, dispute status, and complete audit trail.

Response
{
  "id": "alc_01JXYZ...",
  "title": "Q1 2026 Education Budget",
  "description": "...",
  "reference": "LSG-EDU-Q1-2026",
  "category": "government_budget",
  "currency": "NGNm",
  "total_amount": 50000000,
  "platform_fee": 125000,
  "escrowed_amount": 49875000,
  "status": "funded",
  "conditions_met": false,
  "funded_at": "2026-03-17T22:00:00.000Z",
  "funding_tx_hash": "0xfunding...",
  "released_at": null,
  "cancelled_at": null,
  "created_at": "2026-03-17T21:00:00.000Z",
  "payment_url": "https://usetappay.app/fund/alc_01JXYZ...",
  "dispute_window_hours": 168,
  "tier_dispute_deadline": null,
  "unreleased_amount": 0,
  "unreleased_refunded": false,
  "refund_tx_hash": null,

  "recipients": [{
    "id": "recip_uuid",
    "wallet": "0xPrimarySchools...",
    "label": "Primary Schools Dept",
    "category": "education",
    "amount": 19950000,
    "released_amount": null,
    "status": "pending",
    "tx_hash": null,
    "condition_id": null,
    "completed_at": null
  }],

  "conditions": [{
    "id": "cond_uuid_1",
    "type": "manual_approval",
    "label": "Board of Trustees (2 of 3)",
    "is_met": false,
    "met_at": null,
    "required_approvals": 2,
    "approval_count": 1,
    "approver_wallets": ["0xAuditor1", "0xAuditor2", "0xMinister"],
    "release_after": null,
    "milestone_key": null,
    "milestone_tiers": null,
    "inspector_email": null,
    "selected_tier_index": null,
    "inspection_status": "pending",
    "inspection_note": null,
    "rejected_at": null,
    "rejection_reason": null,
    "sort_order": 0
  }],

  "approvals": [{
    "condition_id": "cond_uuid_1",
    "approver_wallet": "0xAuditor1",
    "note": "Reviewed and approved",
    "created_at": "2026-03-17T22:30:00.000Z"
  }],

  "audit_log": [
    {
      "event": "created",
      "actor_type": "api",
      "detail": { "recipient_count": 2, "condition_count": 3, "total_amount": 50000000 },
      "created_at": "2026-03-17T21:00:00.000Z"
    },
    {
      "event": "platform_fee_collected",
      "actor_type": "system",
      "detail": {
        "fee": 250000, "fee_rate": 0.005, "fee_type": "standard",
        "gross_amount": 50000000, "escrowed_for_recipients": 49750000,
        "fee_tx_hash": "0xfee..."
      },
      "created_at": "2026-03-17T22:00:00.000Z"
    },
    {
      "event": "funded",
      "actor_type": "system",
      "detail": { "tx_hash": "0xfunding...", "amount": 50000000 },
      "created_at": "2026-03-17T22:00:00.000Z"
    },
    {
      "event": "approved",
      "actor_wallet": "0xAuditor1",
      "actor_type": "approver",
      "detail": {
        "note": "Reviewed and approved",
        "count": "1/2",
        "condition_label": "Board of Trustees (2 of 3)"
      },
      "created_at": "2026-03-17T22:30:00.000Z"
    }
  ]
}

New fields explained:

  • dispute_window_hours: hours before unreleased funds are refunded (default 168 = 7 days)
  • tier_dispute_deadline: ISO timestamp when dispute window expires (null if no tiered release)
  • unreleased_amount: amount held pending dispute window expiry
  • unreleased_refunded: whether unreleased funds have been returned to creator
  • refund_tx_hash: on-chain transaction hash of the unreleased funds refund
  • released_amount (on recipients): actual release amount after tier selection — null means full amount
  • milestone_tiers (on conditions): array of tier definitions, or null
  • inspection_status: pending, in_progress, confirmed, or rejected
  • selected_tier_index: which tier was selected by inspector (index into milestone_tiers)
  • inspection_note: note from inspector
  • rejection_reason: reason provided when inspector rejects

GET/v1/allocations

List allocations. Query params: status, limit (default 20, max 100), offset.

Response
{
  "allocations": [{
    "id": "alc_01JXYZ...",
    "title": "Q1 2026 Education Budget",
    "reference": "LSG-EDU-Q1-2026",
    "currency": "NGNm",
    "total_amount": 50000000,
    "status": "funded",
    "conditions_met": false,
    "recipient_count": 2,
    "funded_at": "2026-03-17T22:00:00.000Z",
    "released_at": null,
    "created_at": "2026-03-17T21:00:00.000Z"
  }],
  "total": 7,
  "has_more": false
}

Allocation Discovery

The Allocation Discovery Platform lets investors browse publicly listed allocations without authentication. Creators opt in via the is_discoverable flag at creation time or the portal toggle. All responses are public-safe — zero wallet addresses, secrets, or inspector emails are ever returned.

This is a primary participation platform — investors fund allocations directly. There is no secondary trading. Language: "Allocation Discovery Platform" — never "marketplace" or "exchange."

GET/v1/allocations/public

List discoverable allocations. No API key required. Rate limited to 30 requests/min per IP. Responses are cached for 30 seconds.

Query parameters

ParamTypeDescription
categorystringFilter by category (e.g. real_estate_fractional, trade_finance_invoice)
currencystringFilter by currency (e.g. NGNm, USDm)
statusstringFilter by status (funded, releasing, released)
limitnumberResults per page (default 20, max 100)
offsetnumberPagination offset
idstringFetch a single allocation by ID (e.g. alc_01JXYZ...)
TypeScript SDK
// No API key needed for public discovery
const tappay = new TapPay('tap_live_sk_xxx');
const result = await tappay.allocations.listPublic({
  category: 'real_estate_fractional',
  currency: 'NGNm',
  limit: 10,
});

for (const alloc of result.allocations) {
  console.log(alloc.title, alloc.funding_progress.percent + '% funded');
}
Public responses never include: wallet addresses, approver wallets, milestone keys/secrets, inspector emails, funding transaction hashes, or creator identity. This is enforced server-side.

Making allocations discoverable

Pass is_discoverable: true when creating an allocation via the API. You can also set a public-facing title and description that differs from the internal allocation details.

TypeScript SDK
const alloc = await tappay.allocations.create({
  title: 'Internal: Lekki Phase 2 Round A',
  total_amount: 500_000_000,
  currency: 'NGNm',
  is_discoverable: true,
  discoverable_title: 'Lekki Phase 2 — 40-Unit Apartment Complex',
  discoverable_description: 'Fractional ownership in a 40-unit luxury apartment development in Lekki Phase 2, Lagos.',
  template_id: 're_fractional',
  recipients: [/* ... */],
  conditions: [/* ... */],
});

RWA template reference

Valescrow provides 16 pre-built RWA templates across five sectors. Each template maps to existing condition types — no new backend logic. Pass the template_id when creating allocations for analytics tracking.

Template IDNameSectorConditions
re_fractionalFractional Property InvestmentReal Estateall_of(manual_approval, time_lock)
re_constructionConstruction DrawReal Estate5× milestone + manual_approval
re_rentalRental Income DistributionReal Estatetime_lock + immediate
re_mortgageMortgage EscrowReal Estateall_of(time_lock, milestone×2, manual_approval)
re_deed_transferProperty Deed TransferReal Estateall_of(time_lock, milestone×2, manual_approval×2)
tf_invoiceInvoice EscrowTrade Financeall_of(time_lock, milestone)
tf_poPO FinancingTrade Financemilestone + manual_approval
tf_supply_chainSupply Chain FinanceTrade Financeall_of(milestone×2, manual_approval)
tf_buyer_escrowBuyer-Funded Trade EscrowTrade Financeany_of(milestones, time_lock)
sec_vestingEmployee VestingSecuritiestime_lock + manual_approval
sec_lockupLock-Up PeriodSecuritiestime_lock + manual_approval
sec_dividendDividend DistributionSecuritiestime_lock + immediate
gov_budgetBudget DisbursementGovernmentall_of(manual_approval×2, milestone×3)
gov_procurementProcurement EscrowGovernmentall_of(milestone×2, manual_approval)
ngo_grantGrant ManagementNGO & Developmentall_of(milestone×3, time_lock)
ngo_impactImpact-Linked FundingNGO & Developmentall_of(milestone×2, manual_approval, time_lock)

Payments

POST/v1/payment-requests

Create a payment request (invoice) that can be paid via the TapPay consumer app or web page.

TypeScript SDK
const request = await tappay.paymentRequests.create({
  amount: 5000,
  reference: 'INV-001',
  description: 'Consultation fee',
  splits: [
    { wallet: '0xDoctor...', amount: 3500, label: 'Doctor fee' },
    { wallet: '0xPlatform...', amount: 1500, label: 'Platform fee' },
  ],
});
console.log(request.payment_url);

splits: optional, max 5 recipients. Sum must equal amount. Splits enable automatic revenue distribution on payment — each recipient receives their share on-chain in a single transaction.

currency: any Mento stablecoin (NGNm, KESm, GHSm, USDm, GBPm, EURm, ZARm, XOFm, etc.). Defaults to NGNm if omitted.

GET/v1/payment-requests

ParamTypeDescription
<code>status</code>stringpending | completed | expired
<code>reference</code>stringFilter by your reference
<code>from</code>stringISO datetime
<code>to</code>stringISO datetime
<code>amount_min</code>numberMinimum amount
<code>amount_max</code>numberMaximum amount
<code>limit</code>numberDefault 20, max 100
<code>offset</code>numberPagination offset

GET/v1/payment-requests/:id

Returns full PaymentRequest including refunded_amount.

GET/v1/verify/:reference

Verify payment by your reference string. Returns full PaymentRequest including refund history and split statuses.

TypeScript SDK
const result = await tappay.paymentRequests.verify('INV-001');
if (result.data.status === 'completed') {
  console.log('Paid!', result.data.tx_hash);
}

Transactions

GET/v1/transactions

Query params: type (sent | received), from, to, amount_min, amount_max, limit, offset.

Response
{
  "transactions": [{
    "id": "txn_01J...",
    "type": "received",
    "amount": "5000",
    "currency": "NGNm",
    "counterparty": "0xPayer...",
    "tx_hash": "0xabc123...",
    "status": "confirmed",
    "created_at": "2026-03-17T22:00:00.000Z"
  }],
  "total": 124,
  "has_more": true
}

Refunds

Both full and partial refunds supported. Transfers the original stablecoin on-chain back to the payer's wallet.

POST/v1/refunds

Omit amount for a full refund. Partial refunds can be issued multiple times until the full original amount is refunded.

TypeScript SDK
const refund = await tappay.refunds.create({
  payment_request_id: 'pr_01J...',
  amount: 2500,
  reason: 'Doctor cancelled appointment',
});
console.log(refund.status); // "completed"

Fires: refund.completed or refund.failed webhook.

GET/v1/refunds

List all refunds. Supports payment_request_id, limit, offset query params.

GET/v1/refunds/:id

Get a single refund by ID.

Balance & Payouts

GET/v1/balance

Response
{
  "balances": [
    { "currency": "NGNm", "balance": "284500.00", "fiat_symbol": "₦" },
    { "currency": "KESm", "balance": "0.00", "fiat_symbol": "KSh" }
  ],
  "wallet_address": "0xYourWallet...",
  "environment": "live"
}

POST/v1/payouts

Withdraw stablecoin balance to a local bank account. TapPay's smart router automatically selects the best payout partner based on currency, fees, and availability.

Supported payout corridors: NGNm → NGN (Nigeria), KESm → KES (Kenya), GHSm → GHS (Ghana), ZARm → ZAR (South Africa), XOFm → XOF (West Africa). USDm/GBPm/EURm offramp via Transak (global).
Payout fees are deducted from the withdrawal amount. Fees vary by currency and corridor:

Nigeria (NGNm → NGN):
• Platform fee: 0.75% (min ₦100)
• NIBSS transfer fee: ₦52.50 (regulatory pass-through)
• Stamp duty: ₦50.00 (regulatory pass-through)

Other corridors have different fee structures. Use GET /v1/rail-fees?currency=KESm to retrieve fees for a specific currency at runtime.
TypeScript SDK
const payout = await tappay.payouts.create({
  amount: 50000,
  currency: 'NGNm',  // or KESm, GHSm, USDm, etc.
  bank_code: '044',
  account_number: '0123456789',
  reference: 'PAYOUT-001',
});

Webhooks

TapPay sends signed HTTP POST requests to your endpoint when events occur. Deliveries are retried on failure.

Creating a webhook (TypeScript SDK)
const webhook = await tappay.webhooks.create({
  url: 'https://your-app.com/webhooks/tappay',
  events: ['payment.received', 'allocation.released', 'refund.completed'],
});
// Store webhook.secret for signature verification

Payload envelope:

Webhook payload
{
  "event": "allocation.released",
  "created": 1742248800000,
  "data": { ... }
}

All Webhook Events

Payment events:

EventFires when
<code>payment.received</code>Payment request paid
<code>payment.sent</code>Outbound transfer confirmed
<code>payout.completed</code>Bank withdrawal settled
<code>refund.completed</code>Refund transferred on-chain
<code>refund.failed</code>Refund on-chain transfer failed

Allocation events:

EventFires when
<code>allocation.funded</code>Escrow funded, conditions now active
<code>allocation.condition_met</code>One condition satisfied
<code>allocation.all_conditions_met</code>All conditions met &mdash; disbursement imminent
<code>allocation.releasing</code>Multicall batch disbursement started
<code>allocation.released</code>All recipients paid successfully
<code>allocation.partial_failure</code>Some transfers failed &mdash; review required
<code>allocation.cancelled</code>Cancelled (+ on-chain refund if funded)

Verifying Signatures

TypeScript SDK
import { TapPay } from '@tappay/sdk';

app.post('/webhooks/tappay', (req, res) => {
  const isValid = TapPay.webhooks.verify(
    req.rawBody,
    req.headers['tappay-signature'],
    webhookSecret
  );
  if (!isValid) return res.status(401).send('Invalid signature');

  const { event, data } = JSON.parse(req.rawBody);
  // handle event...
  res.status(200).send('ok');
});

Retry Logic

Delivery attempted up to 4 times on failure:

  • Attempt 1: immediate
  • Attempt 2: 1 minute later
  • Attempt 3: 5 minutes later
  • Attempt 4: 30 minutes later

Your endpoint must return 2xx within 10 seconds. After 4 failures the webhook is disabled. Re-enable via dashboard.

Limits & Quotas

Allocation Tiers

TierAmount rangeRequirement
StandardUp to N500MNo flag needed
LargeN500M &mdash; N2B<code>acknowledge_large_allocation: true</code>
InstitutionalAbove N2BContact support &mdash; multi-sig custody required

Max recipients per allocation: 1,000. Max conditions per allocation: 20. Max splits per payment request: 5.

Limits shown in NGN equivalent. The soft limit reflects TapPay's current single-wallet custody architecture. Institutional allocations above the hard limit require a multi-signature custody arrangement. Contact support@usetappay.app to discuss institutional access.

Throughput

Disbursements use on-chain multicall batching:

  • 100 recipients per multicall transaction
  • 1,000 recipients processed in ~10 transactions (~2 minutes)
  • 10,000 recipients processed in ~100 transactions (~20 minutes)

API Rate Limits

Endpoint categoryLimit
General100 req/min per API key
<code>POST /allocations</code>20 req/min
<code>POST /allocations/approve</code>30 req/min
<code>POST /allocations/milestone</code>20 req/min per key
<code>POST /allocations/metric</code>60 req/min per key
<code>POST /refunds</code>20 req/min

Rate limit headers on every response:

X-RateLimit-Remaining: 94
Retry-After: 12          // only on 429 responses

SDK Reference

Installation

Install
npm install @tappay/sdk
Initialize
import { TapPay } from '@tappay/sdk';

const tappay = new TapPay('tap_live_sk_xxx');

Resources

All resources
tappay.paymentRequests   // .create() .get() .list() .verify()
tappay.transactions      // .list()
tappay.refunds           // .create() .get() .list()
tappay.balance           // .get()
tappay.payouts           // .create()
tappay.webhooks          // .create() .list() .delete()
tappay.allocations       // .create() .fund() .get() .list()
                         // .approve() .triggerMilestone() .cancel()

// Static - no instance needed:
TapPay.webhooks.verify(payload, signature, secret)
AllocationResource.signMilestone(milestoneKey, milestoneSecret)

TypeScript types:

import type {
  PaymentRequest, Allocation, AllocationStatus,
  ConditionType, CreateAllocationParams,
  FundAllocationResponse,
} from '@tappay/sdk';

Error Handling

TypeScript
import { TapPay, TapPayError } from '@tappay/sdk';

try {
  const alloc = await tappay.allocations.create({ ... });
} catch (err) {
  if (err instanceof TapPayError) {
    console.error(err.message);    // human-readable
    console.error(err.statusCode); // HTTP status
    console.error(err.body);       // full error response including code
  }
}
TapPay — Programmable Money Infrastructure. Built on Celo.