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

How to Integrate Lovable with Adyen

Integrating Adyen with Lovable uses Edge Functions to create payment sessions, handle Drop-in UI callbacks, and process webhooks for enterprise payment flows. Store your Adyen API key and merchant account in Cloud Secrets, create an Edge Function to create payment sessions and verify HMAC webhook signatures, then embed Adyen's Drop-in component in your React frontend. Setup takes 45 minutes.

What you'll learn

  • How to create Adyen payment sessions via a Supabase Edge Function
  • How to embed the Adyen Drop-in UI in a React + TypeScript component
  • How to verify Adyen HMAC webhook signatures for secure payment confirmation
  • How to handle the server-to-client-to-server session flow Adyen requires
  • How to store Adyen credentials safely in Lovable Cloud Secrets
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate18 min read45 minutesPaymentMarch 2026RapidDev Engineering Team
TL;DR

Integrating Adyen with Lovable uses Edge Functions to create payment sessions, handle Drop-in UI callbacks, and process webhooks for enterprise payment flows. Store your Adyen API key and merchant account in Cloud Secrets, create an Edge Function to create payment sessions and verify HMAC webhook signatures, then embed Adyen's Drop-in component in your React frontend. Setup takes 45 minutes.

Why integrate Adyen with Lovable?

Adyen is the payment backbone for some of the world's largest companies — Netflix, Spotify, Airbnb, and Uber all run their payments through Adyen. Unlike Stripe, which is designed for startups and developers, Adyen is built for enterprises that need unified payments across web, mobile, and physical point-of-sale terminals in a single platform. If your Lovable-built app is scaling to enterprise deals or serving markets where Stripe has limited coverage, Adyen gives you broader geographic reach with 250+ payment methods including local payment options in Asia, Latin America, and Africa.

The Adyen integration model is session-based: your Edge Function calls the Adyen Checkout API to create a session (a server-side payment object with the amount, currency, and return URL), the session token is returned to the browser, and the Adyen Drop-in JavaScript component handles the entire payment UI. The buyer interacts only with Adyen's hosted component — entering card details, completing 3DS authentication, choosing alternative payment methods like iDEAL or Boleto. Adyen then sends a webhook to your server confirming the result, which your Edge Function verifies with HMAC and stores in Supabase.

For Lovable developers, Adyen makes sense when your users are enterprise buyers who expect a polished, regulation-compliant checkout, when you need to support specific regional payment methods (Alipay, Pix, WeChat Pay) not available in Stripe, or when you need unified reporting across in-store and online channels. The Drop-in UI handles all PCI compliance automatically — no card numbers ever touch your server.

Integration method

Edge Function Integration

Adyen has no native Lovable connector. Integration requires Supabase Edge Functions to create payment sessions via Adyen's Checkout API, serve session tokens to the frontend Adyen Drop-in UI, and verify HMAC-signed webhook notifications when payment events occur. The API key and merchant account are stored in Cloud Secrets and never exposed to the browser.

Prerequisites

  • A Lovable project with Cloud enabled
  • An Adyen test account — register at ca-test.adyen.com/ca/ca/login.shtml
  • Adyen API key created in the Adyen Customer Area under Developers → API credentials
  • Your Adyen merchant account name (shown in Customer Area top-left dropdown)
  • An Adyen client key for frontend Drop-in (created alongside your API key under allowed origins)

Step-by-step guide

1

Create your Adyen API credentials and add to Cloud Secrets

Log into the Adyen Customer Area at ca-test.adyen.com (test) or ca-live.adyen.com (production). In the left sidebar, navigate to Developers → API credentials. Click 'Create new credential' and select 'Web service user' as the credential type. Give it a descriptive name like 'lovable-backend'. Under the Roles section, enable 'Checkout webservice role' and 'Merchant PAL webservice role'. Generate the API key by clicking 'Generate API key' — copy it immediately as it won't be shown again. Next, create a client key for the frontend Drop-in. In the same API credentials page, scroll to 'Client-side authentication' and add your Lovable app's domain to the allowed origins list. Click 'Generate client key' and copy it. Note your merchant account name from the top-left dropdown in Customer Area — it typically looks like 'YourCompanyECOM'. Now open your Lovable project, click the '+' icon next to the Preview panel to open the Cloud tab, then click 'Secrets'. Add three secrets: ADYEN_API_KEY with your server-side API key, ADYEN_MERCHANT_ACCOUNT with your merchant account name (e.g., 'YourCompanyECOM'), and ADYEN_CLIENT_KEY with the client key starting with 'test_' or 'live_'. These will be auto-injected into Edge Functions and never exposed to the browser.

