Tokenization

The Tokenization Component provides a secure, embeddable form for collecting debit card information. It is an alternative to the Cards API. Like the Cards API, the Tokenization Component may be used to process card data for use when creating a card account for a user. The Component is designed for use in mobile apps (WebView) and web apps (iframe), and supports robust validation and cross-platform messaging. This guide explains how to integrate the Tokenization Component in a native application using a WebView.

Tokenization UI

Architecture Overview

The DailyPay Debit Card Tokenization Component is a secure, embeddable web form designed for seamless integration into mobile and web applications. It is intended to be loaded in a WebView (React Native, iOS, Android) or iframe, and communicates with the host application using the postMessage API. The component handles all user input, validation, and securely transmits card data to a DailyPay backend service, which performs the actual tokenization with the payment gateway. Only the resulting token and non-sensitive metadata are returned to the host. No sensitive card data is ever exposed to or stored by the host application.

Component Flow

  1. Embedding: The host app loads the component in a WebView or iframe, passing the DailyPay backend tokenization API URL as a parameter.
  2. User Input: The user enters their debit card and address details into the form. All validation and formatting is handled client-side.
  3. Tokenization: On submit, the component sends the card data via a secure API call to a DailyPay backend service. The backend service performs the actual tokenization with the payment gateway and returns a token to the component.
  4. Result Messaging: On success or failure, the component sends a DAILYPAY_CARD_TOKENIZE_RESPONSE message to the host via postMessage, containing either the token and metadata or error details.
  5. Host Handling: The host listens for this message and processes the token as needed (e.g., for account linking or payment setup).

Component Boundaries

  • Frontend (this component): Handles all UI, validation, formatting, and secure transmission of card data to the DailyPay backend. Communicates only via postMessage with the host, and via HTTPS with the backend.
  • DailyPay Backend Service: Receives card data from the component, performs tokenization with the payment gateway, and returns only the token and non-sensitive metadata to the component.
  • Host App: Responsible for embedding the component, listening for messages, and handling the tokenized result. Never processes or stores raw card data.

Notes

  • The component is robust to being embedded in a variety of host environments and will use the correct postMessage bridge automatically.
  • The form disables the submit button and shows a spinner during submission to prevent duplicate requests.
  • The key data (name, address, issuer or BIN (first 6 digits), etc.) is extracted from the submission and included in the response for downstream API use.

Tokenization Component URLs

The Tokenization Component is hosted at two URLs:

Production: https://companion.workloads.production.dailypay.com/v1/widgets/debitcard/tokenizer

UAT: https://companion.workloads.uat.dailypay.com/v1/widgets/debitcard/tokenizer

Required Query Parameters

The following parameters are required whenever the Component is used:

Parameter Notes
client_id The client ID used in authorization, supplied by DailyPay
implementation_id The ID of this particular application, supplied by DailyPay

For example: https://companion.../tokenizer?client_id=123&implementation_id=3213

Basic Integration Steps

  1. Import required dependencies
  2. Set up the WebView with the tokenizer URL
  3. Implement the message handler
  4. Process the tokenized card data

Response Messages

The widget communicates with your app using the postMessage API:

Successful Tokenization

{
  "type": "DAILYPAY_CARD_TOKENIZE_RESPONSE",
  "response": {
    "first_name": "...", // User's first name
    "last_name": "...", // User's last name
    "address_line_one": "...", // First line of address
    "address_line_two": "...", // Second line of address (optional)
    "address_city": "...", // City
    "address_state": "...", // State (2-letter abbreviation)
    "address_zip_code": "...", // ZIP code
    "address_country": "USA", // Country (always set to "USA")
    "expiration_month": "...", // Card expiration month (MM format)
    "expiration_year": "...", // Card expiration year (YY or YYYY format)
    "issuer": "...", // First 6 digits of the card number (BIN)
    "token": "..." // Tokenized card data from payment gateway
  },
  "success": true
}

Failed Tokenization

{
  "type": "DAILYPAY_CARD_TOKENIZE_RESPONSE",
  "success": false
  // Error details
}

Handling Responses

  1. Always check the type property to identify the message
  2. Verify the success flag to determine if tokenization succeeded
  3. Access tokenized data via the response property
  4. For errors, check the error details in the response

Utilizing a Successful Response with the Accounts API

The response object returned with DAILYPAY_CARD_TOKENIZE_RESPONSE may be used directly in the POST request to the accounts endpoint as the value for the details field when the fields "account_type": "CARD", "subtype": "DEBIT" are used. This action will create a transfer account for the user based on the card details submitted to the webview.

