Docs/Workflows/Tokenizer → Customer → Payment

Tokenizer → Customer → Payment

End-to-end: collect card data in the browser with the Tokenizer, store it against a customer in the Vault, then charge by customer reference. The canonical card-on-file flow.

This workflow is the default for any product that wants to charge the same customer more than once — subscriptions, marketplaces, save-for-later checkout, anything card-on-file. It has three server-side steps and a single client-side step, and keeps you in SAQ A PCI scope throughout.

Overview

  1. Client: render the Tokenizer, collect card data, get a one-time token.
  2. Server: exchange the token for a stored payment method on a Customer record.
  3. Server: charge the customer by customer_id, now and for any future charges.

Step 1 — Tokenize in the browser

On the page where the customer enters card details, mount the Tokenizer:

<script src="https://sandbox.fluidpay.com/js/tokenizer/v1/tokenizer.js"></script>
<form id="checkoutForm" method="POST" action="/checkout">
  <div id="payment-form"></div>
  <input type="hidden" name="payment_token" id="paymentToken" />
  <button type="submit">Save card</button>
</form>

<script>
  const tokenizer = new Tokenizer({
    apikey: "pub_YOUR_PUBLIC_KEY",
    container: "#payment-form",
    submission: (resp) => {
      if (resp.status === "success") {
        document.getElementById("paymentToken").value = resp.token;
        document.getElementById("checkoutForm").submit();
      } else {
        // render resp.validationErrors or resp.msg
      }
    }
  });
</script>

When the customer clicks Save, the tokenizer submits card data to moat and receives a token. The token is a single-use opaque string (prefix tok_) that you post to your server.

Step 2 — Vault the token on a customer

On your server, call moat's Customer Vault to create (or update) a customer and attach the tokenized payment method:

const resp = await fetch("https://sandbox.fluidpay.com/api/vault/customer", {
  method: "POST",
  headers: {
    "Authorization": process.env.API_KEY,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    first_name: form.first_name,
    last_name: form.last_name,
    email: form.email,
    billing_address: {
      address_line_1: form.address1,
      city: form.city,
      state: form.state,
      postal_code: form.zip,
      country: "US"
    },
    payment_method: {
      token: { id: form.payment_token }
    }
  })
});
const customer = (await resp.json()).data;
// customer.id    -> store against your user record
// customer.default_payment_method_id

The token is consumed. The card is now stored in moat's vault and can be referenced by customer.id for any future charge.

Existing customer?

If the user is already in your database and already has a customer_id in moat, call POST /api/vault/customer/{id}/payment-method instead — adds the new card to the existing customer. Set default: true to make it the new default.

Step 3 — Charge the customer

const resp = await fetch("https://sandbox.fluidpay.com/api/transaction", {
  method: "POST",
  headers: {
    "Authorization": process.env.API_KEY,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    type: "sale",
    amount: 2500,
    payment_method: {
      customer: {
        id: customer.id
        // omit payment_method_id to use the default
      }
    },
    order_id: "ORD-00042",
    idempotency_key: "chk_ORD-00042"
  })
});
const txn = (await resp.json()).data;

Future charges

You now have a customer_id linked to your user's account in your database. Any future charge — subscription renewal, re-order, top-up — uses the same step 3 call. You never re-collect the card number.

Updating the card

When the saved card expires or the customer wants to swap it, repeat steps 1 and 2 — tokenize a new card, then attach to the existing customer with POST /api/vault/customer/{id}/payment-method. Set default: true to use the new card for subsequent charges. If you want to remove the old card, call DELETE on it after confirming the new one works.

Error handling checklist

StepFailureHandle by
Tokenizeresp.status !== "success"Render field-level errors. Do not submit the form.
VaultToken expired or invalidToken is single-use and expires in 10 minutes. Prompt the user to re-enter the card.
VaultCard rejected at vault timemoat may perform an AVS/zero-dollar check at vault time. On failure, show the decline reason and prompt for another card.
ChargeTransaction declinedCard is still in the vault. Save the transaction failure against the order; prompt for retry or alternate card.
ChargeNetwork timeoutRetry with the same idempotency_key. See Duplicate detection.

See also