Dark
Light
System
Login Sign Up
Back to API overview
API Documentation
v1 Live

API Reference.
Every endpoint, every language.

Complete reference for the Tenseal REST API — authentication, escrow, inventory, orders, marketplace, wallets, and webhooks. Code samples for cURL, PHP, Node.js, Python, Go, and Ruby.

Quick Start Get API Key
Base URL: https://tenseal.co/api/v1

Quick Start

Get from zero to your first API call in under two minutes.

  1. Sign up for a Tenseal account at app.tenseal.co/register (escrow / wallet) or inventory.tenseal.co/register (inventory tenants).
  2. Head to Settings → API & Integrations and click Generate API Key. Copy it somewhere safe — you will only see it once.
  3. Make your first authenticated call:
curl https://tenseal.co/api/v1/user \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Accept: application/json'
<?php
$ch = curl_init('https://tenseal.co/api/v1/user');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('TENSEAL_API_KEY'),
        'Accept: application/json',
    ],
]);
$response = json_decode(curl_exec($ch), true);
print_r($response);
const res = await fetch('https://tenseal.co/api/v1/user', {
  headers: {
    'Authorization': `Bearer ${process.env.TENSEAL_API_KEY}`,
    'Accept': 'application/json'
  }
});
const user = await res.json();
console.log(user);
import os, requests

r = requests.get(
    'https://tenseal.co/api/v1/user',
    headers={'Authorization': f'Bearer {os.environ["TENSEAL_API_KEY"]}'}
)
print(r.json())
req, _ := http.NewRequest("GET", "https://tenseal.co/api/v1/user", nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("TENSEAL_API_KEY"))
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
require 'net/http'
require 'json'

uri = URI('https://tenseal.co/api/v1/user')
req = Net::HTTP::Get.new(uri)
req['Authorization'] = "Bearer #{ENV['TENSEAL_API_KEY']}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
puts JSON.parse(res.body)

Authentication

Tenseal supports two authentication methods depending on your use case.

1. Bearer Token (Sanctum) — for mobile apps, web dashboards, user sessions

Obtain a token via POST /auth/login and include it in every request:

Authorization: Bearer 1|abc123xyz...

2. API Key — for server-to-server, WordPress plugin, third-party integrations

Generate a long-lived key from Inventory Settings → API & Integrations. Use it the same way:

Authorization: Bearer tns_live_9f8a7b6c5d4e3f2a1b0c...
Keep your keys secret. Never commit API keys to version control or ship them in client-side apps. Use environment variables and rotate keys immediately if exposed.

Base URL & Versioning

All API requests use the following base URL, with the version prefix included:

https://tenseal.co/api/v1

The current stable version is v1. Breaking changes trigger a new version; additive changes (new fields, new endpoints) ship within v1.

Every response includes an X-API-Version header so you know which version handled your request.

Rate Limits

Rate limits protect the platform from abuse. Current quotas:

PlanAuthenticatedPublic (no auth)
Free / Starter60 req/min30 req/min per IP
Pro300 req/min60 req/min per IP
Enterprise1,800 req/minCustom

Each response includes these headers:

X-RateLimit-Limit: 300
X-RateLimit-Remaining: 287
X-RateLimit-Reset: 1713312000

Exceeding the limit returns HTTP 429 with a Retry-After header in seconds.

Error Handling

Tenseal uses conventional HTTP status codes. Errors return a predictable JSON envelope:

{
  "message": "The amount field is required.",
  "errors": {
    "amount": ["The amount field is required."]
  }
}
CodeMeaningWhen you see it
200 / 201OK / CreatedSuccessful request
204No ContentSuccessful delete
400Bad RequestMalformed JSON or query
401UnauthorizedMissing or invalid token
403ForbiddenAuthenticated but lacking permission
404Not FoundResource does not exist or is not yours
422Validation FailedField-level validation errors
429Too Many RequestsRate limit exceeded
500Server ErrorRetry with backoff

Register a User

POST/auth/registerPublic

Create a new account and receive an API token.

Body Parameters