Pro tip: The merchant account name is NOT the same as your company name. Find it in the top-left dropdown of Customer Area — it usually ends in 'ECOM' or 'POS'.

Expected result: Three secrets stored in Cloud Secrets: ADYEN_API_KEY, ADYEN_MERCHANT_ACCOUNT, ADYEN_CLIENT_KEY.

2

Create an Edge Function to generate Adyen payment sessions

In Lovable, open the Code panel, navigate to supabase/functions/, and create a new folder called adyen-checkout with an index.ts file. This Edge Function will create Adyen payment sessions when called from your frontend. The session-based flow is Adyen's recommended approach: your server creates a session object containing the payment details, returns the session ID and client key to the browser, and the browser's Drop-in component handles all subsequent steps — 3DS challenges, payment method selection, redirect flows. Your server never processes card data directly. The Adyen Checkout Sessions API endpoint is POST https://checkout-test.adyen.com/v71/sessions for test, or https://checkout-live.adyen.com/v71/sessions for production. The request body requires merchantAccount, amount (with value in minor units — e.g., 1999 for $19.99 — and currency), returnUrl (where Adyen redirects after payment), and a reference (your order ID). Adyen returns a sessionId and sessionData that you pass to the Drop-in component. Keep the Edge Function focused: receive the order details from your frontend, call the Adyen Sessions API, return the session token and your client key. The function should handle CORS headers so your browser can call it directly.

Lovable Prompt

Create a Supabase Edge Function at supabase/functions/adyen-checkout/index.ts. It should accept a POST request with JSON body containing amount (number in cents), currency (string, default 'USD'), reference (order ID string), and returnUrl (where to redirect after payment). Call the Adyen Checkout Sessions API at https://checkout-test.adyen.com/v71/sessions using the ADYEN_API_KEY and ADYEN_MERCHANT_ACCOUNT from Deno.env.get(). Convert the amount to Adyen minor units (multiply by 1 since it's already in cents). Return the sessionId, sessionData, and ADYEN_CLIENT_KEY from Deno.env.get() to the frontend. Include CORS headers.

Paste this in Lovable chat

