How To Integrate with Othello Payments

A step-by-step guide for integrating crypto payments into your application using the Othello API.


Quick Context

Othello Payments is a non-custodial crypto payment gateway. Merchants create invoices, customers pay on-chain, blockchain watchers detect deposits and fire webhooks. Funds land in merchant-controlled deposit addresses and are automatically swept to a treasury wallet. The merchant never runs a blockchain node.

API Base URL: https://api.othello.money

Dashboard: https://dashboard.othello.money

Hosted payment page: https://pay.othello.money/invoice/{invoice_id}

Authentication: All API endpoints require Authorization: Bearer <api_key> unless noted otherwise.


Step 1 — Create Your Merchant Account (Dashboard)

All merchant onboarding happens through the Othello dashboard. No API calls are needed for this step.

  1. Go to dashboard.othello.money
  2. Sign up using Google social login or your email address
  3. You'll receive a verification code (Othello uses Privy for authentication) — enter the code to complete login
  4. Set your business name — this is the merchant name that will appear on invoices and payment pages for your customers
  5. Initialize your treasury key — click the button to generate your treasury seed phrase. This BIP-39 mnemonic is generated locally in your browser. Othello never sees or stores the plaintext seed phrase. You will be shown:
    • Your main treasury address (the wallet where swept funds accumulate)
    • Your 12-word seed phrase — back this up securely offline. If you lose it, Othello cannot recover it for you.
  6. Copy your API key — navigate to Settings > API Keys (/settings/api-keys in the dashboard). Copy the API key and store it securely. You'll need it for all API calls.

After completing these steps, your merchant account is fully set up. Everything from here on is API integration in your own codebase.


Step 2 — Configure Webhooks

Register the HTTPS endpoint on your server where Othello will send payment notifications. This is the first API call you'll make:

PUT /merchants/webhook Authorization: Bearer <api_key> Content-Type: application/json { "webhook_url": "https://yourapp.com/webhooks/payments" }

Response (the webhook_secret is returned exactly once — store it immediately):

JSON
{
"webhook_url": "https://yourapp.com/webhooks/payments",
"webhook_secret": "whsec_9f4b2c...64hexChars..."
}

Store webhook_secret securely (environment variable, secrets manager, etc.). You need it to verify that incoming webhooks are authentic. It is never returned again. If you lose it, calling PUT /merchants/webhook again will generate a new secret and invalidate the old one.

To check your current webhook configuration (the secret is never revealed):

GET /merchants/webhook Authorization: Bearer <api_key>
JSON
{ "webhook_url": "https://yourapp.com/webhooks/payments", "has_secret": true }

To remove the webhook:

DELETE /merchants/webhook Authorization: Bearer <api_key>

Step 3 — Create Invoices

When a customer needs to pay, your backend creates an invoice via the API:

POST /invoices Authorization: Bearer <api_key> Content-Type: application/json { "expected_amount": "25.00", "asset": "USDC", "expires_at": "2026-03-05T00:00:00Z", "success_url": "https://yourapp.com/payment-success", "failure_url": "https://yourapp.com/payment-failed", "metadata": { "order_id": "ORD-1234", "customer_email": "buyer@example.com" } }
FieldRequiredNotes
expected_amountYesDecimal string. The amount the customer should pay.
assetYesCrypto asset symbol. See Supported Assets table below.
expires_atNoISO 8601 timestamp. Auto-expires the invoice after this time.
deposit_addressNoReuse an existing address. If omitted, a fresh address is deterministically allocated.
success_urlNoURL to redirect customer to after successful payment.
failure_urlNoURL to redirect customer to if invoice expires or fails.
webhook_urlNoPer-invoice webhook URL (overrides merchant-level webhook from Step 2).
metadataNoArbitrary JSON object (max 4 KB). Echoed back in webhooks and GET responses. Use this to attach your internal order IDs, user IDs, etc.

Response:

JSON
{
"id": 42,
"merchant_id": "your-merchant-id",
"merchant_name": "Acme Coffee Co.",
"deposit_address": "0x7A9f...checksummed...",
"expected_amount": "25.00",
"expected_amount_usd": "25.00",
"asset": "USDC",
"asset_type": "erc20",
"token_contract_address": "0xA0b8...",
"status": "created",
"created_at": "2026-03-04T12:00:00+00:00",
"expires_at": "2026-03-05T00:00:00+00:00",
"updated_at": null,
"success_url": "https://yourapp.com/payment-success",
"failure_url": "https://yourapp.com/payment-failed",
"redirect_url": null,
"metadata": { "order_id": "ORD-1234", "customer_email": "buyer@example.com" }
}

Key fields for your integration:

  • id — Invoice ID. Use this to poll status, build payment URLs, and match webhooks.
  • deposit_address — The blockchain address the customer sends funds to.
  • status — Starts as "created". Progresses to "paid" when full payment is received.
  • merchant_name — Your business name (set in the dashboard), displayed on payment pages.

Step 4 — Direct the Customer to Pay

Option A: Hosted payment page (recommended)