FieldTypeRequiredDescription
namestringYesFull name
emailstringYesUnique email
passwordstringYesMin 8 characters
phonestringNoE.164 format recommended
curl -X POST https://tenseal.co/api/v1/auth/register \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Ada Lovelace",
    "email": "ada@example.com",
    "password": "Secret123!",
    "phone": "+2348012345678"
  }'
$r = Http::post('https://tenseal.co/api/v1/auth/register', [
    'name'     => 'Ada Lovelace',
    'email'    => 'ada@example.com',
    'password' => 'Secret123!',
]);
$token = $r['token'];
const r = await fetch('https://tenseal.co/api/v1/auth/register', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name, email, password })
});
const { token, user } = await r.json();
r = requests.post(
    'https://tenseal.co/api/v1/auth/register',
    json={'name': 'Ada', 'email': 'ada@example.com', 'password': 'Secret123!'}
)
token = r.json()['token']

Login

POST/auth/loginPublic
curl -X POST https://tenseal.co/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"ada@example.com","password":"Secret123!"}'
{
  "user":  { "id": 42, "name": "Ada Lovelace", "email": "ada@example.com" },
  "token": "1|abcdef...",
  "token_type": "Bearer"
}

Current User

GET/userBearer

Returns the authenticated user's profile.

Logout

POST/auth/logoutBearer

Revokes the current token. You'll need to login again.

Create Escrow Transaction

POST/escrow/transactionsBearer

Start a new escrow. Funds are held by Tenseal until released by the buyer or auto-released after the inspection period.

Body Parameters

FieldTypeRequiredDescription
titlestringYesShort description shown to both parties
amountdecimalYesAmount in selected currency
currencystringYesNGN, USD, EUR, GBP, GHS, KES, ZAR, XOF, CAD
buyer_emailstringYes*Or buyer_id if account exists
seller_emailstringYes*Or seller_id
descriptionstringNoDelivery terms / scope
inspection_daysintegerNoDefault 3, max 30
delivery_daysintegerNoDefault 7, max 90
fee_payerstringNobuyer, seller, or split
curl -X POST https://tenseal.co/api/v1/escrow/transactions \
  -H 'Authorization: Bearer $TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Website Development",
    "amount": 500000,
    "currency": "NGN",
    "buyer_email": "buyer@example.com",
    "seller_email": "seller@example.com",
    "description": "Full website redesign with CMS",
    "inspection_days": 7,
    "fee_payer": "split"
  }'
{
  "data": {
    "id": "esc_01hq7m8x",
    "title": "Website Development",
    "amount": 500000,
    "currency": "NGN",
    "status": "pending_funding",
    "buyer":  { "id": 101, "email": "buyer@example.com" },
    "seller": { "id": 77,  "email": "seller@example.com" },
    "funding_url": "https://app.tenseal.co/transaction/esc_01hq7m8x/fund",
    "created_at": "2026-04-17T10:30:00Z"
  }
}

List Transactions

GET/escrow/transactionsBearer

Supports filters: ?status=funded&role=buyer&from=2026-01-01&to=2026-04-17

Get a Transaction

GET/escrow/transactions/{id}Bearer

Release Funds

POST/escrow/transactions/{id}/releaseBearer

Buyer-only action. Manually releases funds before inspection period ends.

Raise a Dispute

POST/escrow/transactions/{id}/disputeBearer
{
  "reason": "not_as_described",
  "details": "Product arrived damaged",
  "evidence_urls": ["https://..."]
}

Invite Counterparty

POST/escrow/inviteBearer

Send a secure escrow invitation via email. Recipient gets a funding link even without a Tenseal account.

Milestones

Break large escrows into deliverable milestones — great for freelance and project work.

POST/escrow/transactions/{id}/milestonesBearer
POST/escrow/milestones/{id}/completeBearer

Inventory — Products

GET/inventory/productsBearer
POST/inventory/productsBearer
GET/inventory/products/{id}Bearer
PUT/inventory/products/{id}Bearer
DELETE/inventory/products/{id}Bearer
GET/inventory/products/low-stockBearer
POST /inventory/products
{
  "name":        "Cotton T-Shirt",
  "sku":         "TS-COT-001",
  "price":       5500,
  "cost_price":  2800,
  "quantity":    150,
  "low_stock_threshold": 10,
  "category":    "Apparel",
  "description": "100% organic cotton",
  "weight":      0.25,
  "dimensions":  { "length": 30, "width": 20, "height": 2 },
  "status":      "active"
}

