Dispute evidence

Record that customers agreed to your terms and used your service, and answer card disputes with it. Full API details are in the API reference.

When a customer says they didn't make a payment or didn't get what they paid for, card networks decide using evidence that the cardholder agreed to your terms and used your service. ChainPay records that evidence as it happens and prepares your dispute response from it.

Customers who pay on ChainPay checkout agree to your terms there, so that part needs no code. Add the script below to record sign-ins and use on your own site, and to record agreement on your own checkout.

What card networks accept

Each network lists items, and asks for two or more for a digital service. ChainPay covers the items a website and its server can capture.

ChainPay recordsVisaMastercardAmerican Express
Account id, email and name on the customer’s account with you10.4: name and email linked to the customer profile4837: purchase made from a customer profileF29 (d): customer name linked to the account
Account created and email verified before the purchase10.4: profile set up and verified before the transaction date4837: customer registered with a password before buyingF29 (e): account accessed and verified on or before the transaction date
IP address and country at purchase10.4: purchaser’s IP address and device location4837: purchase from an IP associated with the accountC08: purchase IP matches the IP that used the service. F29 (c)
Device fingerprint10.4: device ID. CE 3.0 main element4837: purchase from a device associated with the accountF29 (c): device linked to the account
Sign-ins and use after paying, with times and IP addresses10.4: site accessed for the service on or after the transaction dateEvidence of use, for recurring paymentsC08: website accessed for services after the transaction date
Password asked for again just before paying——F29 (h): password re-entered in the same session as the purchase
Earlier payments from the same customer or card, not disputed10.4 and CE 3.0: two undisputed payments 120 to 365 days earlier4837: earlier undisputed purchasesF29 (f): same device and card in an earlier undisputed payment
Agreement to your terms and refund policy, ticked before paying13.6 and 13.7: terms disclosed and accepted before the purchase4853: terms disclosed to the cardholderC05 and C28: the policy the card member agreed to

Sources: Visa Core Rules, Compelling Evidence (dispute condition 10.4) and Compelling Evidence 3.0; Mastercard Chargeback Guide, reason 4837; American Express Compelling Evidence policy.

1. Add the script

Get your evidence key and list the sites it may run on in Settings, Dispute evidence. Then add the script to the pages your signed-in customers use.

html
<script src="https://app.chainpay.ac/embed/chainpay-evidence-1.0.0.js"
  integrity="sha384-G6K4LEibRjap+vf09iH81QkTD6VJ+OCy4ple0om+ge6TFrHh/xSFdWHgYsXshU1r"
  crossorigin="anonymous"></script>
javascript
// On every page a signed-in customer sees. Your evidence key (cpe_...) is
// safe in a browser. Never put your API key (cpk_...) here.
ChainPayEvidence.init({
  key: 'cpe_your_evidence_key',
  customer: {
    ref: 'user_123',            // your own id for this customer
    email: '[email protected]',
    name: 'Jane Doe'
  }
});

It needs no styles or inline code, so it works under a strict Content-Security-Policy with these two sources:

text
script-src https://app.chainpay.ac; connect-src https://api.chainpay.ac

2. Record agreement on your checkout

Show the price, the refund rule and a link to your terms in one block, with a checkbox that starts unticked. ChainPay records the block's text exactly as shown when the customer presses the button.

html
<div id="review" role="dialog" aria-labelledby="review-title">
  <h2 id="review-title">Review your purchase</h2>
  <p>Micro: $5 USD for 16,667 credits. This is a one-time payment, with no
    automatic renewal. Credits expire 90 days after purchase.</p>
  <p>Read the <a href="/refunds">Refund Policy</a> for details. Discretionary
    refunds are available within 14 days only if no more than 20% of that
    purchase's paid credits have been used, and cover the unused portion only.</p>
  <label>
    <input type="checkbox" id="agree">
    I agree to the <a href="/terms">Terms of Service</a>, including the refund
    policy, and request immediate access to the service.
  </label>
  <button type="button" id="continue" disabled>Continue to payment</button>
</div>
javascript
var agree = document.getElementById('agree');
var button = document.getElementById('continue');
agree.addEventListener('change', function () { button.disabled = !agree.checked; });

// Records the agreement when the customer presses the button.
ChainPayEvidence.consent({
  element: '#review',          // everything inside is recorded, as shown
  checkbox: '#agree',          // must start unticked
  button: '#continue',
  amount: '5.00',
  currency: 'USD',
  description: 'Micro: 16,667 credits',
  withdrawalWaiver: true       // the text asks for immediate access
}).then(function (consent) {
  // Your server creates the checkout with consent.consent_id.
  return fetch('/checkout', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ plan: 'micro', consent_id: consent.consent_id })
  });
}).then(function (response) { return response.json(); })
  .then(function (checkout) { window.location.assign(checkout.checkout_url); });