Redirect or link the customer to:

https://pay.othello.money/invoice/{invoice_id}

This page displays the deposit address, amount, asset, QR code, countdown timer, and updates in real-time when payment is detected. When success_url is set, the customer is redirected there after payment.

Integration patterns:

  • Redirect: Send the customer's browser to the payment page.
  • New tab / popup: Open payment page in a new tab; listen for webhook on your server.
  • Embedded iframe: Embed the payment page inside your app.

Option B: Build your own payment UI

Use the invoice response data to build a custom payment screen showing:

  1. The deposit address (with QR code)
  2. The amount and asset
  3. A countdown timer based on expires_at

Polling as a fallback

GET /invoices/{invoice_id}

This endpoint is public (no auth required) — designed for payment page polling. Poll every 5-10 seconds. The status field will change from createdpartial_paidpaid.


Step 5 — Receive and Verify Webhooks

When an on-chain deposit is detected, Othello sends an HTTP POST to your registered webhook URL.

Webhook events

EventTrigger
invoice.partial_paidDeposit detected, but received amount < expected amount
invoice.paidReceived amount >= expected amount (fully paid)

Webhook payload

JSON
{
"event": "invoice.paid",
"invoice_id": 42,
"merchant_id": "your-merchant-id",
"deposit_address": "0x7A9f...",
"expected_amount": "25.00",
"asset": "USDC",
"status": "paid",
"metadata": { "order_id": "ORD-1234", "customer_email": "buyer@example.com" },
"timestamp": "2026-03-04T12:05:30.123456+00:00"
}

Webhook headers

HeaderValue
X-Webhook-Signaturesha256={hex_digest} — HMAC-SHA256 of {timestamp}.{body}
X-Webhook-TimestampUnix timestamp (seconds) when the webhook was sent
X-Webhook-EventEvent type, e.g. invoice.paid
Content-Typeapplication/json

Signature verification (mandatory)

Always verify the signature before trusting a webhook.

Algorithm:

  1. Read X-Webhook-Timestamp header (integer)
  2. Read the raw request body as a UTF-8 string
  3. Construct the message: {timestamp}.{body}
  4. Compute HMAC-SHA256 using your webhook_secret as the key
  5. Prepend sha256= and compare to the X-Webhook-Signature header

Python:

Python
import hmac, hashlib
def verify_webhook(body: bytes, headers: dict, secret: str) -> bool:
timestamp = headers.get("X-Webhook-Timestamp", "")
signature = headers.get("X-Webhook-Signature", "")
message = f"{timestamp}.{body.decode('utf-8')}"
expected = "sha256=" + hmac.new(
secret.encode(), message.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)

Node.js:

JavaScript
const crypto = require("crypto");
function verifyWebhook(body, headers, secret) {
const timestamp = headers["x-webhook-timestamp"];
const signature = headers["x-webhook-signature"];
const message = `${timestamp}.${body}`;
const expected = "sha256=" +
crypto.createHmac("sha256", secret).update(message).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

Security tips:

  • Reject webhooks where X-Webhook-Timestamp is more than 5 minutes old.
  • Always use constant-time comparison (hmac.compare_digest / timingSafeEqual).

Response expectations

  • Return any HTTP 2xx to acknowledge receipt. This marks the delivery as delivered.
  • Any non-2xx (or timeout) triggers a retry.

Retry policy

AttemptDelay
1st retry30 seconds
2nd retry2 minutes
3rd retry10 minutes
4th retry1 hour

After 5 total attempts (1 initial + 4 retries), the delivery is marked failed. You can manually retry via POST /webhook-deliveries/{delivery_id}/retry.

Idempotency

Your handler should be idempotent. The API guarantees at-most-once delivery per (invoice, event) pair, but network retries may cause duplicates. Track processed (invoice_id, event) pairs to avoid double-crediting.


Step 6 — Handle Payment in Your App

When you receive an invoice.paid webhook (after verifying the signature):

  1. Look up the order/user using the metadata you attached to the invoice
  2. Credit the customer's account, fulfill the order, grant access, etc.
  3. Return HTTP 200 to acknowledge the webhook

For invoice.partial_paid, notify the customer that more funds are needed — the invoice remains open.


Supported Assets

AssetTypeChainDecimals
ETHnativeethereum18
USDCerc20ethereum6
EURCerc20ethereum6
POLnativepolygon18
USDC_POLYGONerc20polygon6
USDT_POLYGONerc20polygon6
BNBnativebsc18
USDC_BSCerc20bsc18
USDT_BSCerc20bsc18
ETH_BASEnativebase18
USDC_BASEerc20base6
USDT_BASEerc20base6
ETH_ARBnativearbitrum18
USDC_ARBerc20arbitrum6
USDT_ARBerc20arbitrum6
BTCUTXObitcoin8
LTCUTXOlitecoin8
SOLnativesolana9
USDC_SOLsplsolana6
USDT_SOLsplsolana6
PYUSD_SOLsplsolana6
USD1_SOLsplsolana6

Use the asset symbol exactly as shown in the asset column when creating invoices.


Invoice Lifecycle

created ──> pending ──> partial_paid ──> paid │ │ └──> expired └──> expired └──> failed └──> failed
StatusMeaningWebhook?
createdInvoice exists, no deposit detected yetNo
pendingSystem is monitoring for depositsNo
partial_paidSome funds received, less than expectedYes (invoice.partial_paid)
paidFull amount (or more) receivedYes (invoice.paid)
expiredPast expires_at or manually expiredNo
failedMarked failed (e.g. reorg, manual)No

paid, expired, and failed are terminal — no further changes.


API Reference

ActionMethodEndpointAuth
Set webhook URLPUT/merchants/webhookYes
Get webhook configGET/merchants/webhookYes
Remove webhookDELETE/merchants/webhookYes
Get merchant detailsGET/merchants/{merchant_id}Yes
Update merchant namePATCH/merchants/{merchant_id}Yes
Create invoicePOST/invoicesYes
List invoicesGET/invoicesYes
Get invoice (public)GET/invoices/{invoice_id}No
Mark invoice expiredPOST/invoices/{invoice_id}/mark-expiredYes
Mark invoice failedPOST/invoices/{invoice_id}/mark-failedYes
Export invoices (Excel)GET/invoices/export/excelYes
Export invoices (PDF)GET/invoices/export/pdfYes
Allocate deposit addressPOST/deposit-addressYes
List deposit addressesGET/deposit-addresses/{merchant_id}Yes
List primary addressesGET/addresses/primaryYes
List all deposit addressesGET/addresses/depositYes
Get balancesGET/balances/{merchant_id}Yes
Dashboard overviewGET/dashboard/overviewYes
Revenue chartGET/dashboard/revenue-chartYes
Trigger sweepPOST/sweep or /sweep/{chain}Yes
List webhook deliveriesGET/webhook-deliveriesYes
Retry failed deliveryPOST/webhook-deliveries/{delivery_id}/retryYes
Health checkGET/healthNo

Minimal Working Integration (Python)

A minimal Flask server that creates invoices and handles webhooks. Assumes you've already completed Step 1 (dashboard setup) and Step 2 (webhook configuration).

Python
import hmac
import hashlib
import requests
from flask import Flask, request, jsonify
app = Flask(__name__)
API_BASE = "https://api.othello.money"
API_KEY = "YOUR_API_KEY" # from dashboard Settings > API Keys
WEBHOOK_SECRET = "YOUR_SECRET" # from PUT /merchants/webhook response
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
@app.post("/create-payment")
def create_payment():
"""Customer wants to pay — create an invoice and return the payment URL."""
data = request.json
resp = requests.post(f"{API_BASE}/invoices", headers=HEADERS, json={
"expected_amount": data["amount"],
"asset": data.get("asset", "USDC"),
"metadata": {"order_id": data["order_id"]},
"success_url": "https://yourapp.com/payment-success",
})
resp.raise_for_status()
invoice = resp.json()
return jsonify({
"payment_url": f"https://pay.othello.money/invoice/{invoice['id']}",
"invoice_id": invoice["id"],
"deposit_address": invoice["deposit_address"],
})
@app.post("/webhooks/payments")
def handle_webhook():
"""Receive payment notification from Othello."""
body = request.get_data()
if not verify_signature(body, request.headers):
return "Invalid signature", 401
payload = request.get_json()
if payload["event"] == "invoice.paid":
order_id = payload.get("metadata", {}).get("order_id")
fulfill_order(order_id)
return jsonify({"status": "ok"})
def verify_signature(body: bytes, headers) -> bool:
ts = headers.get("X-Webhook-Timestamp", "")
sig = headers.get("X-Webhook-Signature", "")
msg = f"{ts}.{body.decode('utf-8')}"
expected = "sha256=" + hmac.new(
WEBHOOK_SECRET.encode(), msg.encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, sig)
def fulfill_order(order_id):
print(f"Fulfilling order: {order_id}")

Setup Checklist

  • Dashboard account created at dashboard.othello.money
  • Business name set in the dashboard
  • Treasury key initialized, seed phrase backed up securely
  • API key copied from Settings > API Keys
  • PUT /merchants/webhook — webhook URL configured, webhook_secret stored securely
  • Webhook endpoint implemented with signature verification
  • Invoice creation integrated into your checkout flow
  • Customer directed to hosted payment page (or custom UI built)
  • Webhook handler fulfills orders on invoice.paid
  • Idempotency implemented in webhook handler

Debugging & Monitoring

Check webhook deliveries

GET /webhook-deliveries Authorization: Bearer <api_key>

Each delivery includes status (pending, delivered, failed), http_status_code, error_message, and attempt_count.

Retry a failed delivery

POST /webhook-deliveries/{delivery_id}/retry Authorization: Bearer <api_key>

Poll invoice status

If you suspect a missed webhook, poll the invoice directly:

GET /invoices/{invoice_id}

No auth required. The status field reflects the current payment state.