Inventory — Orders

GET/inventory/ordersBearer
POST/inventory/ordersBearer
PUT/inventory/orders/{id}/statusBearer
PUT/inventory/orders/{id}/assignBearer
PUT/inventory/orders/{id}/trackingBearer

Orders support multiple channels: web_form, whatsapp, ussd, api, marketplace. Filter with ?channel=whatsapp.

PUT /inventory/orders/ord_123/status
{
  "status": "shipped",
  "note":   "Dispatched via GIG Logistics"
}

// Status values: pending, confirmed, processing, shipped,
// delivered, completed, cancelled, refunded

Stock Adjustment

PUT/inventory/products/{id}/stockBearer
{
  "type":       "add",   // "add" or "subtract"
  "adjustment": 50
}

Inventory — Staff

GET/inventory/staffBearer
POST/inventory/staffBearer

Roles: owner, manager, sales_rep, call_rep, delivery, accountant, viewer.

Inventory — Order Forms

GET/inventory/formsBearer
POST/inventory/formsBearer
GET/inventory/forms/{id}/embedBearer
GET/inventory/forms/{id}/previewBearer
POST/inventory/forms/{id}/duplicateBearer

Each form gets a public slug you can embed anywhere (see Public Form Submission).

Inventory — Analytics

GET/inventory/analyticsBearer
GET/inventory/analytics/salesBearer
GET/inventory/analytics/productsBearer

All analytics endpoints accept ?from=YYYY-MM-DD&to=YYYY-MM-DD&granularity=day|week|month.

Marketplace — Listings

GET/marketplace/listingsPublic
GET/marketplace/listings/{slug}Public
GET/marketplace/categoriesPublic
GET/marketplace/featuredPublic
POST/marketplace/my-listingsBearer

Marketplace — Place Order

POST/marketplace/listings/{id}/orderBearer

Automatically creates an escrow-backed transaction and returns a funding URL for the buyer.

Favorites

POST/marketplace/listings/{id}/favoriteBearer
GET/marketplace/favoritesBearer

Wallet

GET/walletBearer
GET/wallet/transactionsBearer
GET/wallet/summaryBearer

Fund Wallet

POST/wallet/fundBearer
{
  "amount":   100000,
  "currency": "NGN",
  "provider": "paystack"  // or "flutterwave", "stripe"
}
{
  "reference": "fund_01hq7m8x",
  "authorization_url": "https://checkout.paystack.com/...",
  "expires_at": "2026-04-17T11:30:00Z"
}

Withdraw

POST/wallet/withdrawBearer

Requires a verified payment method. Withdrawals are processed in batches — expect a wallet.withdrawal.completed webhook.

Payment Methods

GET/payment-methodsBearer
POST/payment-methodsBearer
POST/payment-methods/{id}/defaultBearer

Public Form Submission

POST/forms/{slug}/submitPublic

The endpoint used by the WordPress plugin, embedded iframes, and PWA offline forms. Authenticated by the form's public slug — no API key required.

curl -X POST https://tenseal.co/api/v1/forms/summer-orders/submit \
  -H 'Content-Type: application/json' \
  -d '{
    "customer_name":  "Ada Lovelace",
    "customer_email": "ada@example.com",
    "customer_phone": "+2348012345678",
    "items": [
      { "product_id": 12, "quantity": 2, "variant": "Blue-M" }
    ],
    "delivery_address": "12 Marina, Lagos",
    "payment_method": "cash_on_delivery"
  }'

Form Schema

GET/forms/{slug}/schemaPublic
GET/forms/{slug}/productsPublic

Returns the field definitions and available products for a public form. Used to render the form client-side.

Plans

GET/plansPublic

Returns all active subscription plans. Useful for building your own pricing page.

Webhooks — Overview