supabase/functions/adyen-checkout/index.ts
1import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
2
3const ADYEN_API_KEY = Deno.env.get("ADYEN_API_KEY") ?? "";
4const ADYEN_MERCHANT_ACCOUNT = Deno.env.get("ADYEN_MERCHANT_ACCOUNT") ?? "";
5const ADYEN_CLIENT_KEY = Deno.env.get("ADYEN_CLIENT_KEY") ?? "";
6const ADYEN_BASE_URL = "https://checkout-test.adyen.com/v71";
7
8const corsHeaders = {
9 "Access-Control-Allow-Origin": "*",
10 "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
11};
12
13serve(async (req) => {
14 if (req.method === "OPTIONS") {
15 return new Response("ok", { headers: corsHeaders });
16 }
17
18 try {
19 const { amount, currency = "USD", reference, returnUrl } = await req.json();
20
21 const sessionRes = await fetch(`${ADYEN_BASE_URL}/sessions`, {
22 method: "POST",
23 headers: {
24 "X-API-Key": ADYEN_API_KEY,
25 "Content-Type": "application/json",
26 },
27 body: JSON.stringify({
28 merchantAccount: ADYEN_MERCHANT_ACCOUNT,
29 amount: { value: amount, currency },
30 reference,
31 returnUrl,
32 shopperLocale: "en-US",
33 }),
34 });
35
36 if (!sessionRes.ok) {
37 const err = await sessionRes.text();
38 throw new Error(`Adyen error: ${err}`);
39 }
40
41 const session = await sessionRes.json();
42
43 return new Response(
44 JSON.stringify({
45 sessionId: session.id,
46 sessionData: session.sessionData,
47 clientKey: ADYEN_CLIENT_KEY,
48 }),
49 { headers: { ...corsHeaders, "Content-Type": "application/json" } }
50 );
51 } catch (e) {
52 return new Response(
53 JSON.stringify({ error: (e as Error).message }),
54 { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
55 );
56 }
57});

Pro tip: For production, change ADYEN_BASE_URL to https://checkout-live.adyen.com/v71 and update the allowed origins in Customer Area to include your live domain.

Expected result: The Edge Function deployed and accessible at your Supabase functions URL, returning Adyen session tokens when called.

3

Create an Edge Function for Adyen webhook verification

Adyen sends webhook notifications when payment events occur — authorization success, failure, refund, chargeback. You need a second Edge Function to receive these webhooks and verify their authenticity before updating your database. Adyen webhooks use HMAC-SHA256 signatures, and Adyen strongly recommends enabling Basic Auth on the webhook endpoint as an additional layer of protection. In the Adyen Customer Area, navigate to Developers → Webhooks and click 'Add new webhook'. Select 'Standard notification' as the webhook type. Under Server Configuration, enter your Supabase Edge Function URL (you'll get this after deploying). Enable 'HMAC signature verification' and note the generated HMAC key — save this to Cloud Secrets as ADYEN_WEBHOOK_HMAC_KEY. Also set a Basic Auth username and password and add them to Cloud Secrets as ADYEN_WEBHOOK_USERNAME and ADYEN_WEBHOOK_PASSWORD. The webhook body from Adyen contains a notificationItems array, each with NotificationRequestItem objects. The critical fields are eventCode (AUTHORISATION, CANCELLATION, REFUND, CHARGEBACK), success (true/false), pspReference (Adyen's unique payment ID), and merchantReference (your order ID). After HMAC verification, check eventCode === 'AUTHORISATION' && success === 'true' to confirm a payment was captured.

Lovable Prompt

Create a Supabase Edge Function at supabase/functions/adyen-webhook/index.ts. Accept POST requests with Adyen's standard notification format. First verify Basic Auth using ADYEN_WEBHOOK_USERNAME and ADYEN_WEBHOOK_PASSWORD from Deno.env.get(). Then verify the HMAC signature of each notification item using ADYEN_WEBHOOK_HMAC_KEY. For each notification where eventCode is 'AUTHORISATION' and success is 'true', update the corresponding order in Supabase (matching on merchantReference) to status 'paid' using the Supabase service role key. Return '[accepted]' as the response body (Adyen requires this exact string).

Paste this in Lovable chat

supabase/functions/adyen-webhook/index.ts
1import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
2import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
3
4const SUPABASE_URL = Deno.env.get("SUPABASE_URL") ?? "";
5const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "";
6const ADYEN_WEBHOOK_HMAC_KEY = Deno.env.get("ADYEN_WEBHOOK_HMAC_KEY") ?? "";
7const ADYEN_WEBHOOK_USERNAME = Deno.env.get("ADYEN_WEBHOOK_USERNAME") ?? "";
8const ADYEN_WEBHOOK_PASSWORD = Deno.env.get("ADYEN_WEBHOOK_PASSWORD") ?? "";
9
10async function verifyHMAC(item: Record<string, unknown>, hmacKey: string): Promise<boolean> {
11 const data = item.NotificationRequestItem as Record<string, unknown>;
12 const fields = [
13 data.pspReference, data.originalReference ?? "",
14 data.merchantAccountCode, data.merchantReference,
15 (data.amount as Record<string, unknown>)?.value, (data.amount as Record<string, unknown>)?.currency,
16 data.eventCode, data.success,
17 ];
18 const signingString = fields.join(":");
19 const keyBytes = new Uint8Array(hmacKey.match(/.{1,2}/g)!.map(b => parseInt(b, 16)));
20 const key = await crypto.subtle.importKey("raw", keyBytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
21 const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(signingString));
22 const expected = btoa(String.fromCharCode(...new Uint8Array(sig)));
23 return expected === (data.additionalData as Record<string, string>)?.hmacSignature;
24}
25
26serve(async (req) => {
27 const authHeader = req.headers.get("Authorization") ?? "";
28 const expectedAuth = "Basic " + btoa(`${ADYEN_WEBHOOK_USERNAME}:${ADYEN_WEBHOOK_PASSWORD}`);
29 if (authHeader !== expectedAuth) {
30 return new Response("Unauthorized", { status: 401 });
31 }
32
33 const body = await req.json();
34 const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);
35
36 for (const item of body.notificationItems ?? []) {
37 const valid = await verifyHMAC(item, ADYEN_WEBHOOK_HMAC_KEY);
38 if (!valid) continue;
39 const data = item.NotificationRequestItem;
40 if (data.eventCode === "AUTHORISATION" && data.success === "true") {
41 await supabase.from("orders").update({ status: "paid", adyen_psp_reference: data.pspReference })
42 .eq("id", data.merchantReference);
43 }
44 }
45
46 return new Response("[accepted]", { status: 200 });
47});

