← Ofixers for partners

Embed the Ofixers widget

Your customers find, book, chat with, call and pay verified Ofixers artisans without leaving your app. You open a session from your server, show the link in a web view or iframe, and listen for events. This guide covers everything you need.

1. Your keys

Ofixers gives you a live key (ofx_live_…) and a test key (ofx_test_…). Keep them on your server only, never in an app, a browser or a public repository. If a key leaks, tell us and we'll revoke it at once.

2. Open a session from your server

When your customer taps "Book an artisan", your server asks Ofixers for a one-time link for that customer:

// Node.js (any language works — it's one HTTPS call)
const res = await fetch("https://api.ofixers.com/api/v1/partner/sessions", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${process.env.OFIXERS_KEY}`,   // ofx_live_…
  },
  body: JSON.stringify({
    user: {
      id: customer.id,          // your own id for this customer
      name: customer.fullName,
      phone: customer.phone,    // Nigerian mobile number
    },
  }),
});
const { data } = await res.json();
// data.url → https://widget.ofixers.com/s/… (works once, expires in 5 minutes)

Send data.url to your app and open it straight away. Each session lasts about two hours; open a new one for the next visit.

3a. On a website

Ofixers must have your site's origin (for example https://app.yourbank.com) on file, or the browser will refuse to show the widget.

<iframe
  src="SESSION_URL"
  allow="camera; microphone; clipboard-write"
  style="width:100%;max-width:440px;height:720px;border:0;border-radius:16px">
</iframe>

<script>
  window.addEventListener("message", (e) => {
    if (e.origin !== "https://widget.ofixers.com" || e.data?.source !== "ofixers-widget") return;
    if (e.data.type === "widget.close") closeYourModal();
    if (e.data.type === "brief.paid") refreshYourOrders();
  });
</script>

3b. In a mobile app

Open the URL in a web view. Allow camera and microphone (for photos, video and calls).

React Native

import { WebView } from "react-native-webview";

<WebView
  source={{ uri: sessionUrl }}
  mediaPlaybackRequiresUserAction={false}
  allowsInlineMediaPlayback
  mediaCapturePermissionGrantType="grant"
  onMessage={(e) => {
    const event = JSON.parse(e.nativeEvent.data);
    if (event.type === "widget.close") navigation.goBack();
  }}
/>

Flutter

final controller = WebViewController()
  ..setJavaScriptMode(JavaScriptMode.unrestricted)
  ..addJavaScriptChannel('OfixersWidget', onMessageReceived: (m) {
      final event = jsonDecode(m.message);
      if (event['type'] == 'widget.close') Navigator.pop(context);
    })
  ..loadRequest(Uri.parse(sessionUrl));
// webview_flutter_android: grant camera/mic in onPermissionRequest.

Android (WebView)

webView.settings.javaScriptEnabled = true
webView.settings.mediaPlaybackRequiresUserGesture = false
webView.webChromeClient = object : WebChromeClient() {
  override fun onPermissionRequest(request: PermissionRequest) = request.grant(request.resources)
}
webView.addJavascriptInterface(object {
  @JavascriptInterface fun postMessage(json: String) { /* handle event */ }
}, "OfixersWidget")
webView.loadUrl(sessionUrl)

iOS (WKWebView)

let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true
config.userContentController.add(self, name: "ofixers")   // window.webkit.messageHandlers.ofixers
let webView = WKWebView(frame: .zero, configuration: config)
webView.load(URLRequest(url: URL(string: sessionUrl)!))
// Add NSCameraUsageDescription and NSMicrophoneUsageDescription to Info.plist.

4. Widget events

Every event has source: "ofixers-widget" and a type:

These are for updating your screens. For anything you rely on (records, accounting), use webhooks.

5. Webhooks

Ofixers POSTs JSON to your webhook URL for: job.posted, job.artisan_engaged, brief.paid, brief.arrived, brief.completed, brief.approved, dispute.opened, payment.refunded, revenue_share.credited, user_wallet.funded, user_wallet.transferred_to_partner, payout.sent and payout.failed. We retry for about a day if your server doesn't answer 2xx. Always check the signature:

import crypto from "crypto";

// Use the raw request body, not re-serialised JSON.
function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;   // reject replays
  return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
// header = req.headers["ofixers-signature"]  →  "t=1760000000,v1=…"

6. Customer wallets

Each customer gets their own wallet with an account number, shown in the widget. They fund it by bank transfer (a small deposit fee, shown in the widget, is taken from each transfer) and pay for jobs from it. Money in a customer wallet can't be withdrawn to a bank. Refunds from disputes go back to it.

With the customer's agreement you can move their balance to your own wallet. They approve with a code we text to their phone:

POST /api/v1/partner/balance-transfers            { "user": { "id": "cust_123" }, "amount": 5000 }
→ { "request_id": "…" }   // we text the customer a 6-digit code

POST /api/v1/partner/balance-transfers/{request_id}/confirm   { "code": "123456" }

7. Your earnings

You earn a share of Ofixers' commission on every completed job your customers book, credited to your wallet when the artisan is paid. Withdrawals only go to the bank account Ofixers verified for your company.

GET  /api/v1/partner/account                  // balance
POST /api/v1/partner/withdrawals              { "amount": 50000 }   // or omit amount for everything
GET  /api/v1/partner/withdrawals
PUT  /api/v1/partner/payout-settings          { "enabled": true, "frequency": "weekly", "min_amount": 20000 }

8. Testing in the sandbox

Use your ofx_test_… key the same way. Sandbox sessions only see test artisans and use test money, so you can go through a whole job (book, agree a price, pay, arrive, approve) without touching real artisans or real money. Sandbox wallets have no account number: the customer taps "Add test money" in the widget instead.

Questions? Contact your Ofixers partner manager.