Webhooks let Tenseal push real-time events to your server as they happen. Configure endpoints in Settings → Webhooks.

  1. Create an HTTPS endpoint that accepts POST with JSON body.
  2. Register it from the dashboard (or via POST /webhooks).
  3. Respond with HTTP 200 within 10 seconds. Non-2xx responses are retried with exponential backoff for 24 hours.
  4. Verify the X-Tenseal-Signature header on every request.

Webhook Events

EventWhen it fires
escrow.createdNew escrow created
escrow.fundedBuyer funds the escrow
escrow.releasedFunds released to seller
escrow.disputedDispute raised
escrow.completedTransaction closed
order.createdNew inventory/marketplace order
order.status_changedOrder status updated
order.paidOrder payment received
product.low_stockStock hits low threshold
wallet.fundedWallet top-up succeeds
wallet.withdrawal.completedPayout hits bank
kyc.approved / kyc.rejectedKYC status change
{
  "event": "escrow.funded",
  "id":    "evt_01hq7m8x",
  "created_at": "2026-04-17T10:30:00Z",
  "data": {
    "id": "esc_01hq7m8x",
    "amount": 500000,
    "currency": "NGN",
    "status": "funded",
    "buyer":  { "email": "buyer@example.com" },
    "seller": { "email": "seller@example.com" }
  }
}

Signature Verification

Every webhook includes X-Tenseal-Signature, an HMAC-SHA256 of the raw request body using your webhook secret as the key.

$payload   = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_TENSEAL_SIGNATURE'] ?? '';
$secret    = env('TENSEAL_WEBHOOK_SECRET');
$expected  = hash_hmac('sha256', $payload, $secret);

if (!hash_equals($expected, $signature)) {
    abort(401, 'Invalid signature');
}

$event = json_decode($payload, true);
// handle $event['event'] ...
const crypto = require('crypto');
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-tenseal-signature'];
  const expected = crypto
    .createHmac('sha256', process.env.TENSEAL_WEBHOOK_SECRET)
    .update(req.body).digest('hex');
  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return res.status(401).send('Invalid signature');
  }
  const event = JSON.parse(req.body);
  res.sendStatus(200);
});
import hmac, hashlib, os

def verify(request):
    signature = request.headers.get('X-Tenseal-Signature', '')
    expected = hmac.new(
        os.environ['TENSEAL_WEBHOOK_SECRET'].encode(),
        request.body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

Official SDKs

We maintain lightweight client libraries so you don't have to hand-craft HTTP calls:

PHP / Laravel

composer require tenseal/php-sdk

Node.js / TypeScript

npm install @tenseal/sdk

Python

pip install tenseal-sdk

Ruby

gem install tenseal

Go

go get github.com/tenseal/go-sdk

Java / Kotlin

implementation 'co.tenseal:sdk:1.0'

Don't see your language? The API is plain REST + JSON — any HTTP client works. Community SDKs are listed at github.com/tenseal.

WordPress Plugin

Install "Tenseal Inventory" from the WordPress plugin directory, paste your API key, and embed any form using a shortcode:

[tenseal_form slug="summer-orders"]
[tenseal_products category="shoes" limit="8"]
[tenseal_cart theme="dark"]

The plugin also exposes hooks (tenseal_order_created, tenseal_stock_changed) so your theme or other plugins can react to events.

Mobile Apps (iOS / Android)

The same API powers our own mobile apps. You can use it to build:

  • Companion apps that extend your Tenseal account
  • White-label storefronts backed by Tenseal inventory
  • Sales-rep apps that assign orders and track deliveries

Device registration and push-notification endpoints live under /devices/*.

Sandbox / Test Mode

Sandbox is coming soon. A dedicated sandbox environment with isolated data and test cards is on the roadmap. For now you can safely explore the API against a new account on the live environment — webhook signing keys, auth tokens, and read endpoints are free to call. Contact us at developer support if you need a staging tenant provisioned manually.

When sandbox ships you'll get:

  • A separate base URL with isolated data
  • Test keys prefixed tns_test_ that never charge real cards
  • Provider-supplied test card numbers for Paystack / Flutterwave / Stripe flows

Need a hand?

Our developer relations team is online 24/7 on the platforms your team already lives on.

Contact Developer Support View on GitHub