Pro tip: Adyen retries webhooks for 24 hours if it doesn't receive '[accepted]' back. Always return that exact string even if processing fails — log errors separately and process idempotently.

Expected result: The webhook Edge Function deployed and URL configured in Adyen Customer Area. Test notifications from Customer Area → Webhooks → Test should return 200 '[accepted]'.

4

Embed the Adyen Drop-in UI component in your React frontend

The Adyen Drop-in is a JavaScript UI component that renders payment method selection and card input forms. You need to load the Adyen Web library, initialize it with your session data, and mount it to a DOM element. Add the Adyen CDN script and stylesheet to your index.html. In your Lovable project, open the Code panel, find public/index.html and add two tags to the head: the Adyen stylesheet link and the Adyen JS script. Then create a React component that calls your Edge Function to get a session, initializes AdyenCheckout with the session data, and mounts it. The Drop-in handles the entire UX: it shows available payment methods for the shopper's location, collects card details with a secure hosted field, handles 3DS2 redirects and challenges, and calls your returnUrl when complete. Your React component only needs to call the Edge Function, pass the session data to AdyenCheckout, and handle the final redirect result. The returnUrl should be a page in your app that reads the redirectResult URL parameter and calls your Edge Function to verify the payment outcome. For the component, call your adyen-checkout Edge Function with the order details (amount in cents, currency, orderId), receive back sessionId, sessionData, and clientKey, then create an AdyenCheckout instance with environment set to 'test' and mount the dropin to a ref element. This component replaces traditional payment form handling entirely — Adyen's Drop-in is PCI DSS Level 1 certified.

Lovable Prompt

Add Adyen Drop-in to the checkout page. First add the Adyen Web CSS and JS CDN links to index.html head. Create a CheckoutForm component that: 1) on mount calls the adyen-checkout Edge Function with the cart total in cents, currency 'USD', and a unique orderId, 2) initializes window.AdyenCheckout with the returned sessionId, sessionData, clientKey, and environment 'test', 3) mounts the dropin component to a div#adyen-dropin element, 4) configures onPaymentCompleted to show a success message and redirect to /order-confirmation, 5) configures onError to show an error toast. Add Tailwind styling for a centered checkout card.

Paste this in Lovable chat