Your server then creates the checkout with the same customer id and the consent id, which links the agreement to the payment and to any later dispute:

typescript
// Your server. Pass the same customer_ref you gave the script, and the consent_id.
const session = await chainpay.createCheckoutSession({
  amount: '5.00',
  currency: 'USD',
  customer_ref: user.id,
  customer_email: user.email,
  consent_id: req.body.consent_id,
  metadata: { description: 'Micro: 16,667 credits' },
});
res.json({ checkout_url: session.checkout_url });

3. Record sign-ins and use

The script records each visit by itself. Mark the moments that matter most:

javascript
// After a successful sign-in on your site.
ChainPayEvidence.track('login');

// When the customer uses what they paid for.
ChainPayEvidence.track('action', { label: 'Generated report' });
ChainPayEvidence.track('download', { label: 'export.csv', payment_id: 'PAYMENT_ID' });

Events from your server carry more weight, because your server, not the browser, says they happened. Send the account's creation, email verification and each sign-in:

bash
curl -X POST https://api.chainpay.ac/v1/evidence/events \
  -H "Authorization: Bearer $CHAINPAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customer": { "ref": "user_123", "email": "[email protected]" },
    "events": [
      { "kind": "login", "ip": "203.0.113.7", "user_agent": "Mozilla/5.0 ..." }
    ]
  }'
typescript
// Your server, right after each of these happens. ip and user_agent are the
// customer's, from the request you're handling.
const customer = { ref: user.id, email: user.email, name: user.name };
const seen = { ip: req.ip, user_agent: req.get('user-agent') };

await chainpay.recordUsageEvents(customer, [{ kind: 'account_created', ...seen }]);
await chainpay.recordUsageEvents(customer, [{ kind: 'email_verified', ...seen }]);
await chainpay.recordUsageEvents(customer, [{ kind: 'login', ...seen }]);
// Asking for the password again just before paying counts for American Express.
await chainpay.recordUsageEvents(customer, [{ kind: 'password_reentered', ...seen }]);

4. When a dispute arrives

  • ChainPay reads the dispute from your connected Stripe account, fills Stripe's evidence fields, attaches the evidence PDF and saves it as a draft. It emails you the deadline.
  • Open it under Disputes, edit anything, and press Submit. Stripe takes one submission.
  • If you turn on automatic submission in Settings, ChainPay submits drafts you haven't touched 48 hours before the deadline.
  • If the customer deserves the money, accept the dispute instead of refunding it, so they aren't paid twice.

What the evidence contains

AgreementThe exact text the customer saw, the checkbox they ticked (and proof it started unticked), when they ticked it and pressed continue, and copies of your terms and refund pages as ChainPay fetched them at that moment.
IdentityAccount id, email, name, IP address and country, and device fingerprint at purchase, compared with every later visit.
UseEach sign-in and use of your service before and after paying, with times and IP addresses, written as Stripe’s activity log.
HistoryEarlier payments from the same customer or card that weren’t disputed.
TimestampsA timestamp token from an independent authority (RFC 3161) for every record, so anyone can check a record existed at that time and hasn’t changed since.

Download it as JSON or PDF from any payment's page, or from the API at GET /v1/payments/{id}/evidence.

Check a timestamp yourself

Records are batched every hour. The batch's Merkle root is timestamped by an independent authority, and each record carries the proof that it's in the batch. With OpenSSL:

bash
# From the evidence JSON: save the consent's timestamp token and Merkle root.
echo "TSA_TOKEN_BASE64" | base64 -d > token.tsr

# Read it: the time, the authority, and the hash it covers.
openssl ts -reply -in token.tsr -text

# Check the authority's signature over the root, against your system's root
# certificates (the token carries the authority's own certificate). Prints
# "Verification: OK". On Debian or Ubuntu use /etc/ssl/certs/ca-certificates.crt.
openssl ts -verify -digest MERKLE_ROOT -in token.tsr -CAfile /etc/ssl/cert.pem

# Then hash record_json yourself and walk the proof up to MERKLE_ROOT.
printf '%s' "$RECORD_JSON" | shasum -a 256

What this doesn't prove

  • Nothing makes a dispute impossible to lose. The card issuer decides, and evidence makes winning likely, not certain.
  • It shows a device and account used your service, not who sat at the keyboard. A shared or stolen account looks the same.
  • It doesn't shift fraud liability the way 3-D Secure does. Use 3-D Secure on cards when you need that.
  • Records from the browser are only as honest as the browser. Send sign-ins from your server as well.