Authentication & Security

This page documents the live public authentication surface served under https://api.gopiaxis.com/api.

Overview

The public payment API uses three authentication patterns:

  • Merchant API key for server-to-server payment, escrow, and disbursement calls

  • OAuth bearer token when acting on behalf of a registered Piaxis user

  • OTP for unregistered or sensitive external-money flows

Public Contract Notes

The current public router exposes these authentication endpoints:

  • GET /api/authorize

  • POST /api/token

  • POST /api/request-otp

The current public router does not expose these older paths:

  • /api/oauth/token

  • /api/oauth/refresh

  • /api/auth/request-otp

  • /api/auth/verify-otp

API Key Authentication

Merchant-controlled server-to-server calls use your merchant API key.

Environment variable naming used throughout these docs and SDK examples:

  • PIAXIS_API_KEY: your merchant API key

  • PIAXIS_CLIENT_ID: your merchant client identifier

  • PIAXIS_CLIENT_SECRET: your OAuth client secret

The PIAXIS_CLIENT_ID value is not only used for POST /api/token. It is also sent as the X-piaxis-Client-ID header on merchant-secured escrow and disbursement routes.

Current binding model:

  • one api-key resolves to one merchant profile

  • that merchant profile resolves to one merchant account

  • that merchant account id is the merchant’s own Piaxis account id

  • merchant_id, receiver_id, and recipient_id still refer to Piaxis Account.id values whenever those fields appear

  • on public collection routes, Piaxis derives the merchant collector from the authenticated api-key

  • do not send a MerchantProfile.id or UserProfile.id in those fields

In other words, the public payment API does not treat one API key as a multi-merchant key. If you are building a platform, the usual pattern is a single platform merchant account that collects first and then disburses onward, or explicit connected-account mapping for beneficiaries.

Baseline required headers

api-key: YOUR_MERCHANT_API_KEY
Content-Type: application/json

Additional required header on merchant-secured payout and escrow routes

These routes reject requests without the merchant client identifier header and return MISSING_CLIENT_ID when it is absent.

This currently includes:

  • POST /api/escrows/

  • GET /api/merchant-payments

  • POST /api/disbursements/quote

  • POST /api/disbursements

  • GET /api/disbursements

  • GET /api/disbursements/{disbursement_id}

  • POST /api/disbursements/{disbursement_id}/cancel

  • POST /api/escrow-disbursements

  • GET /api/escrow-disbursements

  • GET /api/escrow-disbursements/{disbursement_id}

  • POST /api/escrow-disbursements/{disbursement_id}/release

  • POST /api/escrow-disbursements/{disbursement_id}/cancel

X-piaxis-Client-ID: YOUR_MERCHANT_CLIENT_ID

Example

POST /api/payments/create HTTP/1.1
Host: api.gopiaxis.com
api-key: pk_live_1234567890abcdef
Content-Type: application/json

{
  "amount": 5000,
  "currency": "UGX",
  "payment_method": "mtn",
  "user_info": {
    "phone_number": "+256700000000"
  }
}

Use the same merchant client identifier as a header on the merchant-secured route families listed above:

api-key: YOUR_MERCHANT_API_KEY
X-piaxis-Client-ID: YOUR_MERCHANT_CLIENT_ID
Content-Type: application/json

Merchant account id and collection defaults

On the public collection routes, Piaxis derives the merchant account directly from the authenticated api-key.

That means:

  • direct payments credit the authenticated merchant account automatically

  • public escrows also default receiver_id to that same authenticated merchant account

  • if you send recipient_id on direct payments or receiver_id on public escrows, it must match the authenticated merchant Account.id

  • the public payment API does not currently expose a dedicated merchant whoami or /api/me route for API-key callers because standard collections do not need one

OAuth2 Flow

Use OAuth when the request acts on behalf of a registered Piaxis user, especially for piaxis_external payment flows.

Authorization Request

GET /api/authorize

Start the public OAuth flow.

Query Parameters:
  • merchant_id – Merchant account id (UUID)

  • external_user_id – Your stable user id for this merchant relationship

  • redirect_uri – Registered callback URL

Example

GET /api/authorize?merchant_id=096b723a-45c5-4957-94d7-747835136265&external_user_id=user_123&redirect_uri=https://yourapp.com/callback HTTP/1.1
Host: api.gopiaxis.com

Notes:

  • The public route requires a logged-in Piaxis user session in the browser.

  • redirect_uri must match the merchant OAuth client configuration.

  • The live public route does not rely on scope or state query parameters.

Authorization Callback

On success, Piaxis redirects back to your callback URL with a short-lived code:

GET /callback?code=AUTH_CODE_HERE HTTP/1.1
Host: yourapp.com

Token Exchange

POST /api/token

Exchange an authorization code for tokens.

Form Parameters:
  • grant_type – Must be authorization_code

  • code – Authorization code returned by /api/authorize

  • redirect_uri – Same redirect URI used during authorization

  • client_id – Merchant OAuth client id

  • client_secret – Merchant OAuth client secret

Example

POST /api/token HTTP/1.1
Host: api.gopiaxis.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=AUTH_CODE_HERE&redirect_uri=https://yourapp.com/callback&client_id=merchant_client_id&client_secret=merchant_client_secret

