Introduction

Welcome to the MoneroPay documentation. Here you will find information on how to deploy it and API specification.

    .-"""-.
   /       \
  |__/\_/\__|
   \       /
    '-...-'
       _
    \('v')/
     /   \
    (\_ _/)
     ^^ ^^

Feel free to open a PR, raise an issue or request a new feature.

For related discussions join our Matrix room.

We also offer commercial support for merchants and developers. Contact us at info@digilol.net.

What is this?

A backend service for receiving, sending and tracking status of Monero payments.

MoneroPay provides a simple HTTP API for merchants or individuals who want to accept XMR.

MoneroPay supports optional status updates via HTTP Callbacks.

MoneroPay is not a plugin for an existing e-commerce solution. It is a standalone backend daemon that can be used for any purpose. Some use cases are:

  • Online stores/e-commerce
  • Donation/fundraiser websites
  • ATMs
  • Paid services like parking or bus ticket applications
  • Shell scripts and programs for any purpose

MoneroPay utilizes:

  • Monero Wallet RPC
  • PostgreSQL

Installation

Docker Compose

The MoneroPay repository contains docker-compose.yaml which is a complete setup of MoneroPay + PostgreSQL + monero-wallet-rpc.

Create the .env, docker-compose.override.yaml files from the .env.example, docker-compose.override.yaml.example and configure it.

cp .env.example .env
cp docker-compose.override.yaml.example docker-compose.override.yaml
vim .env
vim docker-compose.override.yaml

Create the data directory and add your wallet files.

mkdir -p data/wallet
cp wallet{,.keys} data/wallet
touch data/wallet/wallet.passwd # if your wallet is password protected, write it in this file. Else leave empty.
chown -R 1000:1000 data/wallet # change owner to prevent permission errors

Docker compose configuration expects the wallet keys file to be called wallet.keys. You can choose to rename your wallet keys file or change the name in the docker-compose.yaml file.

Now you should have 3 files under data/wallet directory: wallet, wallet.keys and wallet.passwd.

Bring it up.

docker compose up -d

You can check the logs using:

docker compose logs -f

Native

Running MoneroPay without Docker.

Prerequisites

  • Go compiler
  • PostgreSQL server (and an empty database)
  • monero-wallet-rpc server
  • A Monero wallet (view-only or full)

Compilation

git clone https://gitlab.com/moneropay/moneropay.git
cd moneropay
go build

Now MoneroPay help page can be checked via ./moneropay -h

Security

MoneroPay is meant to be run inside local network and should not be exposed to the public internet. If it needs to be exposed, we suggest reverse proxying using a web server. This way authentication and SSL encryption can be used to secure your connection.

Below is an example configuration for reverse proxying and enabling basic authentication on top of MoneroPay using Caddy2 web server.

/etc/caddy/Caddyfile:

moneropay.example.net {
	basicauth {
		admin your_password_hash
	}
	reverse_proxy localhost:5000
}

Generate the your_password_hash field using the caddy hash-password command.

Scalability

It is possible differentiate MoneroPay instances based on their X-Moneropay-Address HTTP header. This header contains the wallet's primary address.

Merchant workflow

Here is a sequence diagram that displays the interaction between the merchant and MoneroPay.

diag1

Table of Endpoints

MethodURIInput
GET/balance
GET/health
POST/receive{"amount": 123000000, "description": "Stickers", "callback_url": "http://merchant"}
GET/receive/:address?min=&max=
POST/transfer{"destinations": [{"amount": 1337000000, "address": "47stn..."}]}
GET/transfer/:tx_hash

All the amount fields are in atomic units, also known as piconero.

GET /balance

Get the entire wallet balance.

Request

curl -s -X GET "${endpoint}/balance"

Response

{
  "total": 2513444800,
  "unlocked": 800000000,
}

For querying received amounts to subaddresses take a look at GET /receive/:address endpoint.

GET /health

Check if required services are up.

Request

curl -s -X GET "${endpoint}/health"

Response

200 OK

{
  "status": 200,
  "services": {
    "walletrpc": true,
    "postgresql": true
  }
}

503 Service Unavailable

{
  "status": 503,
  "services": {
    "walletrpc": false,
    "postgresql": true
  }
}

POST /receive

Create a subaddress for incoming transfers.

Request

curl -s -X POST "${endpoint}/receive" \
  -H 'Content-Type: application/json' \
  -d '{"amount": 123000000, "description": "Server expenses", "callback_url": "http://merchant/callback/moneropay_tio2foogaaLo9olaew4o"}'

"complete" will be set to true inside callback and GET /receive/:subaddress payload, when unlocked amount is equal or more to the one specified in "amount".

"description" and "callback_url" are optional. If "callback_url" is set, MoneroPay will send a POST request to URL specified with a payload described here.

Response

{
  "address": "84WsptnLmjTYQjm52SMkhQWsepprkcchNguxdyLkURTSW1WLo3tShTnCRvepijbc2X8GAKPGxJK9hfQhLHzoKSxh7y8Yqrg",
  "amount": 123000000,
  "description": "Server expenses",
  "created_at": "2022-07-18T11:54:49.780542861Z"
}

GET /receive/:address

View incoming transfers for a subaddress.

Request

curl -s -X GET "${endpoint}/receive/${address}?min=${min_height}&max=${max_height}"

Optionally filter "transactions" by min and max block height.

Response

{
  "amount": {
    "expected": 200000000,
    "covered": {
      "total": 200000000,
      "unlocked": 200000000
    }
  },
  "complete": true,
  "description": "Donation to Kernal",
  "created_at": "2022-07-11T19:04:24.574583Z",
  "transactions": [
    {
      "amount": 200000000,
      "confirmations": 10,
      "double_spend_seen": false,
      "fee": 9200000,
      "height": 2402648,
      "timestamp": "2022-07-11T19:19:05Z",
      "tx_hash": "0c9a7b40b15596fa9a06ba32463a19d781c075120bb59ab5e4ed2a97ab3b7f33",
      "unlock_time": 0,
      "locked": false
    }
  ]
}

POST /transfer

Transfer to a single or multiple recipients. If necessary, split the transfer into multiple transactions.

Request

curl -s -X POST "${endpoint}/transfer" \
	-H 'Content-Type: application/json' \
	-d '{"destinations": [{"amount": 1337000000, "address": "47stn..."}]}'

This transaction uses balance of the wallet's Primary Account.

Response

{
  "amount": 1337000000,
  "fee": 87438594,
  "tx_hash": "5ca34...",
  "tx_hash_list": ["5ca34...", "cf448..."],
  "destinations": [
    {
      "amount": 1337000000,
      "address": "47stn..."
    }
  ]
}

Deprecated: TxHash (tx_hash) field will be removed the next major release (3.0.0). Please use TxHashList (tx_hash_list) instead. See here for more details.

GET /transfer/:tx_hash

Get information about transaction via its hash.

Request

curl -s -X GET "${endpoint}/transfer/${tx_hash}"

Response

{
  "amount": 79990000,
  "fee": 9110000,
  "state": "completed",
  "transfer": [
    {
      "amount": 79990000,
      "address": "453biCQpM6oSSr7jgTwmtC9YfiXUWZY1wEfSZJD4r6rf7mPqPj8NZpp7WYpAHVq7p69SYa1B1zMN6SeRc8exYi1WEenqu2c"
    }
  ],
  "confirmations": 15,
  "double_spend_seen": false,
  "height": 2407445,
  "timestamp": "2022-07-18T11:37:50Z",
  "unlock_time": 10,
  "tx_hash": "cf448effb86f24f81476c0012a6636700488e13accd91f8f43302ae90fed25ce"
}

Callback payload

MoneroPay contains a goroutine which checks for new incoming and recently unlocked transactions every 30 seconds and sends a POST request to "callback_url" specified in the POST /receive endpoint with the following payload:

{
  "amount": {
    "expected": 0,
    "covered": {
      "total": 200000000,
      "unlocked": 200000000
    }
  },
  "complete": true,
  "description": "Donation to Kernal",
  "created_at": "2022-07-11T19:04:24.574583Z",
  "transaction": {
    "amount": 200000000,
    "confirmations": 10,
    "double_spend_seen": false,
    "fee": 9200000,
    "height": 2402648,
    "timestamp": "2022-07-11T19:19:05Z",
    "tx_hash": "0c9a7b40b15596fa9a06ba32463a19d781c075120bb59ab5e4ed2a97ab3b7f33",
    "unlock_time": 0,
    "locked": false
  }
}

Additionally the same data can be retrieved via GET /receive/:address endpoint and is encouraged to check once in a while at the higher end application in case of a downtime or failed callback delivery.