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.

Python
pip install intaops
TypeScript
npm install @intaops/connector
Go
go get github.com/IntaOps/connector-go@v0.1.5
Guide 1

Create a schema mapping

Map your own field names onto IntaOps' standard schema once, so you can upload catalogues in your native shape.

Point & click (dashboard)
  1. 1Open the dashboard and go to Upload, then the Schema mapping tab.
  2. 2Click New mapping and name it (your schema → IntaOps).
  3. 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).
  4. 4Save. From now on you can upload files in your own shape and IntaOps maps them automatically.
Or create it with one API call
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" }
    ]
  }'
Guide 2

Upload your catalog

Push single or batch products in JSON or CSV. Products go live immediately, discoverable to IntaOps users within ~100ms.

Point & click (dashboard)
  1. 1Go to Upload
  2. 2Paste or drop your JSON/CSV (up to 1,000 rows). Each row is validated inline, fix any red rows.
  3. 3Click Import. Successful rows land in your Catalog and start showing to buyers straight away.
  4. 4Prefer one at a time? Use Catalog, add item for the single product form.
Or batch-upsert via the API
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.

Guide 3

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.

Point & click (dashboard)
  1. 1Go to Upload, the Webhooks tab.
  2. 2Click Add endpoint and paste your HTTPS URL.
  3. 3Tick the events you care about — product.*, order.*, payment.*, sync/data.*.
  4. 4Subscribe, then copy the signing secret. Use it to verify signatures (see Verify webhooks below).
Or subscribe via the API
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/.

Guide 4

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.

Point & click (dashboard)
  1. 1Go to Upload, the Data sync tab.
  2. 2Click New sync and pick a resource type (e.g. product).
  3. 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).
  4. 4Fill in Target connection: your base URL, the auth scheme (bearer / basic / api key / none) and its secret, plus a path per resource type.
  5. 5Save, then Run to kick off the first sync.
Or configure + run via the API
# 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.

🗄️From your database

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.py
JSON

Already 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": [ … ] }
CSV

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).

POST/api/interop/products/bulk-import/
Upload / upsert catalog

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" }] }'
Response
{ "success": true, "data": { "results": { "successful": 1, "failed": 0 } } }
GET/api/interop/products/mine/
List your products

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"
Response
{ "success": true, "data": { "products": [{ "product_id": "9f3a…", "name": "Basic Life Support — Victoria Island", "base_price": 45000, "currency": "NGN", "status": "active" }] } }
PUT/api/interop/products/{product_id}/update/
Update or discontinue a product

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" }'
Response
{ "success": true, "data": { "product_id": "9f3a…", "updated": true } }
GET/api/interop/products/search/
Search the public catalog

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"
Response
{ "success": true, "data": { "products": [{ "product_id": "9f3a…", "name": "Basic Life Support — Victoria Island", "base_price": 45000, "entity_name": "Naija Medics" }], "page": 1, "total": 12 } }
POST/api/interop/transform/schema-mapping/
Create a 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" }] }'
Response
{ "success": true, "data": { "mapping_id": "map_…" } }
POST/api/interop/sync/config/
Create a 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" } } }'
Response
{ "success": true, "data": { "config_id": "cfg_…", "target_connection": { "base_url": "https://app.domain.com", "auth_type": "bearer", "has_secret": true, "paths": { "product": "/v1/products" } } } }
POST/api/interop/webhooks/subscribe/
Subscribe to webhooks

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 }'
Response
{ "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.

What you receive
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" }
}
Verify — Python
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)
Verify — Node / TypeScript
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.

Categorycategorysubcategory
Healthhealthcare
pharmacymedicalsother
Fashionecommerce
wearsother
Transportationride_hailing
hail_rideambulanceyacht_bookinghelicopter_bookingother
Foodfood_delivery
food_deliverygroceryother
Otherother
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).