Build on IntaOps network.
A friendly, step-by-step guide for technical and non-technical personel. Do everything two ways, click through the dashboard, or call the REST API.
✅ Prerequisites
- ✓IntaOps KYB-verified account — Corporation, NGO, and/or LLC entities.
- ✓Valid access — at least the Occasional commitment plan.
Get your credentials
The API uses OAuth2 client credentials. Create a Developer Application in Developer Portal to get a client_id and client_secret. Exchange them for a short-lived access token, then send that token on every call:
# 1) exchange client credentials for an access token (expires in ~5 min)
curl -X POST https://infra.intaops.io/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=Your_client_id" \
-d "client_secret=Your_client_secret" \
-d "scope=intaops/marketplace.publish intaops/marketplace.read intaops/bookings.read"
# → { "access_token": "eyJhbGci…", "token_type": "Bearer", "expires_in": 300, "scope": "…" }
# 2) reuse it as a Bearer token on every request below
export INTAOPS_TOKEN="eyJhbGci…"Install the SDK
Every endpoint is plain REST. But one package per language wraps the OAuth handshake and the marketplace catalog, bookings, and webhook APIs.
pip install intaopsnpm install @intaops/connectorgo get github.com/IntaOps/connector-go@v0.1.5Create a schema mapping
Map your own field names onto IntaOps' standard schema once, so you can upload catalogues in your native shape.
- 1Open the dashboard and go to Upload, then the Schema mapping tab.
- 2Click New mapping and name it (your schema → IntaOps).
- 3For each of your data fields, pick the matching IntaOps field (name, base_price, currency, category, sku, …). Map your own product id onto sku: every import creates a new product, so re-running an import duplicates your catalogue rather than updating it. Import once, then edit in the dashboard. Add an optional transformation (uppercase, trim, format phone) or a type conversion (e.g. string → number).
- 4Save. From now on you can upload files in your own shape and IntaOps maps them automatically.
curl -X POST https://infra.intaops.io/api/interop/transform/schema-mapping/ \
-H "Authorization: Bearer Your_access_token" \
-H "Content-Type: application/json" \
-d '{
"entity_schema_name": "my_products",
"intaops_schema_name": "product",
"field_mappings": [
{ "source_field": "title", "target_field": "name" },
{ "source_field": "cost", "target_field": "base_price", "type_conversion": "number" },
{ "source_field": "ccy", "target_field": "currency", "transformation": "uppercase" }
]
}'Upload your catalog
Push single or batch products in JSON or CSV. Products go live immediately, discoverable to IntaOps users within ~100ms.
- 1Go to Upload
- 2Paste or drop your JSON/CSV (up to 1,000 rows). Each row is validated inline, fix any red rows.
- 3Click Import. Successful rows land in your Catalog and start showing to buyers straight away.
- 4Prefer one at a time? Use Catalog, add item for the single product form.
curl -X POST https://infra.intaops.io/api/interop/products/bulk-import/ \
-H "Authorization: Bearer Your_access_token" \
-H "Content-Type: application/json" \
-d '{
"products": [{
"name": "Basic Life Support — Victoria Island",
"sku": "bls-lagos-vi",
"base_price": 45000,
"currency": "NGN",
"category": "ride_hailing",
"subcategory": "ambulance",
"location": { "type": "Point", "coordinates": [3.3792, 6.5244] }
}]
}'Note: category must be a ProductCategory value and subcategory must be one of that category's — see Categories below; a mismatched pair is rejected outright. location uses GeoJSON order — [longitude, latitude]. Products are created active; to pause one, PUT /api/interop/products/PRODUCT_ID/update/ with a discontinued status.
Subscribe to webhooks
Get notifications on the dashboard (no action require) or HTTPS POST the moment something happens, a product is pushed, an order is placed, a payment settles.
- 1Go to Upload, the Webhooks tab.
- 2Click Add endpoint and paste your HTTPS URL.
- 3Tick the events you care about — product.*, order.*, payment.*, sync/data.*.
- 4Subscribe, then copy the signing secret. Use it to verify signatures (see Verify webhooks below).
curl -X POST https://infra.intaops.io/api/interop/webhooks/subscribe/ \
-H "Authorization: Bearer Your_access_token" \
-H "Content-Type: application/json" \
-d '{
"url": "https://www.yourapp.com/intaops/webhooks",
"events": ["product.created", "order.placed", "payment.completed"],
"max_retries": 5
}'Note: The response returns signing_secret once. Fire a test delivery any time from the dashboard, or POST /api/interop/webhooks/SUBSCRIPTION_ID/test/.
Keep systems in sync
Automate sync between your system and IntaOps in real-time, batch, or on-demand with the conflict strategy you choose. Unlike bulk upload, this one calls YOUR API, so a sync needs a target_connection describing where to reach you.
- 1Go to Upload, the Data sync tab.
- 2Click New sync and pick a resource type (e.g. product).
- 3Choose a mode (real-time / batch / on-demand / hybrid), a direction (push / pull / bi-directional), and a conflict strategy (latest-wins, source-wins, target-wins, merge, manual).
- 4Fill in Target connection: your base URL, the auth scheme (bearer / basic / api key / none) and its secret, plus a path per resource type.
- 5Save, then Run to kick off the first sync.
# 1) create a sync config — target_connection says where to reach YOUR API
curl -X POST https://infra.intaops.io/api/interop/sync/config/ \
-H "Authorization: Bearer Your_access_token" \
-H "Content-Type: application/json" \
-d '{
"resource_type": "product",
"sync_mode": "real_time",
"sync_direction": "push",
"conflict_resolution": "latest_wins",
"target_connection": {
"base_url": "https://app.domain.com",
"auth_type": "bearer",
"secret": "YOUR_API_TOKEN",
"paths": { "product": "/v1/products" },
"timeout": 30,
"max_retries": 2
}
}'
# auth_type: bearer | basic | api_key | none (basic also takes "username",
# api_key also takes "api_key_header", default X-API-Key)
# secret: stored encrypted; reads come back as has_secret: true, never the value
# conflict_resolution: latest_wins | source_wins | target_wins | merge | manual
# latest_wins — whichever copy was updated most recently
# source_wins — the incoming record always overwrites
# target_wins — your existing copy is always kept
# merge — keep every field either side has; the incoming
# record wins where both set the same field
# manual — nothing is written until you resolve it yourself
# 2) run it (CONFIG_ID comes from the response above)
curl -X POST https://infra.intaops.io/api/interop/sync/CONFIG_ID/execute/ \
-H "Authorization: Bearer Your_access_token" \
-H "Content-Type: application/json" \
-d '{ "data_type": "product", "force": false }'Note: What to expect on your side. PUSH sends one POST per record to base_url + paths[resource_type], with an Intaops-Idempotency-Key header that stays the same across retries. Reply 2xx for accepted, 409 if your copy has diverged, and IntaOps files a conflict instead of a failure. PULL sends a GET with ?since=<last sync, ISO-8601>&limit=<batch size> and accepts either a bare JSON array or an object wrapping one under data / results / items; each record needs a stable id. Timestamps may carry a timezone (2026-01-01T00:00:00Z) or none. Redirects are never followed and the target must resolve to a public address, so localhost and private ranges are rejected.
Bulk sync from your database, JSON, or CSV
Got a full catalog? IntaOps never reaches into your database, you push batches to us (up to ~1,000 products per call). Three ways, same result: products go live within ~100ms. Tip: set up a schema mapping first if you'd rather send your native field names.
Best for a live catalog. A small script reads your DB in pages and pushes each batch — schedule it (cron) for continuous sync. IntaOps never connects to your database; you push to us.
import os, requests
BASE = "https://infra.intaops.io"
TOKEN = os.environ["INTAOPS_TOKEN"] # from POST /oauth/token
CHUNK = 1000 # push 1,000 per call
# ISO 4217 exponents. Most currencies have 2 decimals; these are the
# exceptions. NGN is 2, so kobo -> naira divides by 100.
ZERO_DECIMAL = {"JPY", "KRW", "VND", "XAF", "XOF", "XPF", "CLP", "ISK", "UGX", "RWF"}
THREE_DECIMAL = {"BHD", "IQD", "JOD", "KWD", "LYD", "OMR", "TND"}
def to_major(amount, currency): # minor units -> major (kobo -> naira)
exp = 0 if currency in ZERO_DECIMAL else 3 if currency in THREE_DECIMAL else 2
return amount / (10 ** exp)
def to_product(row): # map YOUR columns -> IntaOps fields
return {
"name": row["title"],
# Your own stable id, carried so you can reconcile the two
# catalogues. IntaOps does NOT match on it, re-running this import
# creates a second copy of every product.
"sku": row["id"],
"base_price": to_major(row["price"], row["currency"]),
"currency": row["currency"],
"category": row["kind"],
}
def push(products):
r = requests.post(
f"{BASE}/api/interop/products/bulk-import/",
headers={"Authorization": f"Bearer {TOKEN}"},
json={"products": products}, timeout=60,
)
r.raise_for_status()
return r.json()["data"]["results"]
# page through your catalog and push each batch
batch = []
for row in db.execute("SELECT id, title, price, currency, kind FROM products WHERE active"):
batch.append(to_product(row))
if len(batch) >= CHUNK:
push(batch); batch = []
if batch:
push(batch)
# schedule (continuous): every 15 min via cron → */15 * * * * python sync.pyAlready have a JSON array? POST it straight to bulk-import, or drop the .json in the dashboard Upload tab.
curl -X POST https://infra.intaops.io/api/interop/products/bulk-import/ \
-H "Authorization: Bearer Your_access_token" \
-H "Content-Type: application/json" \
--data @products.json # file holds { "products": [ … ] }Export your DB to CSV and upload the file, a single bad row is skipped. Or drop the .csv in the dashboard Upload tab.
curl -X POST https://infra.intaops.io/api/interop/products/csv-upload/ \
-H "Authorization: Bearer Your_access_token" \
-F "file=@products.csv"API reference
Every marketplace endpoint. All calls send Authorization: Bearer Your_access_token (the access token from Get credentials).
/api/interop/products/bulk-import/Batch up to 1,000 products.
curl -X POST https://infra.intaops.io/api/interop/products/bulk-import/ \
-H "Authorization: Bearer Your_access_token" \
-H "Content-Type: application/json" \
-d '{ "products": [{ "name": "Basic Life Support — Victoria Island", "sku": "bls-lagos-vi", "base_price": 45000, "currency": "NGN", "category": "ride_hailing", "subcategory": "ambulance" }] }'{ "success": true, "data": { "results": { "successful": 1, "failed": 0 } } }/api/interop/products/mine/Your own catalog across all statuses (active + discontinued), newest first.
curl -X GET 'https://infra.intaops.io/api/interop/products/mine/?page_size=100' \
-H "Authorization: Bearer Your_access_token"{ "success": true, "data": { "products": [{ "product_id": "9f3a…", "name": "Basic Life Support — Victoria Island", "base_price": 45000, "currency": "NGN", "status": "active" }] } }/api/interop/products/{product_id}/update/Change any field, or set status=discontinued to remove it from the Services tab.
curl -X PUT https://infra.intaops.io/api/interop/products/PRODUCT_ID/update/ \
-H "Authorization: Bearer Your_access_token" \
-H "Content-Type: application/json" \
-d '{ "base_price": 4000, "status": "active" }'{ "success": true, "data": { "product_id": "9f3a…", "updated": true } }/api/interop/products/search/The discovery IntaOps app uses — text + category + price filters. Active products only.
curl -X GET 'https://infra.intaops.io/api/interop/products/search/?q=ambulance&category=ambulance&max_price=60000&page_size=20' \
-H "Authorization: Bearer Your_access_token"{ "success": true, "data": { "products": [{ "product_id": "9f3a…", "name": "Basic Life Support — Victoria Island", "base_price": 45000, "entity_name": "Naija Medics" }], "page": 1, "total": 12 } }/api/interop/transform/schema-mapping/Map your field names onto IntaOps' standard schema.
curl -X POST https://infra.intaops.io/api/interop/transform/schema-mapping/ \
-H "Authorization: Bearer Your_access_token" \
-H "Content-Type: application/json" \
-d '{ "entity_schema_name": "my_products", "intaops_schema_name": "product", "field_mappings": [{ "source_field": "title", "target_field": "name" }] }'{ "success": true, "data": { "mapping_id": "map_…" } }/api/interop/sync/config/Automate real-time / batch sync of a resource (see Guide 4). target_connection is required before a sync can run — it tells IntaOps where to reach your API.
curl -X POST https://infra.intaops.io/api/interop/sync/config/ \
-H "Authorization: Bearer Your_access_token" \
-H "Content-Type: application/json" \
-d '{ "resource_type": "product", "sync_mode": "real_time", "sync_direction": "push", "conflict_resolution": "latest_wins",
"target_connection": { "base_url": "https://app.domain.com", "auth_type": "bearer", "secret": "YOUR_API_TOKEN", "paths": { "product": "/v1/products" } } }'{ "success": true, "data": { "config_id": "cfg_…", "target_connection": { "base_url": "https://app.domain.com", "auth_type": "bearer", "has_secret": true, "paths": { "product": "/v1/products" } } } }/api/interop/webhooks/subscribe/Receive product / order / payment events.
curl -X POST https://infra.intaops.io/api/interop/webhooks/subscribe/ \
-H "Authorization: Bearer Your_access_token" \
-H "Content-Type: application/json" \
-d '{ "url": "https://app.domain.com/intaops/webhooks", "events": ["order.placed","payment.completed"], "max_retries": 5 }'{ "success": true, "data": { "subscription_id": "sub_…", "signing_secret": "whsec_…" } }Receive & verify webhooks
IntaOps POSTs events to your URL with an Intaops-Signature: t=<unix>,v1=<hmac> header. Recompute the HMAC-SHA256 over {t}.{raw_body} with your signing secret, compare in constant time, and reject if t is older than 5 minutes.
POST /intaops/webhooks HTTP/1.1
Host: app.domain.com
Intaops-Signature: t=1718712902,v1=5f0c2e… # HMAC-SHA256(secret, "{t}.{raw_body}")
Intaops-Event-Type: payment.completed
Intaops-Event-Id: evt_9z8y7x
Content-Type: application/json
{
"type": "payment.completed",
"event_id": "evt_9z8y7x",
"ts": "2026-06-18T12:35:02Z",
"data": { "booking_id": "bk_1a2b3c4d", "amount": 4800, "currency": "NGN" }
}import hmac, hashlib, time
# header = request.headers["Intaops-Signature"] ("t=...,v1=...")
def verify(secret: str, header: str, raw_body: bytes) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t, v1 = parts.get("t", ""), parts.get("v1", "")
if not (t and v1) or abs(time.time() - int(t)) > 300:
return False # missing / stale — reject replays
signed = (t + ".").encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(v1, expected)import crypto from "crypto";
// header = req.header("Intaops-Signature") ("t=...,v1=...")
function verify(secret, header, rawBody) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const { t, v1 } = parts;
if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const signed = t + "." + rawBody; // rawBody = the RAW request body string
const expected = crypto.createHmac("sha256", secret).update(signed).digest("hex");
return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}Categories
Every product carries a category and, optionally, a subcategory drawn from it. The category decides how the listing is priced and what else we ask for; the subcategory is what buyers actually search on — “transportation” is too coarse to tell an ambulance from a yacht charter.
Send the values in the table, not the display names. A subcategory that does not belong to its category is rejected rather than dropped, so a bad pair fails loudly instead of quietly filing your listing under the wrong search.
| Category | category | subcategory |
|---|---|---|
| Health | healthcare | pharmacymedicalsother |
| Fashion | ecommerce | wearsother |
| Transportation | ride_hailing | hail_rideambulanceyacht_bookinghelicopter_bookingother |
| Food | food_delivery | food_deliverygroceryother |
| Other | other | other |
Omitting subcategory is allowed — it was added after the API shipped, so existing integrations keep working untouched. Every category carries an other so a listing that fits nothing above is still filed honestly rather than forced into a neighbouring value.
Terms explained
The words you'll meet in the dashboard and in the payloads below, in plain language. Skip it if they're already familiar.
SKUStock Keeping Unit- Your own code for one sellable item. It is stored alongside the product and returned on reads, but it is not matched on during import. IntaOps assigns its own product_id and every import creates a new product.
sync configA standing sync arrangement- A saved rule that says which resource to sync, in which direction, how often, and who wins when both sides changed the same record.
target connectionWhere to reach your API- The base URL of your own system, how to authenticate against it, and the path for each resource. Required before a sync can run.
conflict strategyWho wins a tie- What to do when a record changed on both sides at once: latest-wins (most recent edit), source-wins, target-wins, or manual (nothing is overwritten,require your decision).