Security Best Practices

  • Always use HTTPS URLs in production
  • Don't store sensitive card information locally
  • Implement proper error handling

Troubleshooting

  • Enable WebView debugging to see console messages
  • Check network connectivity if widget fails to load
  • Verify that JavaScript is enabled in the WebView

Code Samples

Apple iOS

import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {
    let url: URL
    let handlerName = "iosListener"
    var onTokenReceived: ((String) -> Void)?
    var onFullResponseReceived: ((DailypayCardTokenizeResponse) -> Void)?

    class Coordinator: NSObject, WKScriptMessageHandler {
        var onIssuerReceived: ((String) -> Void)?
        var onTokenReceived: ((String) -> Void)?
        var onFullResponseReceived: ((DailypayCardTokenizeResponse) -> Void)?
        init(onTokenReceived: ((String) -> Void)?, onFullResponseReceived: ((DailypayCardTokenizeResponse) -> Void)?) {
            self.onTokenReceived = onTokenReceived
            self.onFullResponseReceived = onFullResponseReceived
        }
        func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
            print("Received postMessage: \(message.body)")
            var data: Data? = nil
            if let dict = message.body as? [String: Any] {
                data = try? JSONSerialization.data(withJSONObject: dict)
            } else if let str = message.body as? String {
                data = str.data(using: .utf8)
            }
            if let data, let msg = try? JSONDecoder().decode(DailypayCardTokenizeMessage.self, from: data),
               msg.type == "DAILYPAY_CARD_TOKENIZE_RESPONSE" {
                onIssuerReceived?(msg.response.issuer)
                onTokenReceived?(msg.response.token)
                onFullResponseReceived?(msg.response)
            }
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(onTokenReceived: onTokenReceived, onFullResponseReceived: onFullResponseReceived)
    }

    func makeUIView(context: Context) -> WKWebView {
        let contentController = WKUserContentController()
        contentController.add(context.coordinator, name: handlerName)
        let config = WKWebViewConfiguration()
        config.userContentController = contentController

        // Forward window.postMessage to iOS
        let js = """
        window.postMessage = function(data) {
            window.webkit.messageHandlers.\(handlerName).postMessage(data);
        };
        """
        let userScript = WKUserScript(source: js, injectionTime: .atDocumentStart, forMainFrameOnly: false)
        contentController.addUserScript(userScript)

        let webView = WKWebView(frame: .zero, configuration: config)
        let request = URLRequest(url: url)
        webView.load(request)
        return webView
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
        // No-op
    }
}

struct DailypayCardTokenizeResponse: Decodable {
    let firstName: String
    let lastName: String
    let addressLineOne: String
    let addressLineTwo: String
    let addressCity: String
    let addressState: String
    let addressZipCode: String
    let expirationMonth: String
    let expirationYear: String
    let addressCountry: String
    let issuer: String
    let token: String

    enum CodingKeys: String, CodingKey {
        case firstName = "first_name"
        case lastName = "last_name"
        case addressLineOne = "address_line_one"
        case addressLineTwo = "address_line_two"
        case addressCity = "address_city"
        case addressState = "address_state"
        case addressZipCode = "address_zip_code"
        case expirationMonth = "expiration_month"
        case expirationYear = "expiration_year"
        case addressCountry = "address_country"
        case issuer
        case token
    }
}

struct DailypayCardTokenizeMessage: Decodable {
    let type: String
    let response: DailypayCardTokenizeResponse
    let success: Bool
}

struct ContentView: View {
    // Replace client_id and implementation_id with values supplied by DailyPay
    // that are unique to you and your application
    @State private var urlString: String = "https://companion.workloads.production.dailypay.com/v1/widgets/debitcard/tokenizer?client_id=123&implementation_id=3213"
    @State private var showAlert = false
    @State private var issuer: String? = nil
    @State private var showTokenAlert = false
    @State private var token: String? = nil

    func handleCardTokenizeResponse(_ response: DailypayCardTokenizeResponse) {
        // Add card to DailyPay using POST /accounts
        // See https://developer.dailypay.com/tag/Accounts#operation/createAccount
        print("Ready to send to the DailyPay REST API: \(response)")
    }

