Skip to main content
POST
/
transfer
/
create
Create transfer
curl --request POST \
  --url https://pay.gateway.example/opencharge/transfer/create \
  --header 'Content-Type: application/json' \
  --header 'X-OC-ID: <x-oc-id>' \
  --header 'X-OC-Nonce: <x-oc-nonce>' \
  --header 'X-OC-Signature: <x-oc-signature>' \
  --header 'X-OC-Timestamp: <x-oc-timestamp>' \
  --data '
{
  "from": {
    "ocid": 200,
    "reference": "wallet_user_123"
  },
  "to": {
    "ocid": 500,
    "reference": "ord_abc123"
  },
  "amount": "25.00",
  "currency": "USD",
  "memo": "Payment for order ord_abc123",
  "order": {
    "id": "ord_abc123",
    "urls": [
      "https://merchant.example/orders/ord_abc123"
    ]
  }
}
'
{
  "proof": {
    "txid": "gateway_tx_789",
    "issuer": 300,
    "from": {
      "ocid": 200,
      "reference": "wallet_user_123"
    },
    "to": {
      "ocid": 500,
      "reference": "ord_abc123"
    },
    "amount": "25.00",
    "currency": "USD",
    "timestamp": 1706500500,
    "memo": "Payment for order ord_abc123"
  },
  "signature": "a1b2c3d4e5f6..."
}
Use this endpoint for both internal transfers (your app paying a merchant) and external partner transfers.

Use Cases

  1. Internal - Your mobile app requests payment after user scans merchant QR
  2. External - Partner gateways with reserve accounts request transfers

Implementation

app.post('/transfer/create', verifyAuth, async (req, res) => {
  const { from, to, amount, currency, memo, order } = req.body;
  const callerOcid = parseInt(req.headers['x-oc-id']);

  // Determine if internal or external
  if (isInternalRequest(callerOcid, from)) {
    return handleInternalTransfer(req, res);
  } else {
    return handleExternalTransfer(req, res);
  }
});

async function handleInternalTransfer(req, res) {
  const { from, to, amount, currency, memo, order } = req.body;

  // Get user from reference
  const user = await db.getUserByReference(from.reference);
  if (!user) {
    return res.status(400).json({
      error: { code: 'ACCOUNT_NOT_FOUND', message: 'User not found' }
    });
  }

  // Check balance
  if (user.balance < parseFloat(amount)) {
    return res.status(402).json({
      error: { code: 'INSUFFICIENT_FUNDS', message: 'Not enough balance' }
    });
  }

  // Process transfer
  const txid = generateTxid();
  await db.debitUser(user.id, amount);

  // Create proof
  const proof = {
    txid,
    issuer: YOUR_OCID,
    from: { ocid: YOUR_OCID, reference: from.reference },
    to: { ocid: to.ocid, reference: to.reference || order?.id },
    amount,
    currency,
    timestamp: Math.floor(Date.now() / 1000),
    memo
  };

  const signature = signProof(proof);

  // Notify merchant
  await sendMerchantWebhook(to.ocid, proof, signature);

  res.json({ proof, signature });
}

Paying After Scanning Merchant QR

When your user scans a merchant’s QR code:
async function payMerchantOrder(user, merchantOrder) {
  // 1. Verify the order
  const merchant = await fetchMerchantMetadata(merchantOrder.ocid);
  if (!verifyOrderSignature(merchantOrder.order, merchantOrder.signature, merchant.config.publicKey)) {
    throw new Error('Invalid order signature');
  }

  // 2. Debit user
  await db.debitUser(user.id, merchantOrder.order.amount);

  // 3. Settle with merchant - see Settlement Guide
  const proof = await settlePayment(user, merchantOrder.order, merchantOrder.ocid);

  return proof;
}

Settlement Guide

Learn about the different ways to settle payments with merchants

Headers

X-OC-ID
string
required

Caller's OCID (Opencharge ID)

X-OC-Timestamp
string
required

Unix timestamp in seconds

X-OC-Nonce
string
required

Unique request identifier for replay protection

X-OC-Signature
string
required

secp256k1 ECDSA signature of the canonical request

Body

application/json

Request to transfer funds between accounts

from
object
required
to
object
required
amount
string
required

Amount as decimal string

currency
string
required

ISO 4217 currency code

memo
string
order
object

Response

Transfer completed, signed proof returned

Signed proof of completed transfer

proof
object
required
signature
string
required

Your signature of the proof