Skip to main content
RapidDev - Software Development Agency
lovable-integrationsEdge Function Integration

How to Integrate Lovable with Authorize.net

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.

What you'll learn

  • How to load and configure Accept.js for PCI-compliant card tokenization
  • How to create a Supabase Edge Function to process Authorize.net charge requests
  • How to handle the Accept.js token dispatch and pass the nonce to your Edge Function
  • How to set up Authorize.net test credentials and use the sandbox environment
  • How to implement recurring billing using Authorize.net Customer Profiles
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate16 min read30 minutesPaymentMarch 2026RapidDev Engineering Team
TL;DR

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

Edge Function Integration

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

1

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.

2

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.

Lovable Prompt

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

supabase/functions/authorize-net-charge/index.ts
1import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
2
3const 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 production
6
7const corsHeaders = {
8 "Access-Control-Allow-Origin": "*",
9 "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
10};
11
12serve(async (req) => {
13 if (req.method === "OPTIONS") {
14 return new Response("ok", { headers: corsHeaders });
15 }
16
17 try {
18 const { dataDescriptor, dataValue, amount, orderId } = await req.json();
19
20 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 };
37
38 const res = await fetch(API_URL, {
39 method: "POST",
40 headers: { "Content-Type": "application/json" },
41 body: JSON.stringify(payload),
42 });
43
44 const data = await res.json();
45 // Strip BOM that Authorize.net adds to responses
46 const result = data.transactionResponse;
47
48 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.

3

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.

Lovable Prompt

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

src/components/AuthorizeNetForm.tsx
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";
6
7declare global {
8 interface Window {
9 Accept: {
10 dispatchData: (data: Record<string, unknown>, callback: (response: AcceptResponse) => void) => void;
11 };
12 }
13}
14
15interface AcceptResponse {
16 messages: { resultCode: string; message: Array<{ code: string; text: string }> };
17 opaqueData?: { dataDescriptor: string; dataValue: string };
18}
19
20// Client Key is safe for frontend — it cannot be used to charge cards alone
21const CLIENT_KEY = "YOUR_SANDBOX_CLIENT_KEY"; // replace with your Accept.js Client Key
22const API_LOGIN_ID = "YOUR_SANDBOX_LOGIN_ID"; // replace with your API Login ID (public OK for Accept.js)
23
24interface PaymentFormProps {
25 amount: number;
26 orderId: string;
27 onSuccess: (transactionId: string) => void;
28}
29
30export 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);
36
37 const handleSubmit = async (e: React.FormEvent) => {
38 e.preventDefault();
39 setLoading(true);
40
41 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 };
65
66 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.

4

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.

Lovable Prompt

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.

Lovable Prompt

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.

Lovable Prompt

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.

typescript
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.

typescript
1// Strip spaces from card number:
2const cleanCardNumber = cardNumber.replace(/\D/g, "");
3// Then use cleanCardNumber in cardData

Best 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

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.