src/components/AdyenCheckoutForm.tsx
1import { useEffect, useRef } from "react";
2import { supabase } from "@/integrations/supabase/client";
3import { toast } from "@/hooks/use-toast";
4
5declare global {
6 interface Window { AdyenCheckout: (config: Record<string, unknown>) => Promise<Record<string, unknown>>; }
7}
8
9interface AdyenCheckoutProps {
10 amountInCents: number;
11 currency?: string;
12 orderId: string;
13 onSuccess: () => void;
14}
15
16export function AdyenCheckoutForm({ amountInCents, currency = "USD", orderId, onSuccess }: AdyenCheckoutProps) {
17 const dropinRef = useRef<HTMLDivElement>(null);
18
19 useEffect(() => {
20 let dropin: Record<string, unknown> | null = null;
21 async function initAdyen() {
22 try {
23 const { data, error } = await supabase.functions.invoke("adyen-checkout", {
24 body: {
25 amount: amountInCents,
26 currency,
27 reference: orderId,
28 returnUrl: `${window.location.origin}/order-confirmation`,
29 },
30 });
31 if (error) throw error;
32
33 const checkout = await window.AdyenCheckout({
34 environment: "test",
35 clientKey: data.clientKey,
36 session: { id: data.sessionId, sessionData: data.sessionData },
37 onPaymentCompleted: (result: Record<string, unknown>) => {
38 if (result.resultCode === "Authorised") onSuccess();
39 else toast({ title: "Payment failed", description: `Result: ${result.resultCode}`, variant: "destructive" });
40 },
41 onError: (err: Error) => {
42 toast({ title: "Payment error", description: err.message, variant: "destructive" });
43 },
44 });
45
46 dropin = (checkout as Record<string, unknown & { create: (type: string) => Record<string, unknown & { mount: (el: HTMLElement) => void }> }>).create("dropin");
47 if (dropinRef.current) (dropin as Record<string, (el: HTMLElement) => void>).mount(dropinRef.current);
48 } catch (e) {
49 toast({ title: "Checkout error", description: (e as Error).message, variant: "destructive" });
50 }
51 }
52 initAdyen();
53 return () => {
54 if (dropin) (dropin as Record<string, () => void>).unmount?.();
55 };
56 }, [amountInCents, currency, orderId, onSuccess]);
57
58 return (
59 <div className="max-w-md mx-auto p-6 bg-white rounded-xl shadow">
60 <h2 className="text-xl font-semibold mb-4">Complete Payment</h2>
61 <div ref={dropinRef} id="adyen-dropin" />
62 </div>
63 );
64}

Pro tip: Add the Adyen CDN to index.html: <link rel='stylesheet' href='https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/5.63.0/adyen.css'> and <script src='https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/5.63.0/adyen.js'></script>. Update version numbers to the latest in the Adyen docs.

Expected result: A working checkout page with the Adyen Drop-in UI showing payment method cards. In test mode, use Adyen test card 4111 1111 1111 1111 with any future expiry and CVC to complete a test payment.

5

Test the complete payment flow end-to-end

Before going live, you need to thoroughly test every payment scenario using Adyen's test cards and test payment methods. Adyen provides a comprehensive test card library at docs.adyen.com/development-resources/testing/test-card-numbers. The most important test cases are: successful card payment (card 4111 1111 1111 1111), declined payment (card 4000 0000 0000 0002), 3DS2 challenge required (card 4212 3456 7890 1237), and 3DS2 frictionless (card 4000 0000 0000 3220). For each test payment, verify the complete flow: your Edge Function creates a session, the Drop-in renders with the session data, the test payment completes, Adyen sends a webhook to your adyen-webhook Edge Function, your function verifies the HMAC signature and updates the order in Supabase. Check the Supabase Table Editor to confirm the order status changed to 'paid' and the adyen_psp_reference was saved. Check your Edge Function logs in the Cloud tab → Logs for any errors. Test the webhook delivery in Adyen Customer Area by navigating to Developers → Webhooks, finding your webhook config, and using the 'Test' button to send a sample notification. This sends a real HMAC-signed test event to your function even without an actual payment. Also verify that invalid HMAC signatures are rejected by sending a tampered test. For complex production setups, RapidDev's team can help configure additional Adyen features like split payments for marketplaces or subscription recurring charges.

Pro tip: Enable webhook event logging in the Cloud tab → Logs by searching for your adyen-webhook function name. Each webhook invocation should log '[accepted]' on success.

Expected result: End-to-end test passing: test payment with Adyen test card goes through, webhook received by Edge Function, order updated to 'paid' in Supabase database.

Common use cases

Enterprise SaaS checkout with multiple payment methods

A B2B SaaS platform needs to accept credit cards, SEPA Direct Debit, and iDEAL for European customers. The Edge Function creates an Adyen session with the subscription amount, the frontend Drop-in automatically shows the relevant payment methods for the customer's country, and Adyen handles 3DS2 authentication. The webhook confirms payment and provisions the subscription in Supabase.

Lovable Prompt

Add an enterprise checkout flow using Adyen. When a user selects a paid plan, create an Edge Function that calls the Adyen Checkout API to create a session with the plan price in their currency. Return the session ID and client key to the frontend. Embed the Adyen Drop-in UI component which shows available payment methods for their location. After payment, Adyen sends a webhook to another Edge Function — verify the HMAC signature and if the result code is 'Authorised', update the user's subscription status in Supabase.

