# Direct Payments

> **What is this?**  
> A direct (host-to-host) card charge where the **Merchant** collects card data in its own UI, encrypts it with a key from **PlexyPay**, and creates a payment via **[`POST /v1/payments/direct`](https://developer.plexy.money/create-host-to-host-payment-21794552e0.md)**. 3-D Secure may be required by the **Bank/ACS**.

---

## Actors
- **User** — payer in the merchant’s UI  
- **Merchant** — frontend + backend that collects and encrypts card data  
- **PlexyPay** — payment API/orchestrator (routes, 3-DS handling, webhooks)  
- **Bank / ACS** — issuer/acquirer side (authorization & 3-D Secure)

---

## When to use
- You need a fully **API-only** flow (no hosted checkout UI).
- You control the entire user experience and PCI scope.
- You can handle **3-D Secure** redirects/challenges.

---

## Security & Prerequisites
- **Encryption key:** obtain via **`GET /v1/keys`**; encrypt card data client-side.  
- **PCI DSS:** Host-to-Host requires merchant-side PCI DSS compliance.  
- **Idempotency:** send an **`Idempotency-Key`** header for every charge attempt.  
- **Webhooks:** expose an HTTPS endpoint to receive asynchronous status updates.

:::danger
**Do not** send raw PAN/CVV to your backend unless you are PCI DSS certified. Encrypt card data on the client, and send **only encrypted fields** to your server/API.
:::

---

<Frame caption="Direct Payment Integration Flow">
  ![](https://api.apidog.com/api/v1/projects/1075475/resources/363079/image-preview)
</Frame>

<!---
title Direct Payment (Host-to-Host) Flow
autoNumber nested
colorMode pastel
styleMode plain
typeface mono

User [icon: user, color: blue]
Merchant [icon: store, color: green]
PlexyPay [icon: credit-card, color: purple]
Bank [icon: bank, color: orange]

// 1. User initiates payment
User > Merchant: Initiate payment in merchant UI
activate User

// 2. Merchant encrypts card data
Merchant > Merchant: Encrypt card data using key from GET /v1/keys

// 3. Merchant sends transaction to PlexyPay
Merchant > PlexyPay: Send payment request to POST v1/payments/direct
activate Merchant

// 4. PlexyPay validates request and checks Idempotency-Key
PlexyPay > PlexyPay: Validate request and Idempotency-Key

alt [label: Idempotency duplicate, icon: refresh, color: purple] {
  PlexyPay > Merchant: Return current status of in-flight transaction
}
else [label: New request, icon: plus, color: purple] {
  PlexyPay > PlexyPay: Accept payload and start processing

  // 5. PlexyPay forwards to Bank
  PlexyPay > Bank: Forward authorization request
  activate PlexyPay

  // 6. Bank processes and checks for 3-D Secure
  Bank > Bank: Process request and check if 3-D Secure is required

  alt [label: 3-D Secure required?, icon: shield, color: orange] {
    // 7. 3-DS required branch
    PlexyPay > Merchant: Return 3-DS/redirect URL for challenge
    Merchant > User: Present challenge page/link
    User > User: Complete 3-DS challenge/OTP
    Bank <-> PlexyPay: Confirm authentication result
  }
  else [label: 3-DS not required, icon: check, color: orange] {
    // Continue directly to authorization result
    // No additional steps
  }

  // 8. Bank returns payment result
  Bank > PlexyPay: Return payment processing result (authorized/charged/failed)
  deactivate PlexyPay

  // 9. PlexyPay delivers outcome
  par [label: Outcome delivery, icon: send, color: purple] {
    PlexyPay > Merchant: Send webhook with final transaction status
    PlexyPay > Merchant: Return error details if flow fails earlier
  }
}
deactivate Merchant

// 10. Merchant displays outcome to user
Merchant > User: Display final outcome (success or error, with retry/alternative option)
deactivate User
-->

---

## Client-side encryption (JavaScript example)

Use this script to encrypt card fields with PlexyPay **public key** from **`GET https://api.plexypay.com/v1/keys`**, and then send encrypted values in your **`POST /v1/payments/direct`** request payload.

<Tabs>
  <Tab title="Node.js script (ready to run)">
```js
#!/usr/bin/env node
const crypto = require("crypto");
const https = require("https");

/** ---- utils ---- */
function fetchJSON(url) {
  return new Promise((resolve, reject) => {
    https
      .get(url, res => {
        let data = "";
        res.on("data", chunk => (data += chunk));
        res.on("end", () => {
          const ct = (res.headers["content-type"] || "").toLowerCase();
          if (ct.includes("application/json")) {
            try { return resolve(JSON.parse(data)); } catch (e) { return reject(e); }
          }
          try { resolve(JSON.parse(data)); } catch { resolve(data); }
        });
      })
      .on("error", reject);
  });
}

function extractPem(body) {
  if (typeof body === "string" && body.includes("BEGIN PUBLIC")) return body;
  if (body && typeof body === "object") {
    if (body.publicKey) return body.publicKey;
    if (body.key) return body.key;
    if (body.data && typeof body.data === "string" && body.data.includes("BEGIN PUBLIC")) return body.data;
  }
  throw new Error("Public key PEM not found in response");
}

function encryptWithPem(pem, val) {
  return crypto
    .publicEncrypt({ key: pem, padding: crypto.constants.RSA_PKCS1_PADDING }, Buffer.from(String(val), "utf8"))
    .toString("base64");
}

function parseArgs() {
  const a = process.argv.slice(2);
  const out = {};
  for (const arg of a) {
    const m = arg.match(/^--([^=]+)=(.*)$/);
    if (m) out[m[1]] = m[2];
  }
  return out;
}

function normalizeCard({ number, expMonth, expYear, holderName, cvv }) {
  if (!number || !expMonth || !expYear || !holderName || !cvv)
    throw new Error("Missing required fields: number, expMonth, expYear, holderName, cvv");

  const cleanNumber = String(number).replace(/\s+/g, "");
  const m = String(expMonth).padStart(2, "0");
  const y = String(expYear);
  if (!/^\d{13,19}$/.test(cleanNumber)) throw new Error("Invalid card number length");
  if (!/^\d{2}$/.test(m) || Number(m) < 1 || Number(m) > 12) throw new Error("Invalid expMonth");
  if (!/^\d{2,4}$/.test(y)) throw new Error("Invalid expYear");
  if (!/^\d{3,4}$/.test(String(cvv))) throw new Error("Invalid CVV");

  return { number: cleanNumber, expMonth: m, expYear: y, holderName: String(holderName).trim(), cvv: String(cvv) };
}

async function readStdinJSON() {
  return new Promise(resolve => {
    if (process.stdin.isTTY) return resolve(null);
    let data = "";
    process.stdin.setEncoding("utf8");
    process.stdin.on("data", chunk => (data += chunk));
    process.stdin.on("end", () => {
      try { resolve(JSON.parse(data)); } catch { resolve(null); }
    });
  });
}

/** ---- main ---- */
(async () => {
  try {
    const args = parseArgs();

    const input =
      {
        number: args.number || process.env.CARD_NUMBER,
        expMonth: args.expMonth || process.env.CARD_EXP_MONTH,
        expYear: args.expYear || process.env.CARD_EXP_YEAR,
        holderName: args.holderName || process.env.CARD_HOLDER_NAME,
        cvv: args.cvv || process.env.CARD_CVV,
      };

    if (Object.values(input).some(v => !v)) {
      const stdinObj = await readStdinJSON();
      if (stdinObj) {
        input.number = input.number || stdinObj.number;
        input.expMonth = input.expMonth || stdinObj.expMonth;
        input.expYear = input.expYear || stdinObj.expYear;
        input.holderName = input.holderName || stdinObj.holderName;
        input.cvv = input.cvv || stdinObj.cvv;
      }
    }

    const card = normalizeCard(input);

    const KEY_URL = "https://api.plexypay.com/v1/keys";
    const body = await fetchJSON(KEY_URL);
    const pubKey = extractPem(body);

    const result = {
      encryptedCardNumber: encryptWithPem(pubKey, card.number),
      encryptedExpiryMonth: encryptWithPem(pubKey, card.expMonth),
      encryptedExpiryYear: encryptWithPem(pubKey, card.expYear),
      encryptedCardholderName: encryptWithPem(pubKey, card.holderName),
      encryptedCvv: encryptWithPem(pubKey, card.cvv),
    };

    process.stdout.write(JSON.stringify(result, null, 2) + "\n");
  } catch (e) {
    console.error("Error:", e.message || e);
    process.exit(1);
  }
})();
```
  </Tab>

  <Tab title="How to run">
**Option A — CLI flags**
```bash
node encrypt-card.js \
  --number="4111 1111 1111 1111" \
  --expMonth="12" \
  --expYear="2029" \
  --holderName="JOHN DOE" \
  --cvv="123"
```

**Option B — env variables**
```bash
export CARD_NUMBER="4111 1111 1111 1111"
export CARD_EXP_MONTH="12"
export CARD_EXP_YEAR="2029"
export CARD_HOLDER_NAME="JOHN DOE"
export CARD_CVV="123"

node encrypt-card.js
```

**Option C — stdin JSON**
```bash
echo '{"number":"4111 1111 1111 1111","expMonth":"12","expYear":"2029","holderName":"JOHN DOE","cvv":"123"}' | node encrypt-card.js
```
  </Tab>

  <Tab title="Output (example)">
The script prints **only JSON** (no logs), so you can safely pipe it to your backend flow:
```json
{
  "encryptedCardNumber": "base64...",
  "encryptedExpiryMonth": "base64...",
  "encryptedExpiryYear": "base64...",
  "encryptedCardholderName": "base64...",
  "encryptedCvv": "base64..."
}
```
  </Tab>
</Tabs>

:::info
**Encryption details (from the script):**
- RSA public key fetched from **`/v1/keys`**
- Field-by-field encryption
- Padding: **RSA_PKCS1_PADDING** (PKCS#1 v1.5)
:::

---

## Next steps

<Steps>
  <Step title="1) Generate encrypted card fields">
    Use the JS snippet above on the client (or in your PCI-compliant environment).
  </Step>
  <Step title="2) Create payment">
    Call **`POST /v1/payments/direct`** with encrypted fields, amount, currency, and your order reference.
  </Step>
  <Step title="3) Handle 3‑D Secure (if required)">
    If API returns a challenge/redirect URL, present it to the user and complete the flow.
  </Step>
  <Step title="4) Confirm final status via webhooks">
    Always rely on webhooks for final state (success/failure), and treat client responses as intermediate.
  </Step>
</Steps>

