Skip to main content
POST
/
payment
/
settle
Settle payment
curl --request POST \
  --url https://pay.gateway.example/opencharge/payment/settle \
  --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 '
{
  "txid": "gateway_payment_456",
  "proof": {
    "txid": "settlement_tx_789",
    "issuer": 100,
    "from": {
      "ocid": 200,
      "reference": "wallet_tx_123"
    },
    "to": {
      "ocid": 300,
      "reference": "gateway_payment_456"
    },
    "amount": "99.99",
    "currency": "USD",
    "timestamp": 1706500500,
    "memo": "Settlement for gateway_payment_456"
  },
  "signature": "a1b2c3d4e5f6..."
}
'
{
  "status": "completed",
  "txid": "gateway_payment_456",
  "proof": {
    "txid": "gateway_payment_456",
    "issuer": 300,
    "from": {
      "ocid": 200,
      "reference": "wallet_tx_123"
    },
    "to": {
      "ocid": 500,
      "reference": "ord_abc123"
    },
    "amount": "99.99",
    "currency": "USD",
    "timestamp": 1706500500
  },
  "signature": "gateway_sig_xyz..."
}
After creating a payment via /payment/create, payment apps submit a signed proof from an accepted settlement provider to complete the payment.

Flow

  1. Payment app creates pending payment via /payment/create
  2. Payment app transfers funds to your OCID via an accepted settlement provider
  3. Payment app receives signed proof from the settlement provider
  4. Payment app submits proof to your /payment/settle
  5. You verify proof, credit merchant, and return your own signed proof

Implementation

app.post('/payment/settle', verifyAuth, async (req, res) => {
  const { txid, proof, signature } = req.body;
  const callerOcid = parseInt(req.headers['x-oc-id']);

  // 1. Find the pending payment
  const payment = await db.getPayment(txid);
  if (!payment) {
    return res.status(404).json({
      error: { code: 'PAYMENT_NOT_FOUND', message: 'Payment not found' }
    });
  }

  // 2. Check payment hasn't expired
  if (payment.expiresAt < Date.now() / 1000) {
    return res.status(400).json({
      error: { code: 'PAYMENT_EXPIRED', message: 'Payment has expired' }
    });
  }

  // 3. Check payment is still pending
  if (payment.status !== 'pending_settlement') {
    return res.status(400).json({
      error: { code: 'INVALID_STATE', message: `Payment is ${payment.status}` }
    });
  }

  // 4. Verify issuer is accepted
  if (!ACCEPTED_SETTLEMENT_PROVIDERS.includes(proof.issuer)) {
    return res.status(400).json({
      error: { code: 'ISSUER_NOT_ACCEPTED', message: 'Settlement provider not accepted' }
    });
  }

  // 5. Verify proof signature
  const publicKey = await getPublicKey(proof.issuer);
  const canonical = JSON.stringify(proof, Object.keys(proof).sort());
  if (!secp256k1_verify(publicKey, signature, canonical)) {
    return res.status(400).json({
      error: { code: 'PROOF_SIGNATURE_INVALID', message: 'Invalid proof signature' }
    });
  }

  // 6. Verify we are the recipient
  if (proof.to.ocid !== YOUR_OCID) {
    return res.status(400).json({
      error: { code: 'INVALID_PROOF', message: 'Proof recipient mismatch' }
    });
  }

  // 7. Verify amount matches
  if (proof.amount !== payment.amount || proof.currency !== payment.currency) {
    return res.status(400).json({
      error: { code: 'AMOUNT_MISMATCH', message: 'Amount does not match payment' }
    });
  }

  // 8. Complete payment
  await db.completePayment(payment.id, proof.txid, proof.timestamp);

  // 9. Credit merchant
  await db.creditMerchant(payment.merchantOcid, payment.amount, payment.currency);

  // 10. Create our proof for the merchant
  const merchantProof = {
    txid: payment.txid,
    issuer: YOUR_OCID,
    from: proof.from,
    to: { ocid: payment.merchantOcid, reference: payment.orderId },
    amount: payment.amount,
    currency: payment.currency,
    timestamp: Math.floor(Date.now() / 1000),
    memo: `Payment settled for ${payment.orderId}`
  };

  const merchantSignature = signProof(merchantProof);

  // 11. Notify merchant webhook
  await notifyMerchant(payment.merchantOcid, merchantProof, merchantSignature);

  // 12. Return our signed proof
  res.json({
    status: 'completed',
    txid: payment.txid,
    proof: merchantProof,
    signature: merchantSignature
  });
});

Validation Checklist

Before completing a payment:
  • Payment exists and matches the txid
  • Payment hasn’t expired
  • Payment is in pending_settlement state
  • Proof issuer is in your accepts list
  • Proof signature is valid
  • Proof recipient is your OCID
  • Proof amount matches payment amount

Response

On success, return your signed proof that the payment app can also use:
{
  "status": "completed",
  "txid": "gateway_payment_456",
  "proof": {
    "txid": "gateway_payment_456",
    "issuer": 300,
    "from": { "ocid": 200, "reference": "wallet_tx_123" },
    "to": { "ocid": 500, "reference": "ord_abc123" },
    "amount": "99.99",
    "currency": "USD",
    "timestamp": 1706500500
  },
  "signature": "gateway_sig_xyz..."
}

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

Settlement proof for a pending payment

txid
string
required

Your payment transaction ID

proof
object
required
signature
string
required

Settlement provider's signature

Response

Payment settled successfully

Payment completed, your signed proof for the merchant

status
enum<string>
required
Available options:
completed
txid
string
required
proof
object
required
signature
string
required

Your signature of the proof