Integrating Authorize.net with Lovable uses Edge Functions to create payment transactions via the Accept.js tokenization flow. Store your API Login ID and Transaction Key in Cloud Secrets, use Authorize.net's Accept.js to tokenize card data in the browser, then send the payment nonce to an Edge Function that calls the Authorize.net API to charge the card. Setup takes 30 minutes.
Why integrate Authorize.net with Lovable?
Authorize.net has been processing payments since 1996 and is one of the most trusted payment gateways in the United States, particularly among small and mid-sized businesses, brick-and-mortar retailers, and industries with established banking relationships that prefer non-Stripe providers. Owned by Visa since 2010, Authorize.net processes over $149 billion in payments annually and is widely accepted by US merchant banks, making it a common requirement when building apps for clients in retail, healthcare, legal, and nonprofit sectors.
For Lovable developers, Authorize.net is particularly relevant when building for existing businesses that already have Authorize.net merchant accounts, when operating in regulated industries where the customer's bank or ISO (Independent Sales Organization) has a preferred gateway agreement, or when accepting ACH bank transfers as a lower-cost alternative to card payments. Authorize.net's Customer Information Manager (CIM) also offers a mature subscription and recurring billing system that predates Stripe Billing.
The integration pattern follows a two-step tokenization model: Accept.js (Authorize.net's JavaScript library) collects card data directly in the browser and exchanges it for a one-time-use payment nonce without that data passing through your server. Your Edge Function receives the nonce and calls the Authorize.net API to create the charge. This keeps your app out of PCI scope for card data handling.
Integration method
Authorize.net has no native Lovable connector. Integration uses the Accept.js tokenization library in the browser to capture card data without it ever reaching your server, then sends a payment nonce to a Supabase Edge Function that calls the Authorize.net API with your API Login ID and Transaction Key to complete the charge. Credentials are stored in Cloud Secrets and never exposed to the frontend.
Prerequisites
- A Lovable project with Cloud enabled
- An Authorize.net sandbox account — create free at developer.authorize.net
- Your sandbox API Login ID and Transaction Key from Account → Settings → API Credentials & Keys
- Your sandbox Client Key for Accept.js from Account → Settings → Manage Public Client Key
- Basic understanding that Authorize.net has separate sandbox and production environments
Step-by-step guide
Get Authorize.net sandbox credentials and add to Cloud Secrets
Get Authorize.net sandbox credentials and add to Cloud Secrets
Log in to your Authorize.net sandbox account at sandbox.authorize.net. Navigate to Account (top-right) → Settings → API Credentials & Keys. You'll see two credentials you need: the API Login ID (a short alphanumeric string, visible by default) and the Transaction Key. For the Transaction Key, you may need to click 'New Transaction Key' to generate one — note that generating a new key invalidates the previous one after 24 hours. You also need a Client Key for Accept.js. On the same Settings page, find 'Manage Public Client Key' and click it. If no key exists, generate one. The Client Key is safe to expose in your frontend code — it identifies your merchant account to Accept.js but cannot be used to make charges on its own. In your Lovable project, click the '+' button next to the Preview panel to open the Cloud tab, then click 'Secrets'. Add two secrets: AUTHORIZE_NET_LOGIN_ID with your API Login ID, and AUTHORIZE_NET_TRANSACTION_KEY with your Transaction Key. These are sensitive credentials that stay on the server side only. The Client Key goes in your React component code directly (it's designed to be public). For sandbox, the API endpoint is sandbox.api.authorize.net — for production it's api.authorize.net.
Pro tip: The API Login ID and Transaction Key are different from your Authorize.net account username and password. Find them specifically under Account → Settings → API Credentials & Keys.
Expected result: AUTHORIZE_NET_LOGIN_ID and AUTHORIZE_NET_TRANSACTION_KEY stored in Cloud Secrets. Client Key noted separately for use in frontend code.
Create an Edge Function to process Authorize.net charges
Create an Edge Function to process Authorize.net charges
In Lovable, open the Code panel, navigate to supabase/functions/, and create a new folder called authorize-net-charge with an index.ts file. This function receives a payment nonce (dataDescriptor and dataValue from Accept.js), the charge amount, and optional order metadata. It then calls the Authorize.net API to create a transaction. The Authorize.net API uses JSON with a specific nested structure. The endpoint is POST https://apitest.authorize.net/xml/v1/request.api (sandbox) or https://api.authorize.net/xml/v1/request.api (production). The request body wraps everything in a createTransactionRequest object with authentication credentials in merchantAuthentication and the payment details in transactionRequest. The transactionType for a standard card charge is 'authCaptureTransaction'. The payment section uses opaqueData with the dataDescriptor and dataValue from Accept.js. The API returns a JSON response with a transactionResponse containing responseCode ('1' = approved, '2' = declined, '3' = error) and a transId for approved transactions. Your Edge Function should check the response code, return the transaction ID on success, and return a descriptive error on failure by reading the errors array from the response.
Create a Supabase Edge Function at supabase/functions/authorize-net-charge/index.ts. Accept POST requests with JSON body containing dataDescriptor, dataValue (from Accept.js), amount (number with decimals like 19.99), and optional orderId. Call the Authorize.net sandbox API at https://apitest.authorize.net/xml/v1/request.api with a createTransactionRequest using merchantAuthentication from AUTHORIZE_NET_LOGIN_ID and AUTHORIZE_NET_TRANSACTION_KEY in Deno.env.get(). Set transactionType to 'authCaptureTransaction' and payment to opaqueData with the dataDescriptor and dataValue. Return the transId on success or the error description on failure. Include CORS headers.
Paste this in Lovable chat
1import { serve } from "https://deno.land/std@0.168.0/http/server.ts";23const LOGIN_ID = Deno.env.get("AUTHORIZE_NET_LOGIN_ID") ?? "";4const TRANSACTION_KEY = Deno.env.get("AUTHORIZE_NET_TRANSACTION_KEY") ?? "";5const API_URL = "https://apitest.authorize.net/xml/v1/request.api"; // change to api.authorize.net for production67const corsHeaders = {8 "Access-Control-Allow-Origin": "*",9 "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",10};1112serve(async (req) => {13 if (req.method === "OPTIONS") {14 return new Response("ok", { headers: corsHeaders });15 }1617 try {18 const { dataDescriptor, dataValue, amount, orderId } = await req.json();1920 const payload = {21 createTransactionRequest: {22 merchantAuthentication: {23 name: LOGIN_ID,24 transactionKey: TRANSACTION_KEY,25 },26 refId: orderId ?? "order-" + Date.now(),27 transactionRequest: {28 transactionType: "authCaptureTransaction",29 amount: String(amount),30 payment: {31 opaqueData: { dataDescriptor, dataValue },32 },33 order: { invoiceNumber: orderId ?? "", description: "Lovable App Purchase" },34 },35 },36 };3738 const res = await fetch(API_URL, {39 method: "POST",40 headers: { "Content-Type": "application/json" },41 body: JSON.stringify(payload),42 });4344 const data = await res.json();45 // Strip BOM that Authorize.net adds to responses46 const result = data.transactionResponse;4748 if (result?.responseCode === "1") {49 return new Response(50 JSON.stringify({ success: true, transactionId: result.transId }),51 { headers: { ...corsHeaders, "Content-Type": "application/json" } }52 );53 } else {54 const errorText = result?.errors?.[0]?.errorText ?? result?.messages?.message?.[0]?.text ?? "Transaction declined";55 return new Response(56 JSON.stringify({ success: false, error: errorText }),57 { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }58 );59 }60 } catch (e) {61 return new Response(62 JSON.stringify({ error: (e as Error).message }),63 { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }64 );65 }66});Pro tip: Authorize.net's JSON API sometimes prepends a UTF-8 BOM character to responses. If JSON.parse fails, try stripping the first character: response.text().then(t => JSON.parse(t.replace(/^\uFEFF/, ''))).
Expected result: Edge Function deployed and returning successful transaction IDs for test charges.
Add Accept.js card tokenization to your React checkout form
Add Accept.js card tokenization to your React checkout form
Accept.js is Authorize.net's JavaScript library that creates a secure payment form in your browser. When a user submits their card details, Accept.js sends the card data directly to Authorize.net's servers and returns a one-time payment nonce (a dataDescriptor and dataValue pair) that expires in 15 minutes. This nonce is what you send to your Edge Function — it represents the card without exposing the actual card number. Add the Accept.js script to your index.html. For sandbox, use https://jstest.authorize.net/v3/AcceptUI.js. For production, use https://js.authorize.net/v3/AcceptUI.js. The library adds an Accept object to window. Call Accept.dispatchData() with the API Login ID, Client Key, and card data (cardData object with cardNumber, month, year, cardCode). It returns a response via callback — check response.messages.resultCode. If 'Ok', the opaqueData contains your nonce. Create a React component with a controlled card form. On form submission, prevent the default, call Accept.dispatchData(), receive the callback, extract dataDescriptor and dataValue, and call your Edge Function with those values plus the charge amount. Keep the Client Key directly in the component — it's a public identifier designed for frontend use. After the Edge Function returns a transactionId, show a success state and update your Supabase orders table.
Create a checkout payment form component that uses Authorize.net's Accept.js. Add the Accept.js sandbox script to index.html. In the component, create a card form with inputs for card number, expiry month, expiry year, and CVV. On submit, call window.Accept.dispatchData() with the API Login ID (use the hardcoded sandbox login ID from .env or pass as a prop), Client Key, and card data. In the callback, if resultCode is 'Ok', send the opaqueData.dataDescriptor and opaqueData.dataValue along with the charge amount to the authorize-net-charge Edge Function. Show a success message with transaction ID on completion, or show the error message if declined.
Paste this in Lovable chat
1import { useState } from "react";2import { supabase } from "@/integrations/supabase/client";3import { Button } from "@/components/ui/button";4import { Input } from "@/components/ui/input";5import { toast } from "@/hooks/use-toast";67declare global {8 interface Window {9 Accept: {10 dispatchData: (data: Record<string, unknown>, callback: (response: AcceptResponse) => void) => void;11 };12 }13}1415interface AcceptResponse {16 messages: { resultCode: string; message: Array<{ code: string; text: string }> };17 opaqueData?: { dataDescriptor: string; dataValue: string };18}1920// Client Key is safe for frontend — it cannot be used to charge cards alone21const CLIENT_KEY = "YOUR_SANDBOX_CLIENT_KEY"; // replace with your Accept.js Client Key22const API_LOGIN_ID = "YOUR_SANDBOX_LOGIN_ID"; // replace with your API Login ID (public OK for Accept.js)2324interface PaymentFormProps {25 amount: number;26 orderId: string;27 onSuccess: (transactionId: string) => void;28}2930export function AuthorizeNetForm({ amount, orderId, onSuccess }: PaymentFormProps) {31 const [cardNumber, setCardNumber] = useState("");32 const [expMonth, setExpMonth] = useState("");33 const [expYear, setExpYear] = useState("");34 const [cvv, setCvv] = useState("");35 const [loading, setLoading] = useState(false);3637 const handleSubmit = async (e: React.FormEvent) => {38 e.preventDefault();39 setLoading(true);4041 window.Accept.dispatchData(42 {43 authData: { clientKey: CLIENT_KEY, apiLoginID: API_LOGIN_ID },44 cardData: { cardNumber, month: expMonth, year: expYear, cardCode: cvv },45 },46 async (response: AcceptResponse) => {47 if (response.messages.resultCode !== "Ok") {48 toast({ title: "Card error", description: response.messages.message[0].text, variant: "destructive" });49 setLoading(false);50 return;51 }52 const { dataDescriptor, dataValue } = response.opaqueData!;53 const { data, error } = await supabase.functions.invoke("authorize-net-charge", {54 body: { dataDescriptor, dataValue, amount, orderId },55 });56 setLoading(false);57 if (error || !data?.success) {58 toast({ title: "Payment failed", description: data?.error ?? error?.message, variant: "destructive" });59 } else {60 onSuccess(data.transactionId);61 }62 }63 );64 };6566 return (67 <form onSubmit={handleSubmit} className="space-y-4 max-w-sm">68 <Input placeholder="Card number" value={cardNumber} onChange={e => setCardNumber(e.target.value)} required />69 <div className="flex gap-2">70 <Input placeholder="MM" value={expMonth} onChange={e => setExpMonth(e.target.value)} className="w-20" required />71 <Input placeholder="YYYY" value={expYear} onChange={e => setExpYear(e.target.value)} className="w-24" required />72 <Input placeholder="CVV" value={cvv} onChange={e => setCvv(e.target.value)} className="w-20" required />73 </div>74 <Button type="submit" disabled={loading} className="w-full">75 {loading ? "Processing..." : `Pay $${amount.toFixed(2)}`}76 </Button>77 </form>78 );79}Pro tip: Replace YOUR_SANDBOX_CLIENT_KEY and YOUR_SANDBOX_LOGIN_ID with your actual sandbox values. The API Login ID in Accept.js is public — it only works for tokenization, not for making charges directly.
Expected result: Checkout form renders with card fields. Submitting with Authorize.net test card 4111 1111 1111 1111 triggers the Accept.js callback and calls the Edge Function successfully.
Test with Authorize.net test cards and verify transactions
Test with Authorize.net test cards and verify transactions
Authorize.net provides test card numbers that simulate different payment outcomes in the sandbox environment. The primary test card for successful payments is 4111 1111 1111 1111 (Visa) with any future expiry date and any CVV. For a declined transaction, use 4222 2222 2222 2222. For an error (gateway error simulation), use 5424 0000 0000 0015. Set the test amount to $1.00 or more — very small amounts like $0.01 may behave differently. After a successful test payment, log into your sandbox Authorize.net account and navigate to Reports → Transaction History to see the transaction. It should show as 'Settled (Pending)' for authCapture transactions. Copy the transaction ID and verify it matches what your Edge Function returned. Also check your Supabase database to confirm any order status updates your code makes are working correctly. For the Accept.js flow, check browser developer tools Network tab to confirm the call to Authorize.net's tokenization endpoint succeeds and returns opaqueData before your Edge Function is called. If Accept.js returns an error code E_WC_05 or E_WC_06, this typically means the Client Key or Login ID is wrong. Check the Authorize.net sandbox dashboard under Account → Settings → Manage Public Client Key to verify your Client Key hasn't been regenerated. For production deployment, remember to switch both the API endpoint URL in the Edge Function and the Accept.js script URL in index.html from sandbox to production.
Pro tip: In the Authorize.net sandbox, go to Reports → Transaction History to see all test transactions. Filter by date range and verify each test charge appears with the expected amounts and response codes.
Expected result: Test charges visible in Authorize.net sandbox transaction history with responseCode '1' (Approved). Edge Function returning correct transaction IDs. Order status updating in Supabase.
Common use cases
One-time payment for services or products
A local service business (e.g., a law firm, medical office, or contractor) uses their existing Authorize.net merchant account to accept payments on their Lovable-built client portal. Clients enter card details via Accept.js, the nonce goes to an Edge Function, and the charge is processed through their existing merchant banking relationship. The Edge Function stores the transaction ID in Supabase for invoicing records.
Add a payment page to the client portal. Load Accept.js from Authorize.net's CDN. Create a card form with fields for card number, expiry, and CVV. When submitted, call Accept.dispatchData() with the API Login ID to get a payment nonce. Send that nonce to an Edge Function that calls the Authorize.net createTransactionRequest API with the nonce and charge amount. On success, save the transactionId to the Supabase invoices table and show a confirmation page.
Copy this prompt to try it in Lovable
Recurring membership billing with Customer Profiles
A subscription-based membership platform stores customer payment profiles in Authorize.net's vault to enable recurring billing without storing card data. On initial signup, an Edge Function creates a Customer Profile with the payment nonce. Monthly, a scheduled function charges each active member using their stored profile ID and payment profile ID, updating subscription status in Supabase.
Build a membership subscription system using Authorize.net Customer Profiles. When a member signs up and enters payment, create an Edge Function that calls createCustomerProfileRequest with the payment nonce to store their payment method in Authorize.net's vault. Save the customerProfileId and customerPaymentProfileId to the Supabase users table. Create a second Edge Function for monthly billing that reads active members and charges each one using createTransactionRequest with the stored profile IDs instead of card data.
Copy this prompt to try it in Lovable
ACH bank payment for high-value B2B invoices
A B2B software platform accepts ACH bank transfers for annual enterprise invoices to avoid the 2.9% credit card fee on large amounts. The Edge Function calls the Authorize.net API with bank account details (routing + account number, which are less sensitive than card data) to initiate an eCheck transaction. ACH processing takes 3-5 business days and costs a flat $0.75 per transaction.
Add an ACH bank payment option for invoices over $1,000. Create a form with fields for bank routing number, account number, account type (checking/savings), and bank name. When submitted, send these directly to an Edge Function that calls the Authorize.net createTransactionRequest API with payment method type bankAccount, routing number, account number, and amount. On success, show a confirmation that the bank transfer will process in 3-5 business days and update the invoice status to 'pending_ach'.
Copy this prompt to try it in Lovable
Troubleshooting
Accept.js returns error 'E_WC_05: Please include authentication information' or 'E_WC_21'
Cause: The API Login ID or Client Key passed to Accept.dispatchData() is incorrect or doesn't match the sandbox vs production environment. E_WC_05 means the Client Key is invalid. E_WC_21 means you're using sandbox Accept.js with a production Client Key or vice versa.
Solution: Verify that the Accept.js script URL in index.html matches your environment: jstest.authorize.net for sandbox, js.authorize.net for production. In your Authorize.net account, navigate to Account → Settings → Manage Public Client Key to confirm the Client Key is active and matches what's in your component. Both the script URL and the Client Key must be from the same environment.
Edge Function returns '403 Forbidden' or 'User authentication failed'
Cause: The AUTHORIZE_NET_LOGIN_ID or AUTHORIZE_NET_TRANSACTION_KEY in Cloud Secrets is incorrect. The Transaction Key may have been regenerated in Authorize.net (the old key deactivates after 24 hours), or there may be a typo in the secret values.
Solution: In your Authorize.net sandbox account, go to Account → Settings → API Credentials & Keys. If you need to verify the Transaction Key, note that Authorize.net only shows the key once when generated — generate a new one if you've lost the previous value. Update AUTHORIZE_NET_TRANSACTION_KEY in Cloud → Secrets with the new value. The new key takes effect immediately.
JSON.parse error on Authorize.net API response — 'Unexpected token' at position 0
Cause: Authorize.net prepends a UTF-8 BOM (byte order mark) character to their API responses. This is a known Authorize.net quirk that breaks standard JSON.parse.
Solution: Read the response as text first, strip the BOM, then parse it.
1// Instead of: const data = await res.json();2const text = await res.text();3const data = JSON.parse(text.replace(/^\uFEFF/, ""));Payment shows declined with error 'The credit card number is invalid' even using test cards
Cause: The card number is being passed with spaces or dashes, or the Accept.js opaqueData is not being extracted correctly before sending to the Edge Function.
Solution: Strip all non-digit characters from the card number input before passing to Accept.js. Also verify you're extracting response.opaqueData.dataDescriptor and response.opaqueData.dataValue (not the entire opaqueData object). Log the response object from Accept.js in the browser console to confirm the structure.
1// Strip spaces from card number:2const cleanCardNumber = cardNumber.replace(/\D/g, "");3// Then use cleanCardNumber in cardDataBest practices
- Store only AUTHORIZE_NET_LOGIN_ID and AUTHORIZE_NET_TRANSACTION_KEY in Cloud Secrets — the Client Key is intentionally public and can be safely included in your React component code
- Use Accept.js for all card data collection rather than collecting card numbers yourself — this keeps your app out of PCI DSS scope and protects you from liability for card data breaches
- Always check the responseCode in the Authorize.net response: '1' = Approved, '2' = Declined (tell user to try a different card), '3' = Error (system issue, try again later) — these have very different user-facing messages
- Store the Authorize.net transactionId for every successful charge in Supabase alongside the order record — you need this for refunds, disputes, and customer service
- Implement idempotency in your Edge Function by checking if an order already has a transactionId before calling Authorize.net — this prevents double charges if the user clicks the button twice
- Switch both the API endpoint URL and the Accept.js CDN URL when going to production — sandbox and production use completely different hostnames
- For subscription billing, store customerProfileId and customerPaymentProfileId from Authorize.net CIM rather than re-collecting card data each billing cycle — this enables seamless recurring charges
Alternatives
Stripe has a significantly better developer experience and more modern API design; Authorize.net is preferred when your client already has an Authorize.net merchant account or requires a Visa-owned gateway for compliance reasons.
Braintree offers native PayPal and Venmo checkout and a more modern API than Authorize.net, making it a better choice for consumer apps where PayPal is a popular payment option.
Adyen is better for enterprise global payments with 250+ payment methods; Authorize.net is more appropriate for US-focused small and mid-size businesses with simpler payment needs.
Frequently asked questions
Do I need an Authorize.net account to test the integration?
Yes, but you can create a free sandbox account at developer.authorize.net without a merchant bank account or approval process. The sandbox is fully functional for testing all features. You only need a paid production account when you're ready to accept real payments, which requires a merchant bank account and approval from Authorize.net.
Is Accept.js the only way to collect card payments with Authorize.net?
Accept.js is the recommended approach for web apps because it keeps card data off your server. Alternatives include Authorize.net's Hosted Payment Page (redirects to Authorize.net's page, similar to Stripe Checkout) and Accept Hosted (embedded iframe). For server-side processing where you control card collection (requires PCI compliance), you can send card data directly in the API request, but this is not recommended for most Lovable apps.
How does Authorize.net pricing compare to Stripe?
Authorize.net charges a $25/month gateway fee plus 2.9% + $0.30 per transaction for their all-in-one plan. Stripe charges no monthly fee with 2.9% + $0.30 per transaction. For very high transaction volumes, Authorize.net can be negotiated to lower per-transaction rates. The monthly fee makes Authorize.net more expensive for low-volume merchants but potentially cheaper for high-volume ones with negotiated rates.
Can Authorize.net handle subscriptions without building my own scheduler?
Yes — Authorize.net has Automated Recurring Billing (ARB) which lets you define a subscription schedule with start date, interval, duration, and amount. The API creates a subscriptionId and Authorize.net automatically charges the card on schedule. This is simpler than building your own scheduler, though less flexible than Stripe Billing for complex subscription logic like tiered pricing or usage-based billing.
How do I handle refunds through Authorize.net?
To issue a refund, call the createTransactionRequest API with transactionType set to 'refundTransaction', provide the original transId, the last 4 digits of the card, and the refund amount. Refunds must be for an amount less than or equal to the original charge. Transactions can only be refunded after they settle (typically next business day). For same-day voids, use transactionType 'voidTransaction' with the original transId instead.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation