Quick Start
Get from zero to your first API call in under two minutes.
- Sign up for a Tenseal account at app.tenseal.co/register (escrow / wallet) or inventory.tenseal.co/register (inventory tenants).
- Head to Settings → API & Integrations and click Generate API Key. Copy it somewhere safe — you will only see it once.
- 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...
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:
| Plan | Authenticated | Public (no auth) |
|---|---|---|
| Free / Starter | 60 req/min | 30 req/min per IP |
| Pro | 300 req/min | 60 req/min per IP |
| Enterprise | 1,800 req/min | Custom |
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.
Pagination
List endpoints return Laravel-style paginated responses. Control the page and size with query parameters:
curl 'https://tenseal.co/api/v1/inventory/orders?page=2&per_page=50' \ -H 'Authorization: Bearer YOUR_KEY'
{
"data": [ /* 50 items */ ],
"links": {
"first": ".../orders?page=1",
"last": ".../orders?page=12",
"prev": ".../orders?page=1",
"next": ".../orders?page=3"
},
"meta": {
"current_page": 2,
"per_page": 50,
"total": 600,
"last_page": 12
}
}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."]
}
}| Code | Meaning | When you see it |
|---|---|---|
| 200 / 201 | OK / Created | Successful request |
| 204 | No Content | Successful delete |
| 400 | Bad Request | Malformed JSON or query |
| 401 | Unauthorized | Missing or invalid token |
| 403 | Forbidden | Authenticated but lacking permission |
| 404 | Not Found | Resource does not exist or is not yours |
| 422 | Validation Failed | Field-level validation errors |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Server Error | Retry with backoff |
Register a User
Create a new account and receive an API token.
Body Parameters
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Full name |
email | string | Yes | Unique email |
password | string | Yes | Min 8 characters |
phone | string | No | E.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
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
Returns the authenticated user's profile.
Logout
Revokes the current token. You'll need to login again.
Create Escrow Transaction
Start a new escrow. Funds are held by Tenseal until released by the buyer or auto-released after the inspection period.
Body Parameters
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Short description shown to both parties |
amount | decimal | Yes | Amount in selected currency |
currency | string | Yes | NGN, USD, EUR, GBP, GHS, KES, ZAR, XOF, CAD |
buyer_email | string | Yes* | Or buyer_id if account exists |
seller_email | string | Yes* | Or seller_id |
description | string | No | Delivery terms / scope |
inspection_days | integer | No | Default 3, max 30 |
delivery_days | integer | No | Default 7, max 90 |
fee_payer | string | No | buyer, 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
Supports filters: ?status=funded&role=buyer&from=2026-01-01&to=2026-04-17
Get a Transaction
Release Funds
Buyer-only action. Manually releases funds before inspection period ends.
Raise a Dispute
{
"reason": "not_as_described",
"details": "Product arrived damaged",
"evidence_urls": ["https://..."]
}Invite Counterparty
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.
Inventory — Products
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
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, refundedStock Adjustment
{
"type": "add", // "add" or "subtract"
"adjustment": 50
}Inventory — Staff
Roles: owner, manager, sales_rep, call_rep, delivery, accountant, viewer.
Inventory — Order Forms
Each form gets a public slug you can embed anywhere (see Public Form Submission).
Inventory — Analytics
All analytics endpoints accept ?from=YYYY-MM-DD&to=YYYY-MM-DD&granularity=day|week|month.
Marketplace — Listings
Marketplace — Search
Supports ?q=laptop&category=electronics&min_price=50000&max_price=500000&location=Lagos&sort=newest.
Marketplace — Place Order
Automatically creates an escrow-backed transaction and returns a funding URL for the buyer.
Favorites
Wallet
Fund Wallet
{
"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
Requires a verified payment method. Withdrawals are processed in batches — expect a wallet.withdrawal.completed webhook.
Payment Methods
Public Form Submission
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
Returns the field definitions and available products for a public form. Used to render the form client-side.
Plans
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.
- Create an HTTPS endpoint that accepts
POSTwith JSON body. - Register it from the dashboard (or via
POST /webhooks). - Respond with HTTP 200 within 10 seconds. Non-2xx responses are retried with exponential backoff for 24 hours.
- Verify the
X-Tenseal-Signatureheader on every request.
Webhook Events
| Event | When it fires |
|---|---|
escrow.created | New escrow created |
escrow.funded | Buyer funds the escrow |
escrow.released | Funds released to seller |
escrow.disputed | Dispute raised |
escrow.completed | Transaction closed |
order.created | New inventory/marketplace order |
order.status_changed | Order status updated |
order.paid | Order payment received |
product.low_stock | Stock hits low threshold |
wallet.funded | Wallet top-up succeeds |
wallet.withdrawal.completed | Payout hits bank |
kyc.approved / kyc.rejected | KYC 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
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.