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 records | Visa | Mastercard | American Express |
|---|---|---|---|
| Account id, email and name on the customer’s account with you | 10.4: name and email linked to the customer profile | 4837: purchase made from a customer profile | F29 (d): customer name linked to the account |
| Account created and email verified before the purchase | 10.4: profile set up and verified before the transaction date | 4837: customer registered with a password before buying | F29 (e): account accessed and verified on or before the transaction date |
| IP address and country at purchase | 10.4: purchaser’s IP address and device location | 4837: purchase from an IP associated with the account | C08: purchase IP matches the IP that used the service. F29 (c) |
| Device fingerprint | 10.4: device ID. CE 3.0 main element | 4837: purchase from a device associated with the account | F29 (c): device linked to the account |
| Sign-ins and use after paying, with times and IP addresses | 10.4: site accessed for the service on or after the transaction date | Evidence of use, for recurring payments | C08: 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 disputed | 10.4 and CE 3.0: two undisputed payments 120 to 365 days earlier | 4837: earlier undisputed purchases | F29 (f): same device and card in an earlier undisputed payment |
| Agreement to your terms and refund policy, ticked before paying | 13.6 and 13.7: terms disclosed and accepted before the purchase | 4853: terms disclosed to the cardholder | C05 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.
<script src="https://app.chainpay.ac/embed/chainpay-evidence-1.0.0.js"
integrity="sha384-G6K4LEibRjap+vf09iH81QkTD6VJ+OCy4ple0om+ge6TFrHh/xSFdWHgYsXshU1r"
crossorigin="anonymous"></script>// 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:
script-src https://app.chainpay.ac; connect-src https://api.chainpay.ac2. 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.
<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>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:
// 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:
// 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:
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 ..." }
]
}'// 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
| Agreement | The 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. |
| Identity | Account id, email, name, IP address and country, and device fingerprint at purchase, compared with every later visit. |
| Use | Each sign-in and use of your service before and after paying, with times and IP addresses, written as Stripe’s activity log. |
| History | Earlier payments from the same customer or card that weren’t disputed. |
| Timestamps | A 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:
# 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 256What 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.