    var body: some View {
        VStack {
            TextField("Enter URL", text: $urlString)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
            if let url = URL(string: urlString) {
                WebView(
                    url: url,
                    onTokenReceived: { token in
                        self.token = token
                        self.showTokenAlert = true
                    },
                    onFullResponseReceived: { response in
                        handleCardTokenizeResponse(response)
                    }
                )
                .edgesIgnoringSafeArea(.all)
            } else {
                Text("Invalid URL")
            }
        }
        .alert(isPresented: $showTokenAlert) {
            Alert(
                title: Text("Token Received"),
                message: Text(token ?? "No token"),
                dismissButton: .default(Text("OK"))
            )
        }
    }
}

#Preview {
    ContentView()
}

Google Android

package com.example.webviewrunner

import android.os.Bundle
import android.webkit.JavascriptInterface
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import android.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    private lateinit var webView: WebView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        webView = findViewById(R.id.webView)

        // Configure WebView settings
        val webSettings: WebSettings = webView.settings
        webSettings.javaScriptEnabled = true // Enable JavaScript if your page uses it
        webSettings.setSupportZoom(true) // Enable zoom controls
        webSettings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW // Allow mixed content (HTTP/HTTPS)
        webSettings.domStorageEnabled = true // Enable DOM storage

        // Set a WebViewClient to handle page navigation within the WebView itself
        webView.webViewClient = object : WebViewClient() {
            // This method is deprecated in API 24 and later, but still useful for older devices
            override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
                url?.let {
                    view?.loadUrl(it)
                }
                return true // Return true to indicate that the app handled the URL
            }

            // For API 24 and later
            override fun shouldOverrideUrlLoading(view: WebView?, request: android.webkit.WebResourceRequest?): Boolean {
                request?.url?.let { uri ->
                    view?.loadUrl(uri.toString())
                }
                return true // Return true to indicate that the app handled the URL
            }

            override fun onReceivedError(view: WebView?, request: android.webkit.WebResourceRequest?, error: android.webkit.WebResourceError?) {
                super.onReceivedError(view, request, error)
                android.util.Log.e("WebView", "Error: ${error?.description}")
            }
            override fun onReceivedHttpError(view: WebView?, request: android.webkit.WebResourceRequest?, errorResponse: android.webkit.WebResourceResponse?) {
                super.onReceivedHttpError(view, request, errorResponse)
                android.util.Log.e("WebView", "HTTP error: ${errorResponse?.statusCode}")
            }
        }

        // Add JavaScript interface for postMessage
        webView.addJavascriptInterface(object {
            @JavascriptInterface
            fun postMessage(message: String) {
        // Interpret response and send to DailyPay using POST /accounts
        // See https://developer.dailypay.com/tag/Accounts#operation/createAccount

        // For demo - alert the response payload here.
                runOnUiThread {
                    AlertDialog.Builder(this@MainActivity)
                        .setTitle("Message from WebView")
                        .setMessage(message)
                        .setPositiveButton("OK", null)
                        .show()
                }
            }
        }, "AndroidInterface")

        // Replace client_id and implementation_id with values supplied by DailyPay
        // that are unique to you and your application
        webView.loadUrl("https://companion.workloads.production.dailypay.com/v1/widgets/debitcard/tokenizer?client_id=123&implementation_id=3213")
    }

    // Handle back button presses to navigate within the WebView's history
    override fun onBackPressed() {
        if (webView.canGoBack()) {
            webView.goBack()
        } else {
            super.onBackPressed()
        }
    }
}

React Native

Install dependencies: npm install react-native-webview or yarn add react-native-webview

import React, { useRef, useState } from "react";
import { View } from "react-native";
import { WebView } from "react-native-webview";
const TokenizerComponent = () => {
  const webViewRef = useRef(null);
  const [tokenData, setTokenData] = useState(null);
  const handleWebViewMessage = (event) => {
    try {
      const messageData = JSON.parse(event.nativeEvent.data);
      if (messageData.type === "DAILYPAY_CARD_TOKENIZE_RESPONSE") {
        if (messageData.success && messageData.response) {
          setTokenData(messageData.response);
        }
      }
    } catch (error) {
      console.error("Failed to parse message:", error);
    }
  };
  return (
    <WebView
      ref={webViewRef}
      source={{
        // Replace client_id and implementation_id with values supplied by DailyPay
        // that are unique to you and your application
        uri: "https://companion.workloads.production.dailypay.com/v1/widgets/debitcard/tokenizer?client_id=123&implementation_id=3213",
      }}
      onMessage={handleWebViewMessage}
      javaScriptEnabled={true}
      domStorageEnabled={true}
    />
  );
};