Copy this prompt to try it in Lovable

Global marketplace payouts with Adyen for Platforms

A freelance marketplace uses Adyen for Platforms to split payments between the platform and service providers. When a client pays for a project, the Edge Function creates a split payment that routes a percentage to the freelancer's Adyen account and the remainder to the platform. Freelancers can withdraw to their local bank using Adyen's built-in payout rails.

Lovable Prompt

Build a marketplace payment flow with Adyen. Create an Edge Function that, when a project is marked complete and the client pays, creates an Adyen session with payment splits configured — 85% to the freelancer's Adyen account ID (stored in their Supabase profile) and 15% to the platform merchant account. Add a payout dashboard Edge Function that calls the Adyen Transfers API to initiate bank transfers for freelancers who have earnings above their threshold.

Copy this prompt to try it in Lovable

Subscription billing with automatic retries

A subscription product needs to handle failed payments and automatic retries without user intervention. The Edge Function creates Adyen recurring payment tokens during initial checkout, stores the recurring detail reference in Supabase, and uses that token to charge returning subscribers without requiring them to re-enter payment details. Failed payments trigger Adyen's built-in intelligent retry logic.

Lovable Prompt

Implement subscription billing with Adyen. During first payment, create an Adyen session with shopperInteraction set to Ecommerce and storePaymentMethod true. Store the returned recurringDetailReference in the Supabase users table. For monthly renewals, create an Edge Function that runs on schedule to charge subscribers using the stored recurringDetailReference via the Adyen Payments API with shopperInteraction set to ContAuth. Webhook confirms charges and updates next_billing_date in Supabase.

Copy this prompt to try it in Lovable

Troubleshooting

Adyen Drop-in shows 'window.AdyenCheckout is not a function' or blank checkout container

Cause: The Adyen Web JavaScript SDK hasn't loaded before the React component tries to initialize it. This happens when the CDN script tag is missing from index.html, or when AdyenCheckout is called before the DOM-loaded script has executed.

Solution: Open Code panel, find public/index.html, and add the Adyen script tag to the <head> section — not at the bottom of body. Verify the version in the CDN URL matches the latest Adyen Web release. In your React component, check that window.AdyenCheckout exists before calling it: if (!window.AdyenCheckout) { toast({ title: 'Checkout unavailable', ... }); return; }

typescript
1// In index.html <head>:
2// <link rel="stylesheet" href="https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/5.63.0/adyen.css">
3// <script src="https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/5.63.0/adyen.js"></script>
4
5// Guard in component:
6if (typeof window.AdyenCheckout === 'undefined') {
7 toast({ title: 'Payment system loading...', description: 'Please refresh the page' });
8 return;
9}

Edge Function returns 'Adyen error: 403 Forbidden' or '401 Unauthorized'

Cause: The ADYEN_API_KEY is incorrect, the API credential doesn't have the Checkout webservice role enabled, or you're using a live API key against the test endpoint (or vice versa).

Solution: In Adyen Customer Area, go to Developers → API credentials, find your credential, and verify: 1) The API key hasn't been regenerated since you added it to Secrets (if it was regenerated, add the new key), 2) 'Checkout webservice role' is checked under Roles, 3) You're using ca-test.adyen.com keys with the test Checkout endpoint (checkoutshopper-test). Open Cloud → Secrets and update ADYEN_API_KEY if needed.

Webhooks arriving but HMAC verification failing — payment not updating in Supabase

Cause: The HMAC key stored in ADYEN_WEBHOOK_HMAC_KEY doesn't match what Adyen uses to sign the webhook. This happens when the webhook was reconfigured in Customer Area and a new HMAC key was generated, or when the key was copied with extra whitespace.

Solution: In Adyen Customer Area → Developers → Webhooks, click your webhook configuration and find the HMAC key. It's shown as a hex string. Copy it carefully — no spaces, no newlines. Update ADYEN_WEBHOOK_HMAC_KEY in Cloud → Secrets. Use the 'Test' button in Customer Area to send a test notification and check your Edge Function logs for the HMAC comparison output.