Response

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer",
  "expires_in": 3600,
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user_id": "096b723a-45c5-4957-94d7-747835136265",
  "merchant_id": "f530533e-3761-4cde-9c9d-88c5be6493bb",
  "external_user_id": "user_123"
}

Important:

  • The public token response returns a refresh_token.

  • The public token response also returns user_id for the connected Piaxis account.

  • Persist that user_id in your own beneficiary directory if you want to send later internal wallet payouts using recipients[].recipient_id.

  • The current public paymentAPI router does not expose a dedicated refresh endpoint.

  • For now, plan to restart the authorization flow when you need a fresh public access token.

Using The Bearer Token

Include the token on routes that require user-context authorization:

Authorization: Bearer YOUR_ACCESS_TOKEN

OTP Flow

The public OTP route is used before external-money calls where the payer is not already authenticated as a registered Piaxis user.

Request OTP

POST /api/request-otp

Request an OTP for an email address, phone number, or both.

JSON Parameters:
  • email (string) – Email address (optional)

  • phone_number (string) – E.164 phone number such as +256700000000 (optional)

Example

POST /api/request-otp HTTP/1.1
Host: api.gopiaxis.com
api-key: YOUR_API_KEY
Content-Type: application/json

{
  "email": "[email protected]",
  "phone_number": "+256700000000"
}

Use OTP In Money-Moving Calls

There is no standalone public verify-otp route in this router.

Instead:

  • escrow-family routes for unregistered users carry the OTP in user_info.otp

  • POST /api/payments/create also accepts an optional route parameter named mfa_code

Escrow example

{
  "amount": "50000.00",
  "currency_code": "UGX",
  "payment_method": "mtn",
  "terms": [
    {
      "type": "delivery_confirmation",
      "data": {
        "deadline": "2026-12-31T23:59:59Z"
      }
    }
  ],
  "user_info": {
    "email": "[email protected]",
    "phone_number": "+256700000000",
    "otp": "123456"
  }
}

Implementation Examples

Python

import os
import requests


class PiaxisAuth:
    def __init__(self, api_key: str, client_id: str, client_secret: str) -> None:
        self.api_key = api_key
        self.client_id = client_id
        self.client_secret = client_secret
        self.base_url = "https://api.gopiaxis.com"

    def api_headers(self) -> dict[str, str]:
        return {
            "api-key": self.api_key,
            "Content-Type": "application/json",
        }

      def merchant_secure_headers(self) -> dict[str, str]:
        return {
          **self.api_headers(),
          "X-piaxis-Client-ID": self.client_id,
        }

    def exchange_code(self, code: str, redirect_uri: str) -> dict:
        response = requests.post(
            f"{self.base_url}/api/token",
            data={
                "grant_type": "authorization_code",
                "code": code,
                "redirect_uri": redirect_uri,
                "client_id": self.client_id,
                "client_secret": self.client_secret,
            },
            headers={"Content-Type": "application/x-www-form-urlencoded"},
            timeout=30,
        )
        response.raise_for_status()
        return response.json()

    def request_otp(self, phone_number: str, email: str | None = None) -> dict:
        response = requests.post(
            f"{self.base_url}/api/request-otp",
            json={"phone_number": phone_number, "email": email},
            headers=self.api_headers(),
            timeout=30,
        )
        response.raise_for_status()
        return response.json()


piaxis = PiaxisAuth(
    api_key=os.environ["PIAXIS_API_KEY"],
    client_id=os.environ["PIAXIS_CLIENT_ID"],
    client_secret=os.environ["PIAXIS_CLIENT_SECRET"],
)

  # Use piaxis.merchant_secure_headers() for merchant-secured escrow and
  # disbursement routes that require X-piaxis-Client-ID.

Node.js

 const axios = require("axios");

 class PiaxisAuth {
   constructor({ apiKey, clientId, clientSecret }) {
     this.apiKey = apiKey;
     this.clientId = clientId;
     this.clientSecret = clientSecret;
     this.baseUrl = "https://api.gopiaxis.com";
   }

   apiHeaders() {
     return {
       "api-key": this.apiKey,
       "Content-Type": "application/json",
     };
   }

   merchantSecureHeaders() {
     return {
       ...this.apiHeaders(),
       "X-piaxis-Client-ID": this.clientId,
     };
   }

   async exchangeCode(code, redirectUri) {
     const response = await axios.post(
       `${this.baseUrl}/api/token`,
       new URLSearchParams({
         grant_type: "authorization_code",
         code,
         redirect_uri: redirectUri,
         client_id: this.clientId,
         client_secret: this.clientSecret,
       }),
       {
         headers: { "Content-Type": "application/x-www-form-urlencoded" },
       }
     );
     return response.data;
   }

   async requestOtp(phoneNumber, email = null) {
     const response = await axios.post(
       `${this.baseUrl}/api/request-otp`,
       { phone_number: phoneNumber, email },
       { headers: this.apiHeaders() }
     );
     return response.data;
   }
 }

// Use piaxis.merchantSecureHeaders() for merchant-secured escrow and
// disbursement routes that require X-piaxis-Client-ID.

Security Best Practices

  • Keep API keys and OAuth client secrets on the server only.

  • Use different credentials for sandbox, staging, and production.

  • Treat OTPs as single-use secrets and expire them quickly in your own UX.

  • Persist the returned payment_id or escrow_id and reconcile by id.

  • Verify webhook authenticity before changing internal state.