PayFacto API - v2.0 - Solution : Secure Hosted Checkout (SHC)
Secure Hosted Checkout (SHC) is a JavaScript library you add to your own page. It draws a form inside a secure iframe โ the cardholder name, card number, expiry, and CVV go directly from the cardholder's browser to PayFacto. Your page never touches raw card data, which keeps you out of scope for the strictest PCI requirements.
Version: v2.0 | Last updated: 2026-09-16
2
Step 1 โ Request a Session Token (Server-Side)
โ ๏ธ
Call the session endpoint from your server, not the browser. The Authorization header contains your API secret. If you call it from the browser, anyone can steal your key.
โน๏ธ
The session token is a one-time token that expires in 5 minutes by default (configurable via expires). Request it fresh for each checkout โ never cache or reuse tokens.
Required session fields
| Field |
Type |
Description |
| transactionType |
string ยท required |
What to do with the card. VERIFY โ validate the card, no money moves. PURCHASE โ charge immediately. PREAUTHORIZATION โ reserve funds, capture later. PURCHASE and PREAUTHORIZATION also require purchaseAmount. |
| language |
string ยท required |
Language for the form. en = English, fr = French. |
Optional session fields
| Field |
Type / Default |
Description |
| allowList |
array of strings |
Restrict the payment methods offered by the session. One or more of AMEX, DINERS, DISCOVER, INTERAC_DIRECT, JCB, MASTERCARD, VISA, APPLE_PAY, GOOGLE_PAY, KONEK, PAYMENT_FORM. When omitted, every payment method enabled on the merchant account is offered. See Restricting Payment Methods โ allowList below. |
| purchaseAmount |
integer |
Amount in cents. $10.25 = 1025. Required for PURCHASE and PREAUTHORIZATION. The amount set here is the amount charged โ a different value typed in the form is ignored. |
| amount |
string |
How to display the amount field. disabled, editable, or hidden. If purchaseAmount is set and amount is not passed, the field is hidden. |
| avs |
string |
Show address verification fields. postal (postal code only) or both (street + postal). If full AVS is enabled on your account, ignore this option. |
| disableSubmitIndicator |
boolean ยท default false |
Set true to stop the Pay button from changing to a spinner on submit. |
| email |
boolean ยท default false |
Show the email address field. Ignored if 3DS is active โ when 3DS is on, the email field is shown automatically and is required. |
| expires |
string (ISO-8601) |
When the session token expires. Default: 5 minutes from creation. Format: YYYY-MM-DDTHH:MM:SS.000Z. |
| ignoreInitAuthTimeout |
boolean ยท default false |
Set true to ignore a 3DS device-collection timeout and continue the 3DS flow. Warning: this may shift fraud liability to the merchant. |
| invoice |
string |
How to display the invoice field. disabled, editable, or hidden. |
| invoiceNumber |
string ยท max 25 chars |
Pre-fill the invoice number. If invoice is not set, the field displays as read-only. |
| transactionDescription |
string |
Text shown in the description field of the generated receipt. |
| useRecaptcha |
boolean ยท default true |
Set false to disable Google reCAPTCHA. Only disable if reCAPTCHA causes issues on your site โ disabling it increases exposure to automated attacks. |
| webhookUrl |
string (URL) |
An extra HTTPS URL that PayFacto will POST the transaction result to in addition to the browser callback. |
JSON โ Session request with all options
{
"transactionType": "PURCHASE",
"purchaseAmount": 1025,
"language": "en",
"amount": "hidden",
"avs": "both",
"email": true,
"invoice": "editable",
"invoiceNumber": "INV-001",
"transactionDescription": "Order #2024-001",
"expires": "2026-06-30T23:59:59.0Z",
"useRecaptcha": true,
"webhookUrl": "https://your-site.com/webhook",
"disableSubmitIndicator": false,
"ignoreInitAuthTimeout": false
}JavaScript โ Server-side session request
// Server-side โ Node.js example
const response = await fetch(
'https://uat.ca.shc.payfacto.cloud/secure-hosted-checkout/v1/sessions',
{
method: 'POST',
headers: {
'Authorization': `Basic ${Buffer.from('username:password').toString('base64')}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
transactionType: 'PURCHASE',
purchaseAmount: 1025, // $10.25 in cents โ mandatory for PURCHASE
language: 'en'
})
}
);
const { token } = await response.json();
// Return this token to the browser โ never expose your API key
โ
Restricting Payment Methods โ allowList
Pass allowList in the session request to restrict which payment methods a session offers. It takes an array containing one or more of the following values:
| Value |
What it controls |
AMEX, DINERS, DISCOVER, JCB, MASTERCARD, VISA |
Card brands. Only the listed brand images are displayed, and a card number of any other brand is flagged as invalid in the form. |
INTERAC_DIRECT |
Konek (pay-by-bank) payment method. |
APPLE_PAY |
Offers Apple Pay. Only offered when this value is present, and the card networks Apple Pay accepts are limited to the card brands also listed in allowList. |
GOOGLE_PAY |
Offers Google Pay. Only offered when this value is present, and the card networks Google Pay accepts are limited to the card brands also listed in allowList. |
KONEK |
Offers the Konek wallet option. Only offered when this value is present. |
PAYMENT_FORM |
Shows the card entry fields. Required to accept card payments at all when allowList is set โ see the warning below. |
โน๏ธ
When allowList is omitted entirely, every payment method enabled on the merchant account is offered.
โ ๏ธ
PAYMENT_FORM Must Be Listed to Accept Cards
The card payment fields are only displayed when PAYMENT_FORM is listed. An allowList without PAYMENT_FORM offers the listed wallets only, and the server rejects any card submission with CARD_DECLINED / PAYMENT_FORM_NOT_ALLOWED.
This restriction is enforced on both sides โ hiding fields in the form is a convenience, not the security boundary. The server independently rejects a card whose brand (detected from the card number) is not listed, with CARD_DECLINED / CARD_BRAND_NOT_ALLOWED, at /browser/process and again during 3DS authentication.
JSON โ Restrict a session to Visa, Mastercard, and Apple Pay
{
"transactionType": "PURCHASE",
"purchaseAmount": 1025,
"language": "en",
"allowList": [
"VISA",
"MASTERCARD",
"APPLE_PAY",
"PAYMENT_FORM"
]
}The allowList value you send is echoed back in the session response.
4
Step 3 โ Pass the Token to shc() in the Browser
โน๏ธ
Your server returns the session token to the browser as JSON. The browser then calls the global shc() function with the token as the first argument. The second argument is a callback. The callback fires when the cardholder clicks Pay. The third argument is optional and is an object containing browser-side options.
JavaScript โ Fetch token from server and call shc()
// Fetch the session token from your server, then call shc()
function getShcToken(done) {
fetch('/your-server/get-shc-token')
.then(r => r.json())
.then(data => done(data.token));
}
getShcToken(shcToken => {
shc(shcToken, response => handleResponse(response));
});
// shc() draws the iframe inside #shc-fields automatically
Third argument โ browser-side options
โน๏ธ
These options are different from session options โ they control how the iframe behaves once it is created. Pass them as the third argument to shc(). Each entry in fields must have both visibility and value or SHC throws an error.
| Option |
Description |
| sourceElement |
Override the default shc-fields container. Pass the id of any div on your page. |
| cardholderName |
Pre-fill the cardholder name and hide the name field. |
| fields |
Object of field overrides. Each key is a field name; each value must have visibility (hidden, disabled, or enabled) and value. |
JavaScript โ Third argument (browser options)
const options = {
sourceElement: 'my-checkout-div', // use a different container id
cardholderName: 'Jane Doe', // pre-fill and hide the name field
purchaseAmount: 1025, // hidden field, required for 3DS
fields: {
email: { visibility: 'enabled', value: 'jane@example.com' },
postal: { visibility: 'disabled', value: 'M5V 1A1' },
address: { visibility: 'hidden', value: '123 Main St' }
}
};
shc(shcToken, response => handle(response), options);5
Step 4 โ Handle the Callback Response
โ ๏ธ
Never fulfil an order based on the callback alone. Always verify the HMAC on your server (Step 5) before releasing goods or services.
Successful response (Pay by card)
JSON โ Success callback
{
"success": true,
"response": {
"authCode": "754062",
"avs": "U",
"brand": "VISA",
"card": "************4242",
"cvv2Cvc2Status": "M",
"expiry": "1226",
"invoiceNumber": "41343428",
"isoResponseCode": "00",
"issuerMessage": "AP",
"lastFour": "4242",
"name": "Test Card",
"paymentMethod": "VISA CARD",
"purchaseAmount": 500,
"purchaseCurrency": "CAD",
"purchaseDate": "2026-06-23 17:49:16 EDT",
"secureId": "33f19cdc-5b74-456a-8e95-51527c5cc910",
"status": "SUCCEEDED",
"store": {
"address": {
"line1": "123 Some Street",
"city": "Toronto",
"state": "ON",
"postalCode": "A1A 1A1",
"country": "CA"
},
"email": "contact@merchant-store.com",
"name": "Merchant Store",
"phone": "(123) 456-7890",
"website": "www.merchant.com"
},
"token": "c2FLxMZn51DZAMjAM3CkcppWf",
"transactionDescription": "Transaction Description",
"transactionId": "af569819-0125-4979-9dff-a81cda21ebd0",
"transactionType": "PURCHASE",
"type": "CREDIT"
},
"metadata": {
"algorithm": "sha256",
"HMAC": "3928cbccac77e3b66404c37bbfa2ec44dc8f839ed478fcc4637e29f600ec70e4",
"receipt": "",
"timestamp": "2026-06-23 17:49:16 EDT"
}
}Successful response (Pay by Konek)
JSON โ Success callback
{
"success": true,
"response": {
"accountType": "BANK_ACCOUNT_CHEQUING",
"expiry": "1230",
"invoiceNumber": "61341840",
"lastFour": "6457",
"paymentConsentId": "37c3449c-e876-4190-9284-3a744048e8b5",
"paymentMethod": "INTERAC_DIRECT",
"purchaseAmount": 500,
"purchaseCurrency": "CAD",
"purchaseDate": "2026-06-23 17:34:12 EDT",
"secureId": "d2f01954-a644-45b9-b1f3-03930f436af5",
"status": "SUCCEEDED",
"store": {
"address": {
"line1": "123 Some Street",
"city": "Toronto",
"state": "ON",
"postalCode": "A1A 1A1",
"country": "CA"
},
"email": "contact@merchant-store.com",
"name": "Merchant Store",
"phone": "(123) 456-7890",
"website": "www.merchant.com"
},
"transactionDescription": "Transaction Description",
"transactionId": "3ab9cd17-babb-4c53-aa02-23ed4bb0e962",
"transactionType": "PURCHASE",
"type": "INTERAC_DIRECT"
},
"metadata": {
"algorithm": "sha256",
"HMAC": "ee0600b30d0f0c955d40854334fd97245eb3c30d4bebc60668310c83fb221c4c",
"receipt": "",
"timestamp": "2026-06-23 17:34:12 EDT"
}
}
| Field |
Description |
| success | true if the transaction succeeded. |
| response.authCode | Authorization code from the card issuer. Display on the receipt. |
| response.status | SUCCEEDED, DECLINED, etc. |
| response.brand | Card brand โ Visa, Mastercard, Amex, etc. |
| response.card | Masked card number, safe to show on a receipt. |
| response.lastFour | Last 4 digits of the card. |
| response.token | Reusable card token. Store securely for future charges. |
| response.transactionId | Unique PayFacto transaction identifier. |
| response.receipt | HTML receipt you can display on screen or send by email. |
| response.purchaseAmount | Amount charged, in cents. |
| response.purchaseCurrency | Currency of the transaction. |
| metadata.HMAC | SHA-256 fingerprint of the response. Verify this on your server before fulfilling the order. |
| metadata.algorithm | Hashing algorithm used for the HMAC. Currently always sha256. |
| metadata.timestamp | ISO-8601 timestamp of the response. |
Additional response fields
| Field |
Description |
| accountType | Pay-by-bank bank account type. |
| avs | The AVS response code. Omitted for Konek. |
| cvv2Cvc2Status | The CVV. Valid values are 'M' for PASS and 'N' for FAIL. Omitted for Konek. |
| email | The cardholder's e-mail address. |
| expiry | Expiry date of the card. |
| invoiceNumber | Passed invoice number. If no invoice number is passed then one is generated on the fly. |
| isoResponseCode | ISO response code from issuer. Omitted for pay-by-bank transactions. |
| issuerMessage | Message from the issuer. Omitted for pay-by-bank transactions. |
| name | Cardholder name. Omitted for Konek. |
| paymentConsentId | Verification ID for pay-by-bank. |
| paymentMethod | Card brand and type. If the transaction is pay-by-bank then the type is INTERAC_DIRECT. |
| processorAvsResultCode | The AVS response code returned from the processor. Value varies by card brand and processor. Omitted for Konek. |
| purchaseDate | The date and time stamp when the transaction was processed. |
| secureId | The session ID. |
| store.address.line1 | Store address line1. |
| store.address.city | Store city. |
| store.address.state | Store state, province, or territory. |
| store.address.postalCode | Store postal or ZIP code. |
| store.address.country | Store ISO 3166-1 alpha-2 country code. [CONFIRM: source list named this field "store.address.store" โ reproduced here as "country" to match the field name shown in the JSON example] |
| store.email | Store support contact e-mail address. |
| store.name | Name of the store the merchant account is associated with. |
| store.phone | Store support contact phone number. |
| store.website | Store website. |
| transactionDescription | The value that shows up on the description field of a receipt. |
| transactionType | The type of requested transaction. Valid values are 'PREAUTHORIZATION', PURCHASE, and 'VERIFY'. |
| type | Card type. Valid values are 'CREDIT' and 'DEBIT'. |
| algorithm | The algorithm used to generate an HMAC. |
| HMAC | The contents of the response converted to a string and then encrypted using the sha256 algorithm and the merchant account's private API password. |
| timestamp | The time the transaction was generated. |
| receipt | An HTML receipt that can be displayed on the screen or embedded in an e-mail. |
Error response
JSON โ Error callback
{
"success": false,
"response": {
"type": "CARD_DECLINED",
"code": "INVALID_NUMBER",
"message": "card number is incorrect"
}
}
| Error type |
Error codes |
| CARD_DECLINED |
INVALID_CARD_NUMBER, AVS_DECLINED, CARD_BRAND_NOT_SUPPORTED, CARD_BRAND_NOT_ALLOWED, CVV_DECLINED, RISK_DECLINED, ISSUER_DECLINED, PAYMENT_FORM_NOT_ALLOWED |
| AMOUNT_DECLINED |
AMOUNT_TOO_LARGE, AMOUNT_TOO_SMALL |
| UNAUTHORIZED |
โ |
| BATCH_CLOSING |
BATCH_CLOSING |
| RECAPTCHA_VERIFICATION_FAILED |
โ |
| TOO_MANY_REQUESTS |
โ |
| INTERNAL_ERROR |
โ or ISSUER_TIMEOUT |
| TIMEOUT |
TIMEOUT |
3D Secure error codes
| Type |
Code |
Meaning |
| CARD_ERROR | NOT_VERIFIED | Could not verify the card number. |
| CARD_ERROR | NOT_AUTHENTICATED | The issuer rejected the card during 3DS. |
| DOWNSTREAM_ERROR | NOT_VERIFIED | The 3DS provider is having technical issues. |
6
Step 5 โ Verify the HMAC (Server-Side)
โ ๏ธ
Always verify HMACs on your server, never in the browser. The check only proves anything if your API secret stays secret.
โน๏ธ
Run JSON.stringify(response.response) โ the inner response object, not the full envelope โ through HMAC-SHA256 keyed with your API secret. Compare the result against metadata.HMAC. If they differ, discard the response and do not fulfil the order.
JavaScript โ HMAC verification
const crypto = require('crypto');
function verifyHmac(shcResponse, apiSecret) {
const computed = crypto
.createHmac(shcResponse.metadata.algorithm, apiSecret)
.update(JSON.stringify(shcResponse.response))
.digest('hex');
return computed === shcResponse.metadata.HMAC;
}
if (!verifyHmac(response, process.env.API_SECRET)) {
throw new Error('HMAC mismatch โ do not process this response');
}
// Safe to fulfil the orderโน๏ธ
If the default styling options are not enough, you can build your own form. Wrap it in a <div> with an id, and pass that id to shc() as sourceElement. Give any input the matching SHC field id to activate validation, formatting, and secure submission for that field.
โ ๏ธ
Before SHC copies your custom form into the secure iframe, it strips <script>, <iframe>, <object>, <a> tags, any <input type=submit> or <button type=submit>, the action and method attributes from <form> tags, and all inline JavaScript event handlers. This prevents code injection.
HTML + JavaScript โ Minimal custom form
<div id="custom-form">
<form style="display: flex; flex-direction: column; gap: 12px;">
<!-- Hidden name field โ value pre-filled -->
<input type="hidden" name="name" value="JOHN DOE" />
<!-- SHC handles validation for fields with matching ids -->
<input id="shc-card-input" name="card" type="text" placeholder="1234 1234 1234 1234" required autocomplete="cc-number" />
<input id="shc-expiry-input" name="expiry" type="text" placeholder="MM / YY" required />
<input id="shc-cvv-input" name="cvv" type="text" placeholder="123" required autocomplete="cc-csc" />
<!-- 3DS hidden fields (required if 3DS is enabled on your account) -->
<input id="purchaseAmount" name="purchaseAmount" type="hidden" value="1025" />
<input id="purchaseCurrency" name="purchaseCurrency" type="hidden" value="CAD" />
<input id="purchaseDate" name="purchaseDate" type="hidden" value="2026-06-01T14:30:00.000Z" />
<button id="shc-pay-button" type="button">Pay</button>
</form>
</div>
<script>
shc(shcToken, response => handle(response), { sourceElement: 'custom-form' });
</script>
3DS hidden fields โ required if your account uses 3D Secure
| Hidden field id |
Value format |
Description |
| purchaseAmount | Integer (minor units) | Amount in cents. $9.20 = 920. |
| purchaseCurrency | String | Must match your merchant account currency. Usually CAD. |
| purchaseDate | ISO-8601 extended | Date/time in YYYY-MM-DDTHH:MM:SS.000Z format. |
Sending messages from custom buttons
Add a data-message attribute to any button inside your custom form. When clicked, SHC sends the message to the parent page. Listen with a message event listener:
JavaScript โ Custom button messages
window.addEventListener('message', event => {
console.log(event.data); // { message: 'whatever you put in data-message' }
});โน๏ธ
SHC reads a <style> tag with the id shc-style and injects it into the iframe. This is the only way to style the iframe โ normal CSS on your page does not reach inside it.
HTML โ Style block location
<style id="shc-style">
/* Your custom styles here โ applied inside the SHC iframe */
</style>
Common style recipes
CSS โ Hiding icons, removing borders, showing card brands
/* Hide all field icons */
.confidante-field-img svg { visibility: collapse; }
/* Hide just the card icon */
#shc-card-img { visibility: collapse; }
/* Move icons to the right side */
.confidante-field-img svg { left: auto; right: 5px; }
.confidante-field-validate svg { display: none; }
/* Show supported card brands (hidden by default) */
#shc-card-brands { display: flex; justify-content: center; }
/* Show the session timeout countdown (hidden by default) */
#shc-countdown { display: block; }
/* Remove the outer border */
#target { border: 0; padding: 0; padding-top: 20px; }
CSS โ Full re-skin (underline style)
/* Full re-skin โ underline style with a custom button color */
#shc label {
margin-bottom: 15px;
position: relative;
border: 0;
border-bottom: 1px solid #ddd;
box-shadow: none;
}
#shc input:not(.shc-hidden-fields) {
width: 100%;
padding: 20px 0 10px 0;
margin-top: 20px;
border: none;
outline: none;
}
#shc span.confidante-field-span {
position: absolute; top: 0; left: 0;
transform: translateY(40px);
font-size: 0.825em;
transition-duration: 400ms;
}
#shc button {
padding: 15px 0;
margin-top: 20px;
background: #BC955C;
color: #fff;
border: 1px solid #BC955C;
cursor: pointer;
border-radius: 3px;
}
#shc label:focus-within > span.confidante-field-span,
#shc input:not(:placeholder-shown) + span.confidante-field-span {
color: #BC955C;
transform: translate(32px, 12px);
}
Available CSS ids and classes
| Selector |
What it targets |
| .confidante-field-label | Each field label wrapper |
| .confidante-field-img | Icon area at the start of each field |
| .confidante-field-input | The input element inside each field |
| .confidante-field-span | The label text (animates on focus) |
| .confidante-field-validate | Validation icon area at the end of each field |
| .confidante-field-mask | Card number masking overlay |
| #shc-{name}-input | Target a specific field, e.g. #shc-card-input |
| #shc-pay-button | The Pay button |
| #shc-card-brands | Card brand logos row (hidden by default) |
| #shc-countdown | Session timeout countdown (hidden by default) |
| #target | The outer iframe container / border |
โ
Listening for Events and Sending Commands โ shcMessenger
โน๏ธ
shc() returns an shcMessenger object. Use .on() to listen for events from the form, and the send methods to control the form programmatically.
JavaScript โ shcMessenger events and commands
let shcMessenger;
getShcToken(shcToken => {
shcMessenger = shc(shcToken, response => handle(response));
});
// โโ Listen for form events โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
shcMessenger.on('shc-card-brand', data => console.log('Brand:', data.brand));
shcMessenger.on('shc-ready-to-submit', data => console.log('Ready:', data.isReady));
shcMessenger.on('shc-submit', () => console.log('Form submitted'));
shcMessenger.on('shc-3ds', () => console.log('3DS started'));
shcMessenger.on('shc-3ds-challenge', () => console.log('3DS challenge shown'));
shcMessenger.on('shc-response', data => console.log('Result:', data));
shcMessenger.on('shc-reset', () => console.log('Form reset'));
// โโ Send commands to the form โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
shcMessenger.sendBlockSubmit(); // disable Pay button programmatically
shcMessenger.sendUnblockSubmit(); // re-enable Pay button
shcMessenger.sendSubmit(); // submit the form (ignored if fields invalid)
shcMessenger.sendCancel(); // destroy the form entirely
| Event |
When it fires |
Payload |
| shc-card-brand | As soon as the card brand is detected from the number entered. | { brand: "Visa" } |
| shc-ready-to-submit | All fields are valid and the Pay button is enabled โ or re-fires with isReady: false if a field becomes invalid again. | { isReady: true } |
| shc-submit | The cardholder pressed Pay and the form was submitted. | โ |
| shc-3ds | The 3DS process has started. | โ |
| shc-3ds-challenge | The 3DS challenge step is being shown to the cardholder. | โ |
| shc-response | The same data as the main callback. Useful if you need to react in more than one place. | response object |
| shc-reset | A recoverable error occurred (often a digital wallet issue) and the form has reset itself. | โ |