commit 72ccb985186968c6a214da23d82f34fef49234e2 Author: Rob Riddle Date: Thu Feb 1 16:52:44 2018 -0500 Initial release of JSON payment protocol module diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..19fc17e --- /dev/null +++ b/.jshintrc @@ -0,0 +1,32 @@ +{ + "esversion": 6, + "bitwise": true, + "camelcase": true, + "curly": true, + "devel": false, + "eqeqeq": true, + "eqnull": false, + "freeze": true, + "funcscope": false, + "immed": true, + "indent": 2, + "latedef": "nofunc", + "maxdepth": 4, + "maxerr": 99999, // so that jshint doesn't give up on a file with tons of errors + "maxlen": 120, + "maxparams": 4, + "maxstatements": 35, + "mocha": true, + "newcap": true, + "noarg": true, + "node": true, + "noempty": true, + "nonew": true, + "quotmark": "single", + "regexp": true, + "smarttabs": false, + "strict": true, + "trailing": true, + "undef": true, + "unused": true +} diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..b726568 --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +examples \ No newline at end of file diff --git a/examples/bitcoinRpc.js b/examples/bitcoinRpc.js new file mode 100644 index 0000000..2c1c000 --- /dev/null +++ b/examples/bitcoinRpc.js @@ -0,0 +1,246 @@ +'use strict'; +const async = require('async'); +const request = require('request'); +const promptly = require('promptly'); + +const JsonPaymentProtocol = require('../index'); //or require('json-payment-protocol') +const paymentProtocol = new JsonPaymentProtocol({strictSSL: false}); + +let config = { + network: 'test', + currency: 'BTC', + rpcServer: { + username: 'fakeUser', + password: 'fakePassword', + ipAddress: '127.0.0.1', + port: '18332' + } +}; + +if (config.rpcServer.username === 'fakeUser') { + return console.log('You should update the config in this file to match the actual configuration of your bitcoind' + + ' RPC interface'); +} + +/** + * While this client does show effective use of json payment protocol, it may not follow best practices generate + * payments via bitcoinRPC. We do not recommend copying this code verbatim in any product designed for actual users. + */ + +let paymentUrl; +let requiredFee = 0; +let outputObject = {}; + +async.waterfall([ + function askForPaymentUrl(cb) { + promptly.prompt('What is the payment protocol uri?', {required: true}, cb); + }, + function retrievePaymentRequest(uri, cb) { + paymentProtocol.getRawPaymentRequest(uri, (err, rawResponse) => { + if (err) { + console.log('Error retrieving payment request', err); + return cb(err); + } + return cb(null, rawResponse); + }); + }, + function parsePaymentRequest(rawResponse, cb) { + paymentProtocol.parsePaymentRequest(rawResponse.rawBody, rawResponse.headers, (err, paymentRequest) => { + if (err) { + console.log('Error parsing payment request', err); + return cb(err); + } + return cb(null, paymentRequest); + }); + }, + function checkAndDisplayPaymentRequestToUser(paymentRequest, cb) { + requiredFee = (paymentRequest.requiredFeePerByte * 1024) / 1e8; + + //Make sure request is for the currency we support + if (paymentRequest.currency.toLowerCase() !== config.currency.toLowerCase()) { + console.log('Server requested a payment in', paymentRequest.currency, 'but we are configured to accept', + config.currency); + return cb(new Error('Payment request currency did not match our own')); + } + + if (paymentRequest.network.toLowerCase() !== config.network.toLowerCase()) { + console.log('Server requested a payment on the', paymentRequest.network, 'network but we are configured for the', + config.network, 'network'); + return cb(new Error('Payment request network did not match our own')); + } + + console.log('Server is requesting payments for:'); + console.log('---'); + + paymentRequest.outputs.forEach(function (output) { + let cryptoAmount = round(output.amount / 1e8, 8); + console.log(cryptoAmount + ' to ' + output.address); + outputObject[output.address] = cryptoAmount; + }); + + console.log('---'); + + paymentUrl = paymentRequest.paymentUrl; + + cb(); + }, + function createRawTransaction(cb) { + let createCommand = { + jsonrpc: '1.0', + method: 'createrawtransaction', + params: [ + [], + outputObject + ] + }; + + execRpcCommand(createCommand, (err, rawTransaction) => { + if (err) { + console.log('Error creating raw transaction', err); + return cb(err); + } + else if (!rawTransaction) { + console.log('No raw tx generated'); + return cb(new Error('No tx generated')); + } + else { + return cb(null, rawTransaction); + } + }); + }, + function fundRawTransaction(rawTransaction, cb) { + let fundCommand = { + jsonrpc: '1.0', + method: 'fundrawtransaction', + params: [ + rawTransaction, + { + feeRate: requiredFee + } + ] + }; + + execRpcCommand(fundCommand, (err, fundedRawTransaction) => { + if (err) { + console.log('Error funding transaction', err); + return cb(err); + } + if (!fundedRawTransaction) { + console.log('No funded tx generated'); + return cb(new Error('No funded tx generated')); + } + else { + cb(null, fundedRawTransaction.hex); + } + }); + }, + function signRawTransaction(fundedRawTransaction, cb) { + let command = { + jsonrpc: '1.0', + method: 'signrawtransaction', + params: [fundedRawTransaction] + }; + + execRpcCommand(command, function (err, signedTransaction) { + if (err) { + console.log('Error signing transaction:', err); + return cb(err); + } + if (!signedTransaction) { + console.log('Bitcoind did not return a signed transaction'); + return cb(new Error('Missing signed tx')); + } + cb(null, signedTransaction.hex); + }); + }, + function displayTransactionToUserForApproval(signedRawTransaction, cb) { + let command = { + jsonrpc: '1.0', + method: 'decoderawtransaction', + params: [signedRawTransaction] + }; + + execRpcCommand(command, function (err, decodedTransaction) { + if (err) { + console.log('Error signing transaction:', err); + return cb(err); + } + if (!decodedTransaction) { + console.log('Bitcoind did not return a decoded transaction'); + return cb(new Error('Missing decoded tx')); + } + + console.log(JSON.stringify(decodedTransaction, null, 2)); + + promptly.confirm('Send payment shown above? (y/n)', function (err, accept) { + if (!accept) { + console.log('Payment cancelled'); + return cb(new Error('Payment Cancelled')); + } + return cb(null, signedRawTransaction); + }); + }); + }, + function sendTransactionToServer(signedRawTransaction, cb) { + paymentProtocol.sendPayment(config.currency, signedRawTransaction, paymentUrl, function (err, response) { + if (err) { + console.log('Error sending payment to server', err); + return cb(err); + } + else { + console.log('Payment accepted by server'); + return cb(null, signedRawTransaction); + } + }); + }, + //Note we only broadcast AFTER a SUCCESS response from the server + function broadcastPayment(signedRawTransaction, cb) { + let command = { + jsonrpc: '1.0', + method: 'sendrawtransaction', + params: [signedRawTransaction] + }; + + execRpcCommand(command, function (err, signedTransaction) { + if (err) { + console.log('Error broadcasting transaction:', err); + return cb(err); + } + if (!signedTransaction) { + console.log('Bitcoind failed to broadcast transaction'); + return cb(new Error('Failed to broadcast tx')); + } + cb(); + }); + } +]); + +function execRpcCommand(command, callback) { + request + .post({ + url: 'http://' + config.rpcServer.ipAddress + ':' + config.rpcServer.port, + body: command, + json: true, + auth: { + user: config.rpcServer.username, + pass: config.rpcServer.password, + sendImmediately: false + } + }, function (err, response, body) { + if (err) { + return callback(err); + } + if (body.error) { + return callback(body.error); + } + if (body.result) { + return callback(null, body.result); + } + return callback(); + }); +} + +function round(value, places) { + let tmp = Math.pow(10, places); + return Math.ceil(value * tmp) / tmp; +} diff --git a/index.js b/index.js new file mode 100644 index 0000000..7fad7dc --- /dev/null +++ b/index.js @@ -0,0 +1,163 @@ +'use strict'; +//Native +const crypto = require('crypto'); +const query = require('querystring'); +const url = require('url'); +const util = require('util'); + +//Modules +const _ = require('lodash'); +const request = require('request'); + +function PaymentProtocol(options) { + this.options = _.merge({ + strictSSL: true + }, options); +} + +/** + * Makes a request to the given url and returns the raw JSON string retrieved as well as the headers + * @param paymentUrl {string} the payment protocol specific url + * @param callback {function} (err, body, headers) + */ +PaymentProtocol.prototype.getRawPaymentRequest = function getRawPaymentRequest(paymentUrl, callback) { + let paymentUrlObject = url.parse(paymentUrl); + + //Detect 'bitcoin:' urls and extract payment-protocol section + if (paymentUrlObject.protocol !== 'http' && paymentUrlObject.protocol !== 'https') { + let uriQuery = query.decode(paymentUrlObject.query); + if (!uriQuery.r) { + return callback(new Error('Invalid payment protocol url')); + } + else { + paymentUrl = uriQuery.r; + } + } + + let requestOptions = _.merge(this.options, { + url: paymentUrl, + headers: { + 'Accept': 'application/payment-request' + } + }); + + request.get(requestOptions, (err, response) => { + if (err) { + return callback(err); + } + if (response.statusCode !== 200) { + return callback(new Error(response.body.toString())); + } + + return callback(null, {rawBody: response.body, headers: response.headers}); + }); +}; + +/** + * Makes a request to the given url and returns the raw JSON string retrieved as well as the headers + * @param url {string} the payment protocol specific url (https) + */ +PaymentProtocol.prototype.getRawPaymentRequestAsync = util.promisify(PaymentProtocol.prototype.getRawPaymentRequest); + +/** + * Given a raw payment protocol body, parses it and validates it against the digest header + * @param rawBody {string} Raw JSON string retrieved from the payment protocol server + * @param headers {object} Headers sent by the payment protocol server + * @param callback {function} (err, paymentRequest) + */ +PaymentProtocol.prototype.parsePaymentRequest = function parsePaymentRequest(rawBody, headers, callback) { + let paymentRequest; + + if (!rawBody) { + return callback(new Error('Parameter rawBody is required')); + } + if (!headers) { + return callback(new Error('Parameter headers is required')); + } + + try { + paymentRequest = JSON.parse(rawBody); + } + catch (e) { + return callback(new Error(`Unable to parse request - ${e}`)); + } + + if (!headers.digest) { + return callback(new Error('Digest missing from response headers')); + } + + let digest = headers.digest.split('=')[1]; + let hash = crypto.createHash('sha256').update(rawBody, 'utf8').digest('hex'); + + if (digest !== hash) { + return callback(new Error(`Response body hash does not match digest header. Actual: ${hash} Expected: ${digest}`)); + } + + return callback(null, paymentRequest); +}; + +/** + * Given a raw payment protocol body, parses it and validates it against the digest header + * @param rawBody {string} Raw JSON string retrieved from the payment protocol server + * @param headers {object} Headers sent by the payment protocol server + */ +PaymentProtocol.prototype.parsePaymentRequestAsync = util.promisify(PaymentProtocol.prototype.parsePaymentRequest); + +/** + * Sends a given payment to the server for validation + * @param currency {string} Three letter currency code of proposed transaction (ie BTC, BCH) + * @param signedRawTransaction {string} Hexadecimal format raw signed transaction + * @param url {string} the payment protocol specific url (https) + * @param callback {function} (err, response) + */ +PaymentProtocol.prototype.sendPayment = function sendPayment(currency, signedRawTransaction, url, callback) { + let paymentResponse; + + //Basic sanity checks + if (typeof signedRawTransaction !== 'string') { + return callback(new Error('signedRawTransaction must be a string')); + } + if (!/^[0-9a-f]+$/i.test(signedRawTransaction)) { + return callback(new Error('signedRawTransaction must be in hexadecimal format')); + } + + let requestOptions = _.merge(this.options, { + url: url, + headers: { + 'Content-Type': 'application/payment' + }, + body: JSON.stringify({ + currency: currency, + transactions: [signedRawTransaction] + }) + }); + + request.post(requestOptions, (err, response) => { + if (err) { + return callback(err); + } + if (response.statusCode !== 200) { + return callback(new Error(response.body.toString())); + } + + try { + paymentResponse = JSON.parse(response.body); + } + catch (e) { + return callback(new Error('Unable to parse response from server')); + } + + callback(null, paymentResponse); + }); +}; + +/** + * Sends a given payment to the server for validation + * @param currency {string} Three letter currency code of proposed transaction (ie BTC, BCH) + * @param signedRawTransaction {string} Hexadecimal format raw signed transaction + * @param url {string} the payment protocol specific url (https) + */ +PaymentProtocol.prototype.sendPaymentAsync = util.promisify(PaymentProtocol.prototype.sendPayment); + +module.exports = PaymentProtocol; + diff --git a/package.json b/package.json new file mode 100644 index 0000000..0e10f7e --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "json-payment-protocol", + "version": "0.1.0", + "description": "Simple interface for retrieving JSON payment requests and submitting payments", + "main": "index.js", + "dependencies": { + "lodash": "^4.17.4", + "request": "^2.83.0" + }, + "devDependencies": { + "async": "^2.6.0", + "promptly": "^2.2.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "BitPay", + "repository": { + "type": "git", + "url": "https://github.com/bitpay/jsonPaymentProtocol.git" + }, + "license": "MIT" +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..72fea8a --- /dev/null +++ b/readme.md @@ -0,0 +1,91 @@ +### JSON Payment Protocol Interface + +This is the first version of the JSON payment protocol interface. If you have questions about the specification itself, [view the documentation](specification.md). + +### Getting Started + +`npm install json-payment-protocol` + +### Usage + +We support both callbacks and promises. For promises just add Async to the end of the function name. Be careful to follow the notes about when to broadcast your payment. **Broadcasting a payment before getting a success notification back from the server in most cases will lead to a failed payment for the sender.** The sender will bear the cost of paying transaction fees yet again to get their money back. + +#### Callbacks +``` +const JsonPaymentProtocol = require('json-payment-protocol'); +const paymentProtocol = new JsonPaymentProtocol(); + +let requestUrl = 'bitcoin:?r=https://test.bitpay.com/i/Jr629pwsXKdTCneLyZja4t'; + +paymentProtocol.getRawPaymentRequest(requestUrl, function (err, response) { + if (err) { + return console.log('Error retrieving payment request', err); + } + paymentProtocol.parsePaymentRequest(response.body, response.headers, function (err, paymentRequest) { + if (err) { + return console.log('Error parsing payment request', err); + } + + console.log('Payment request retrieved'); + console.log(paymentRequest); + + //TODO: Create the rawTransaction and sign it in your wallet instead of this, do NOT broadcast yet + let currency = 'BTC' + let signedRawTransaction = '02000000010c2b0d60448d5cdfebe222014407bdb408b8427f837447484911efddea700323000000006a47304402201d3ed3117f1968c3b0a078f15f8462408c745ff555b173eff3dfe0a25e063c0c02200551572ec33d45ece8e64275970bd1b1694621f0ed8fac2f7e18095f170fe3fe012102d4edb773e3bd94e1251790f5cc543cbfa76c2b0abad14898674b1c4e27176ef2ffffffff02c44e0100000000001976a914dd826377dcf2075e5065713453cfad675ba9434f88aca070002a010000001976a914e7d0344ba970301e93cd7b505c7ae1b5bcf5639288ac00000000'; + + paymentProtocol.sendPayment(currency, signedRawTransaction, paymentRequest.paymentUrl, function(err, response) { + if (err) { + //DO NOT BROADCAST PAYMENT + return console.log('Error sending payment to server'); + } + console.log('Payment sent successfully'); + //TODO: Broadcast payment to network here + }); + }); +}); +``` + +#### Promises +``` +const JsonPaymentProtocol = require('json-payment-protocol'); +const paymentProtocol = new JsonPaymentProtocol(); + +let requestUrl = 'bitcoin:?r=https://test.bitpay.com/i/Jr629pwsXKdTCneLyZja4t'; +paymentProtocol + .getRawPaymentRequestAsync(requestUrl) + .then((response) => { + return paymentProtocol.parsePaymentRequestAsync(response.rawBody, response.headers); + }) + .then((paymentRequest) => { + console.log('Payment request retrieved'); + console.log(paymentRequest); + + //TODO: Create the rawTransaction and sign it in your wallet instead of this, do NOT broadcast yet + let currency = 'BTC' + let signedRawTransaction = '02000000010c2b0d60448d5cdfebe222014407bdb408b8427f837447484911efddea700323000000006a47304402201d3ed3117f1968c3b0a078f15f8462408c745ff555b173eff3dfe0a25e063c0c02200551572ec33d45ece8e64275970bd1b1694621f0ed8fac2f7e18095f170fe3fe012102d4edb773e3bd94e1251790f5cc543cbfa76c2b0abad14898674b1c4e27176ef2ffffffff02c44e0100000000001976a914dd826377dcf2075e5065713453cfad675ba9434f88aca070002a010000001976a914e7d0344ba970301e93cd7b505c7ae1b5bcf5639288ac00000000'; + + return paymentProtocol.sendPaymentAsync(currency, signedRawTransaction, paymentRequest.paymentUrl); + }) + .then((response) => { + console.log('Payment sent successfully'); + //TODO: Broadcast payment to network here + }) + .catch((err) => { + //DO NOT BROADCAST PAYMENT + return console.log('Error processing payment request', err); + }); +``` + +### Options + +Options passed to `new JsonPaymentProtocol()` are passed to request, so if you need to use a proxy or set any other request.js flags you can do so by including them when instantiating your instance. For example: + +``` +new JsonPaymentProtocol({ + proxy: 'socks://mySocksProxy.local' +}) +``` + +### URI Formats +You can provide either the `bitcoin:?r=https://bitpay.com/i/invoice` format or `https://bitpay.com/i/invoice` directly. + diff --git a/specification.md b/specification.md new file mode 100644 index 0000000..e226603 --- /dev/null +++ b/specification.md @@ -0,0 +1,190 @@ +# JSON Payment Protocol Specification + +Revision 0.5 + +## Application Logic + +1. (Web) User selects preferred currency on invoice if multiple options are available +2. (Client) Wallet obtains payment protocol uri +3. (Client) Fetches payment information from server +4. (Server) Verifies invoice exists and is still accepting payments, responds with payment request +5. (Client) Validates payment request hash +6. (Client) Generates a payment to match conditions on payment request +7. (Client) Submits proposed signed transaction to server +8. (Server) Validates invoice exists and is still accepting payments +9. (Server) Validates payment matches address, amount, and currency of invoice and has a reasonable transaction fee. +10. (Server) Broadcasts payment to network and notifies client payment was accepted. +11. (Client) If payment is accepted by server, wallet broadcasts payment + +If at any time the payment is rejected by the server **your client should not broadcast the payment**. +Broadcasting a payment before getting a success notification back from the server will in most cases lead to a failed payment for the sender. The sender will bear the cost of paying transaction fees yet again to get their money back. + +## Payment Request + +### Request +A GET request should be made to the payment protocol url. + +### Response +The response will be a JSON format payload quite similar to the BIP70 format. + +#### Headers +On a successful request, the response will contain one header of note. + +* `digest` - A SHA256 hash of the JSON response string, should be verified by the client before proceeding + +#### Body +* `network` - Which network is this request for (main / test / regtest) +* `currency` - Three digit currency code representing which coin the request is based on +* `requiredFeePerByte` - The minimum fee per byte required on this transaction, if lower than that we will reject it +* `outputs` - What output(s) your transaction must include in order to be accepted +* `time` - ISO Date format of when the invoice was generated +* `expires` - ISO Date format of when the invoice will expire +* `memo` - A plain text description of the payment request, can be displayed to the user / kept for records +* `paymentUrl` - The url where the payment should be sent +* `paymentId` - The invoice ID, can be kept for records + +#### Response Body Example +``` +{ + "network": "test", + "currency": "BTC", + "requiredFeePerByte": 200, + "outputs": [ + { + "amount": 39300, + "address": "mthVG9kuRTJQtXieJVDSrrvWyM7QDZ3rcV" + } + ], + "time": "2018-01-12T22:04:54.364Z", + "expires": "2018-01-12T22:19:54.364Z", + "memo": "Payment request for BitPay invoice TmyrxFvAi4DjFNy3c7EjVm for merchant Robs Fake Business", + "paymentUrl": "https://test.bitpay.com/i/TmyrxFvAi4DjFNy3c7EjVm", + "paymentId": "TmyrxFvAi4DjFNy3c7EjVm" +} +``` + +## Payment Payload + +### Request +A POST request should be made to the payment protocol url with a `Content-Type` header set to `application/payment`. A JSON format body should be included with the following fields: + +``` +{ + "currency": "", + "transactions": [ + "" + ] +} +``` + +#### Example Request Body +``` +{ + "currency": "BTC", + "transactions": [ + "02000000011f0f762184cbc8e94b307fab6f805168724f123a23cd48aac4a9bac8768cfd67000000004847304402205079b96def679f04de9698dd8b9f58dff3e4a13c075f5939c6edfbb8698c8cc802203eac5a3d6410a9f94a86828a4e207f8083fe0bf1c77a74a0cb7add49100d427001ffffffff0284990000000000001976a9149097a519e42061e4977b07b69735ed842b755c0088ac08cd042a010000001976a914cf4b90bca14deab1315c125b8b74b7d31eea97b288ac00000000" + ] +} +``` + +### Response +The response will be a JSON format payload containing the original payment body and a memo field which should be displayed to the user. + +#### Response Example +``` +{ + "payment": { + "transactions": [ + "020000000121053733b28b90707a3c63a48171f71abfdc7288bf9d78170e73cfedbbbdfcea00000000484730440220545d53b54873a5afbaf01a77943828f25c6a28d9c5ca4d0968130b5788fc6f9302203e45125723844e4752202792b764b6538342ad169d3828dad18eb231ea01f05101ffffffff02b09a0000000000001976a9149659267896dda4e5aef150e4ca83f0d76022c7b288ac84dd042a010000001976a914fa1a5ed99ce09fd901e9ca7d6f8fcc56d3d5eccf88ac00000000" + ] + }, + "memo": "Transaction received by BitPay. Invoice will be marked as paid if the transaction is confirmed." +} +``` + +### Curl Example +``` +curl -v -H 'Content-Type: application/payment' -d '{"currency": "BTC", "transactions":["02000000012319227d3995427b05429df7ea30b87cb62f986ba3003311a2cf2177fb5b0ae8000000004847304402205bd75d6b654a70dcc8f548b630c39aec1d2c1de6900b5376ef607efc705f65b002202dd1036f091d4d6047e2f5bcd230ec8bcd5ad2f0785908d78f08a52b8850559f01ffffffff02b09a0000000000001976a9140b2a833c4183c51b86f5dcbb2eeeaca2dfb44bae88acdccb042a010000001976a914f0fd63e5880cbed2fa856e1f4174fc875eeccc5a88ac00000000"]}' https://test.bitpay.com/i/7QBCJ2TpazTKKnczzJQJMc +* Trying 127.0.0.1... +* TCP_NODELAY set +* Connected to test.bitpay.com (127.0.0.1) port 8088 (#0) +* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 +> POST /i/7QBCJ2TpazTKKnczzJQJMc HTTP/1.1 +> Host: test.bitpay.com +> User-Agent: curl/7.54.0 +> Accept: */* +> Content-Type: application/payment-ack +> Content-Length: 403 +> +* upload completely sent off: 403 out of 403 bytes +< HTTP/1.1 200 OK +< Content-Length: 520 +< Date: Fri, 12 Jan 2018 22:44:13 GMT +< Connection: keep-alive +< +* Connection #0 to host test.bitpay.com left intact +{"payment":{"transactions":["02000000012319227d3995427b05429df7ea30b87cb62f986ba3003311a2cf2177fb5b0ae8000000004847304402205bd75d6b654a70dcc8f548b630c39aec1d2c1de6900b5376ef607efc705f65b002202dd1036f091d4d6047e2f5bcd230ec8bcd5ad2f0785908d78f08a52b8850559f01ffffffff02b09a0000000000001976a9140b2a833c4183c51b86f5dcbb2eeeaca2dfb44bae88acdccb042a010000001976a914f0fd63e5880cbed2fa856e1f4174fc875eeccc5a88ac00000000"]},"memo":"Transaction received by BitPay. Invoice will be marked as paid if the transaction is confirmed."}% +``` + + +## Errors + +All errors are communicated in plaintext with an appropriate status code. + +### Example Error + +``` +curl -v https://test.bitpay.com/i/48gZau8ao76bqAoEwAKSwx -H 'Accept: application/payment-request' +* Trying 104.17.68.20... +* TCP_NODELAY set +* Connected to test.bitpay.com (104.17.68.20) port 443 (#0) +* TLS 1.2 connection using TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 +> GET /i/48gZau8ao76bqAoEwAKSwx HTTP/1.1 +> Host: test.bitpay.com +> User-Agent: curl/7.54.0 +> Accept: application/payment-request +> +< HTTP/1.1 400 Bad Request +< Date: Fri, 26 Jan 2018 01:54:03 GMT +< Content-Type: text/html; charset=utf-8 +< Content-Length: 44 +< Connection: keep-alive +< Strict-Transport-Security: max-age=31536000 +< X-Download-Options: noopen +< X-Content-Type-Options: nosniff +< Access-Control-Allow-Origin: * +< Access-Control-Allow-Methods: GET, POST, OPTIONS +< Access-Control-Allow-Headers: Host, Connection, Content-Length, Accept, Origin, User-Agent, Content-Type, Accept-Encoding, Accept-Language +< +* Connection #0 to host test.bitpay.com left intact +This invoice is no longer accepting payments +``` + +### Common Errors + +| Http Status Code | Response | Cause | +|---|---|---| +| 404 | This invoice was not found or has been archived | Invalid invoiceId, or invoice has been archived (current TTL is 3 days) | +| 400 | Unsupported Content-Type for payment | Your Content-Type header was not valid | +| 400 | Invoice no longer accepting payments | Invoice is either paid or has expired | +| 400 | We were unable to parse your payment. Please try again or contact your wallet provider | Request body could not be parsed / empty body | +| 400 | Request must include exactly one (1) transaction | Included no transaction in body / Included multiple transactions in body | +| 400 | Your transaction was an in an invalid format, it must be a hexadecimal string | Make sure you're sending the raw hex string format of your signed transaction +| 400 | We were unable to parse the transaction you sent. Please try again or contact your wallet provider | Transaction was hex, but it contained invalid transaction data or was in the wrong format | +| 400 | The transaction you sent does not have any output to the bitcoin address on the invoice | The transaction you sent does not pay to the address listed on the invoice | +| 400 | The amount on the transaction (X BTC) does not match the amount requested (Y BTC). This payment will not be accepted. | Payout amount to address does not match amount that was requested | +| 400 | Transaction fee (X sat/kb) is below the current minimum threshold (Y sat/kb) | Your fee must be at least the amount sent in the payment request as `requiredFeePerByte`| +| 400 | This invoice is priced in BTC, not BCH. Please try with a BTC wallet instead | Your transaction currency did not match the one on the invoice | +| 422 | One or more input transactions for your transaction were not found on the blockchain. Make sure you're not trying to use unconfirmed change | Spending outputs which have not yet been broadcast to the network | +| 422 | One or more input transactions for your transactions are not yet confirmed in at least one block. Make sure you're not trying to use unconfirmed change | Spending outputs which have not yet confirmed in at least one block on the network | +| 500 | Error broadcasting payment to network | Our Bitcoin node returned an error when attempting to broadcast your transaction to the network. This could mean our node is experiencing an outage or your transaction is a double spend. | + +Another issue you may see is that you are being redirected to `bitpay.com/invoice?id=xxx` instead of being sent a payment-request. In that case you are not setting your `Accept` header to a valid value and we assume you are a browser or other unknown requester. + +## MIME Types + +|Mime|Description| +|---|---| +|application/payment-request| Associated with the server's payment request, this specified on the client `Accept` header when retrieving the payment request| +|application/payment| Used by the client when sending their proposed payment transaction payload| +|application/payment-ack| Used by the server to state acceptance of the client's proposed payment transaction|