> ## Documentation Index
> Fetch the complete documentation index at: https://docs.opencharge.network/llms.txt
> Use this file to discover all available pages before exploring further.

# Transfer Webhook

> Receive signed transfer proofs from settlement providers

<Info>
  Settlement providers POST signed proofs to this endpoint when transfers complete. Match the proof to pending payments and forward your own signed proof to merchants.
</Info>

## When You Receive Webhooks

You receive webhooks when:

1. A settlement provider you accept completes a transfer to your OCID
2. The transfer was for a pending payment at your gateway
3. You need to credit a merchant and notify them

## Implementation

```javascript theme={null}
app.post('/transfer/webhook', verifyAuth, async (req, res) => {
  const { proof, signature } = req.body;
  const senderOcid = parseInt(req.headers['x-oc-id']);

  // 1. Verify sender is an accepted settlement provider
  if (!ACCEPTED_SETTLEMENT_PROVIDERS.includes(senderOcid)) {
    return res.status(400).json({
      error: { code: 'ISSUER_NOT_ACCEPTED', message: 'Unknown settlement provider' }
    });
  }

  // 2. Verify sender is the proof issuer
  if (proof.issuer !== senderOcid) {
    return res.status(400).json({
      error: { code: 'INVALID_PROOF', message: 'Issuer mismatch' }
    });
  }

  // 3. Verify proof signature
  const publicKey = await getPublicKey(senderOcid);
  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 signature' }
    });
  }

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

  // 5. Find the pending payment
  const payment = await db.getPaymentByReference(proof.to.reference);
  if (!payment) {
    // Log for investigation but accept
    console.log(`Unknown payment reference: ${proof.to.reference}`);
    return res.json({ status: 'accepted', txid: proof.txid });
  }

  // 6. 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' }
    });
  }

  // 7. Mark payment as complete
  await db.completePayment(payment.id, proof.txid, proof.timestamp);

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

  // 9. Create and send proof to 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 for ${payment.orderId}`
  };

  await notifyMerchant(payment.merchantOcid, merchantProof);

  res.json({ status: 'accepted', txid: proof.txid });
});
```

## Forwarding to Merchants

After receiving a valid proof, create your own proof for the merchant:

```javascript theme={null}
async function notifyMerchant(merchantOcid, proof) {
  const merchant = await db.getMerchant(merchantOcid);
  const endpoint = merchant.webhookUrl || await resolveEndpoint(merchantOcid);

  const signature = signProof(proof);

  await fetch(`${endpoint}/transfer/webhook`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      ...createAuthHeaders(YOUR_OCID, YOUR_PRIVATE_KEY)
    },
    body: JSON.stringify({ proof, signature })
  });
}
```

<Warning>
  Always verify the proof signature before crediting accounts or forwarding to merchants.
</Warning>


## OpenAPI

````yaml merchant-gateway-api/endpoint/merchant-gateway-api/openapi.json post /transfer/webhook
openapi: 3.1.0
info:
  title: Opencharge Merchant API
  description: >-
    API specification for merchants integrating with the Opencharge protocol.
    These endpoints are implemented by merchants and called by payment apps and
    gateways.
  version: 0.1.0
  license:
    name: MIT
servers:
  - url: https://api.merchant.example/opencharge
    description: Example merchant Opencharge endpoint
security: []
paths:
  /transfer/webhook:
    post:
      summary: Instant Transfer Notification (ITN)
      description: >-
        Receive signed transfer proofs on this endpoint. When a transfer
        involving your OCID is completed, a signed proof is POSTed here. Verify
        the proof signature, validate the issuer is trusted, and take
        appropriate action (e.g., mark order as paid, fulfill the order).
      operationId: transferWebhook
      parameters:
        - $ref: '#/components/parameters/X-OC-ID'
        - $ref: '#/components/parameters/X-OC-Timestamp'
        - $ref: '#/components/parameters/X-OC-Nonce'
        - $ref: '#/components/parameters/X-OC-Signature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TransferWebhookRequest'
            example:
              proof:
                txid: gateway_tx_456
                issuer: 100
                from:
                  ocid: 200
                  reference: wallet_tx_123
                to:
                  ocid: 500
                  reference: ord_abc123
                amount: '15.00'
                currency: USD
                timestamp: 1706500500
                memo: Payment for ord_abc123
              signature: a1b2c3d4e5f6...
      responses:
        '200':
          description: Notification received and processed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TransferWebhookResponse'
              example:
                status: accepted
                txid: gateway_tx_456
        '400':
          description: Invalid proof or signature
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Authentication failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  parameters:
    X-OC-ID:
      name: X-OC-ID
      in: header
      required: true
      description: Sender's OCID (Opencharge ID)
      schema:
        type: string
      example: '200'
    X-OC-Timestamp:
      name: X-OC-Timestamp
      in: header
      required: true
      description: Unix timestamp in seconds
      schema:
        type: string
      example: '1706500000'
    X-OC-Nonce:
      name: X-OC-Nonce
      in: header
      required: true
      description: Unique request identifier for replay protection
      schema:
        type: string
      example: req_abc123
    X-OC-Signature:
      name: X-OC-Signature
      in: header
      required: true
      description: secp256k1 ECDSA signature of the canonical request
      schema:
        type: string
      example: a1b2c3d4...7f1b
  schemas:
    TransferWebhookRequest:
      type: object
      required:
        - proof
        - signature
      properties:
        proof:
          $ref: '#/components/schemas/TransactionProof'
        signature:
          type: string
          description: secp256k1 signature of the proof from the issuer
    TransferWebhookResponse:
      type: object
      required:
        - status
        - txid
      properties:
        status:
          type: string
          enum:
            - accepted
            - rejected
          description: Whether you accepted the notification
        txid:
          type: string
          description: The transaction ID from the proof
        message:
          type: string
          description: Optional message explaining rejection reason
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: Error code
              enum:
                - INVALID_SIGNATURE
                - TIMESTAMP_EXPIRED
                - NONCE_REUSED
                - UNKNOWN_OCID
                - INVALID_PROOF
                - PROOF_SIGNATURE_INVALID
                - ISSUER_NOT_ACCEPTED
                - ORDER_NOT_FOUND
                - ORDER_EXPIRED
            message:
              type: string
              description: Human-readable error message
            details:
              type: object
              description: Additional error context
    TransactionProof:
      type: object
      required:
        - txid
        - issuer
        - from
        - to
        - amount
        - currency
        - timestamp
      properties:
        txid:
          type: string
          description: Unique transaction ID from issuer
        issuer:
          type: integer
          description: OCID of service that executed the transfer
        from:
          type: object
          required:
            - ocid
          description: Sender information
          properties:
            ocid:
              type: integer
              description: Sender's OCID
            reference:
              type: string
              description: Sender's transaction reference
        to:
          type: object
          required:
            - ocid
          description: Recipient information
          properties:
            ocid:
              type: integer
              description: Recipient's OCID (your merchant OCID)
            reference:
              type: string
              description: Your order ID or transaction reference
        amount:
          type: string
          description: Amount as decimal string
        currency:
          type: string
          description: ISO 4217 currency code
        timestamp:
          type: integer
          description: Unix timestamp when transfer completed
        memo:
          type: string
          description: Human-readable description

````