Payment shows 'Authorised' in Drop-in but webhook says 'Refused' or never arrives

Cause: The Drop-in shows the result from the session response (which can indicate 'Authorised' for the authorization step), but the actual capture or settlement can fail. Also, webhooks require your Supabase Edge Function URL to be publicly accessible.

Solution: Check that your webhook URL in Adyen Customer Area is the correct deployed Supabase function URL (not a local URL). In Cloud tab, verify the adyen-webhook function is deployed. Test webhooks manually using Customer Area → Webhooks → Test button. For authorization vs capture: Adyen separates Authorization (card approved) from Capture (funds collected). If you need immediate capture, set captureDelayHours: 0 in your session creation request.

Best practices

  • Store ADYEN_API_KEY, ADYEN_MERCHANT_ACCOUNT, ADYEN_CLIENT_KEY, ADYEN_WEBHOOK_HMAC_KEY, ADYEN_WEBHOOK_USERNAME, and ADYEN_WEBHOOK_PASSWORD all in Cloud Secrets — never hardcode any of them in frontend or Edge Function source code
  • Use Adyen's session-based integration (not the older payment methods + details flow) for new projects — sessions are stateless and simpler to implement correctly
  • Always verify the HMAC signature on every webhook notification before trusting the event — a missing or invalid signature should result in a 401 response and no database update
  • Return '[accepted]' to Adyen within 10 seconds even if your processing fails — queue the event for async processing to avoid Adyen flooding your endpoint with retries for 24 hours
  • Set up separate Adyen merchant accounts for test and production environments — use ADYEN_ENVIRONMENT variable in your Edge Function to switch between test/live Checkout endpoints
  • Enable Adyen's Additional Data fields (additionalData.fraudResultType, additionalData.avsResultRaw) in your webhook to get fraud signals and use them to flag suspicious orders before shipping
  • Use Adyen's idempotency keys by adding 'Idempotency-Key': orderId to your session creation request — this prevents duplicate charges if your Edge Function is called twice for the same order

Alternatives

Frequently asked questions

Does Adyen work for small businesses or only enterprises?

Adyen has historically required a minimum transaction volume commitment (often $100K+ annually) for direct merchant accounts. Small businesses are typically better served by Stripe or Braintree which have no minimums. However, Adyen for Platforms allows smaller businesses to access Adyen through a marketplace or platform provider. Check Adyen's current account requirements before assuming you qualify.

What's the difference between Adyen test mode and live mode?

Adyen has completely separate test and live environments with different API endpoints, API keys, and Customer Area portals. Test uses ca-test.adyen.com and checkout-test.adyen.com. Live uses ca-live.adyen.com and a region-specific endpoint like checkout-live.adyen.com or a custom live endpoint assigned during merchant activation. You need to change both your API key and the base URL when going live — update both in Cloud Secrets.

Can Adyen handle subscriptions and recurring billing?

Yes. Adyen supports recurring payments by tokenizing the payment method during first checkout (set storePaymentMethod: true in the session) and returning a recurringDetailReference. Use this token for subsequent charges via the Payments API with shopperInteraction: 'ContAuth'. This is different from Stripe Billing — Adyen doesn't have a built-in subscription scheduler, so you'll need to implement the renewal logic yourself in a scheduled Edge Function.

Is the Adyen Drop-in PCI compliant?

Yes. The Adyen Drop-in UI uses iframes and hosted fields to collect card data directly on Adyen's servers — card numbers never touch your Lovable app or Edge Functions. This makes your integration PCI DSS SAQ A compliant (the lowest compliance tier), meaning you don't need to undergo a full PCI audit. Adyen is a PCI DSS Level 1 certified payment processor.

How do I switch from Adyen test to production?

To go live with Adyen, complete their KYC process in Customer Area, get your live merchant account approved, generate live API credentials, and update Cloud Secrets with live values. In your Edge Function, change the Adyen base URL from https://checkout-test.adyen.com/v71 to your live endpoint (Adyen provides a region-specific live endpoint URL). Update the Drop-in environment from 'test' to 'live' in your React component. Update webhook URLs in Customer Area to point to your production domain.

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.