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.
- Go to dashboard.othello.money
- Sign up using Google social login or your email address
- You'll receive a verification code (Othello uses Privy for authentication) — enter the code to complete login
- Set your business name — this is the merchant name that will appear on invoices and payment pages for your customers
- 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.
- Copy your API key — navigate to Settings > API Keys (
/settings/api-keysin 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):
{"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>
{ "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"
}
}
| Field | Required | Notes |
|---|---|---|
expected_amount | Yes | Decimal string. The amount the customer should pay. |
asset | Yes | Crypto asset symbol. See Supported Assets table below. |
expires_at | No | ISO 8601 timestamp. Auto-expires the invoice after this time. |
deposit_address | No | Reuse an existing address. If omitted, a fresh address is deterministically allocated. |
success_url | No | URL to redirect customer to after successful payment. |
failure_url | No | URL to redirect customer to if invoice expires or fails. |
webhook_url | No | Per-invoice webhook URL (overrides merchant-level webhook from Step 2). |
metadata | No | Arbitrary JSON object (max 4 KB). Echoed back in webhooks and GET responses. Use this to attach your internal order IDs, user IDs, etc. |
Response:
{"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:
- The deposit address (with QR code)
- The amount and asset
- 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 created → partial_paid → paid.
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
| Event | Trigger |
|---|---|
invoice.partial_paid | Deposit detected, but received amount < expected amount |
invoice.paid | Received amount >= expected amount (fully paid) |
Webhook payload
{"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
| Header | Value |
|---|---|
X-Webhook-Signature | sha256={hex_digest} — HMAC-SHA256 of {timestamp}.{body} |
X-Webhook-Timestamp | Unix timestamp (seconds) when the webhook was sent |
X-Webhook-Event | Event type, e.g. invoice.paid |
Content-Type | application/json |
Signature verification (mandatory)
Always verify the signature before trusting a webhook.
Algorithm:
- Read
X-Webhook-Timestampheader (integer) - Read the raw request body as a UTF-8 string
- Construct the message:
{timestamp}.{body} - Compute HMAC-SHA256 using your
webhook_secretas the key - Prepend
sha256=and compare to theX-Webhook-Signatureheader
Python:
import hmac, hashlibdef 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:
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-Timestampis 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
| Attempt | Delay |
|---|---|
| 1st retry | 30 seconds |
| 2nd retry | 2 minutes |
| 3rd retry | 10 minutes |
| 4th retry | 1 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):
- Look up the order/user using the
metadatayou attached to the invoice - Credit the customer's account, fulfill the order, grant access, etc.
- 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
| Asset | Type | Chain | Decimals |
|---|---|---|---|
ETH | native | ethereum | 18 |
USDC | erc20 | ethereum | 6 |
EURC | erc20 | ethereum | 6 |
POL | native | polygon | 18 |
USDC_POLYGON | erc20 | polygon | 6 |
USDT_POLYGON | erc20 | polygon | 6 |
BNB | native | bsc | 18 |
USDC_BSC | erc20 | bsc | 18 |
USDT_BSC | erc20 | bsc | 18 |
ETH_BASE | native | base | 18 |
USDC_BASE | erc20 | base | 6 |
USDT_BASE | erc20 | base | 6 |
ETH_ARB | native | arbitrum | 18 |
USDC_ARB | erc20 | arbitrum | 6 |
USDT_ARB | erc20 | arbitrum | 6 |
BTC | UTXO | bitcoin | 8 |
LTC | UTXO | litecoin | 8 |
SOL | native | solana | 9 |
USDC_SOL | spl | solana | 6 |
USDT_SOL | spl | solana | 6 |
PYUSD_SOL | spl | solana | 6 |
USD1_SOL | spl | solana | 6 |
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
| Status | Meaning | Webhook? |
|---|---|---|
created | Invoice exists, no deposit detected yet | No |
pending | System is monitoring for deposits | No |
partial_paid | Some funds received, less than expected | Yes (invoice.partial_paid) |
paid | Full amount (or more) received | Yes (invoice.paid) |
expired | Past expires_at or manually expired | No |
failed | Marked failed (e.g. reorg, manual) | No |
paid, expired, and failed are terminal — no further changes.
API Reference
| Action | Method | Endpoint | Auth |
|---|---|---|---|
| Set webhook URL | PUT | /merchants/webhook | Yes |
| Get webhook config | GET | /merchants/webhook | Yes |
| Remove webhook | DELETE | /merchants/webhook | Yes |
| Get merchant details | GET | /merchants/{merchant_id} | Yes |
| Update merchant name | PATCH | /merchants/{merchant_id} | Yes |
| Create invoice | POST | /invoices | Yes |
| List invoices | GET | /invoices | Yes |
| Get invoice (public) | GET | /invoices/{invoice_id} | No |
| Mark invoice expired | POST | /invoices/{invoice_id}/mark-expired | Yes |
| Mark invoice failed | POST | /invoices/{invoice_id}/mark-failed | Yes |
| Export invoices (Excel) | GET | /invoices/export/excel | Yes |
| Export invoices (PDF) | GET | /invoices/export/pdf | Yes |
| Allocate deposit address | POST | /deposit-address | Yes |
| List deposit addresses | GET | /deposit-addresses/{merchant_id} | Yes |
| List primary addresses | GET | /addresses/primary | Yes |
| List all deposit addresses | GET | /addresses/deposit | Yes |
| Get balances | GET | /balances/{merchant_id} | Yes |
| Dashboard overview | GET | /dashboard/overview | Yes |
| Revenue chart | GET | /dashboard/revenue-chart | Yes |
| Trigger sweep | POST | /sweep or /sweep/{chain} | Yes |
| List webhook deliveries | GET | /webhook-deliveries | Yes |
| Retry failed delivery | POST | /webhook-deliveries/{delivery_id}/retry | Yes |
| Health check | GET | /health | No |
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).
import hmacimport hashlibimport requestsfrom flask import Flask, request, jsonifyapp = Flask(__name__)API_BASE = "https://api.othello.money"API_KEY = "YOUR_API_KEY" # from dashboard Settings > API KeysWEBHOOK_SECRET = "YOUR_SECRET" # from PUT /merchants/webhook responseHEADERS = {"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.jsonresp = 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", 401payload = 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_secretstored 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.