# Lynq.js Source: https://docs.lynq.am/build/checkout-sdk Embed Lynq hosted Checkout in a browser page and handle its events. `@lynqpay/js` embeds the Lynq-hosted payment form in your page. Your application controls where Checkout appears and how the page around it responds; Lynq controls the payment form, authentication, redirects, and its height. ## Install ```bash theme={null} npm install @lynqpay/js ``` The package is ESM-only and includes TypeScript declarations. ### Script-tag installation Without a bundler, load the IIFE bundle with a classic script tag. It exposes `window.Lynq`. ```html theme={null}
``` Pin the exact version, never `@latest`, so a new release cannot change your production checkout. Keep the `integrity` attribute: the browser refuses to run the file if the CDN serves anything other than the published release. Each version has its own hash, listed in the [release notes](/resources/release-notes). ## Mount Checkout Create one instance after your server returns a Payment Intent client secret. Register handlers before `mount()` so you cannot miss an early event. ```ts theme={null} import { Checkout } from "@lynqpay/js"; const checkout = new Checkout({ publicKey: "pk_test_...", clientSecret: paymentIntent.clientSecret, }); checkout .onReady(() => hideCheckoutSkeleton()) .onPaymentResult((result) => { if (result.status === "succeeded") { showSubmitted(result.paymentIntentId); return; } if (result.status === "processing") { showPending(result.paymentIntentId); return; } showPaymentError(result.error.message); }) .onCheckoutError((error) => { showCheckoutError(error.message, { canRetry: error.retryable }); }) .mount("#lynq-checkout"); ``` The mount target is a CSS selector or an `HTMLElement`. Checkout inserts an iframe and resizes it as its content changes, so do not set its height yourself. ## Handle events | Handler | When it fires | Use it to | | - | - | - | | `onReady` | The form is visible and accepts input. | Remove your loading placeholder. | | `onValidation` | The card form's state changes. | React to whether the form can submit, or to the detected card brand. | | `onPaymentResult` | A payment attempt finished. | Update the customer-facing page. | | `onCheckoutError` | Checkout failed to load or initialize. | Show the message and offer a retry when `error.retryable` is true. | ### Payment results | Result | Show the customer | On your server | | - | - | - | | `succeeded` | Payment was submitted successfully. | Wait for the verified webhook before fulfilment. | | `processing` | A pending state; they do not need to pay again. | Leave the order pending and wait for the webhook. | | `requires_payment_method` | `result.error.message`, and let them correct or replace the method. | Do not mark the order paid. | A declined payment is a result, not a Checkout error. Authentication challenges and wallet redirects happen inside Checkout and never appear as results. See the [Lynq.js reference](/reference/checkout-sdk) for exact payloads and [Errors](/reference/errors) for each error code. ## Customize the appearance Match Checkout to your site with `appearance`. Every field is optional; unset fields keep the theme's own value. ```ts theme={null} const checkout = new Checkout({ publicKey: "pk_test_...", clientSecret: paymentIntent.clientSecret, appearance: { theme: "dark", labels: "floating", colorPrimary: "#c6f432", borderRadius: "6px", }, }); ``` | Field | Default | Description | | - | - | - | | `theme` | `light` | `light` or `dark` base palette. The colours below apply on top of it. | | `labels` | `above` | `above` keeps labels above the fields; `floating` shows them inside the fields and moves them up as the customer types. | | `colorPrimary` | Theme | Pay button, focus rings, and the selected payment method. The button text switches between black and white to stay readable. | | `colorBackground` | Theme | Background of the fields and the payment-method list. | | `colorText` | Theme | Text and labels. Secondary text is derived from it. | | `colorBorder` | Theme | Borders around and between fields. | | `colorDanger` | Theme | Error messages and invalid fields. | | `borderRadius` | `8px` | Corner radius in whole pixels, from `0px` to `99px`. | Colours must be hex values such as `#2563eb` or `#26e`. An invalid or unknown entry never stops Checkout: it is ignored and the theme's own value applies. With a `pk_test_…` key the SDK logs a console warning saying why. ## Clean up Only one Checkout may be mounted on a page at once. Call `destroy()` when the customer leaves the payment step, and create a new instance for the next Payment Intent. `unmount()` removes Checkout but keeps the instance reusable. ## Server-rendered applications The package can be imported during server rendering, but `Checkout` can only be constructed in a browser; anywhere else it throws `ConfigError`. In React, construct it inside an effect and destroy it in the cleanup function: ```tsx theme={null} useEffect(() => { const checkout = new Checkout({ publicKey, clientSecret }); checkout.onPaymentResult(onPaymentResult).mount(containerRef.current!); return () => checkout.destroy(); }, [publicKey, clientSecret, onPaymentResult]); ``` Keep `clientSecret` stable for the lifetime of that component. # Payment Intents Source: https://docs.lynq.am/build/payment-intents Create and retrieve payments from your server, with every request and response field. A Payment Intent represents one attempt to collect a specific amount for an order. Create it from your server when the customer starts paying, then pass its client secret to the browser so Checkout can collect the payment. All requests go to `https://api.lynq.am/api` as JSON over HTTPS, with your secret key as a bearer token. The key selects the merchant and the mode: a test key reaches only test Payment Intents. ## Create a Payment Intent `POST /payment-intents` ```bash theme={null} curl --request POST "https://api.lynq.am/api/payment-intents" \ --header "Authorization: Bearer sk_test_..." \ --header "Content-Type: application/json" \ --header "Idempotency-Key: order:order_123:attempt:1" \ --data '{ "amount": 2500000, "currency": "AMD", "orderId": "order_123", "customer": { "id": "customer_123", "email": "buyer@example.com", "name": "Aram Petrosyan" }, "metadata": { "cartId": "cart_456" } }' ``` ```json theme={null} { "id": "pi_...", "amount": 2500000, "currency": "AMD", "status": "requires_payment_method", "orderId": "order_123", "clientSecret": "pi_..._secret_...", "paymentMethod": null, "lastPaymentError": null, "metadata": { "cartId": "cart_456" } } ``` ### Request fields **amount** `number` *required* Positive integer in minor currency units. For AMD 25,000.00, send `2500000`. Calculate it on your server from the order; do not trust a total sent by the browser. **currency** `string` *required* `AMD`, `USD`, `EUR`, or `RUB`. Each uses two minor-unit digits. Input is normalized to uppercase. **orderId** `string` *required* Your order identifier, used to find the payment during reconciliation. Maximum 32 characters. **customer** `object` Optional customer details. `id` is required when the object is present; `email` and `name` are optional. **metadata** `object` Optional string-to-string values for your own systems, such as a cart or invoice identifier. Do not put secrets, card data, or sensitive personal information here. **Idempotency-Key** `string` Stable unique key for this create request. Recommended on every request. ### Idempotency A stable key such as `order:order_123:attempt:1` makes a retry return the original result instead of creating another Payment Intent. Reuse a key only when retrying the exact same request, for example after a timeout. Use a new key when the amount, currency, order, customer, or metadata changes, or when the customer starts a new attempt. ### The client secret `clientSecret` is returned once, when the Payment Intent is created. It authorizes Checkout for this one payment: * Return it only to the browser session paying the matching order, over HTTPS. * Do not store it in browser storage, analytics, error reports, logs, or URLs. * If the session is no longer usable, create a new Payment Intent. ## Retrieve a Payment Intent `GET /payment-intents/:id` ```bash theme={null} curl --request GET "https://api.lynq.am/api/payment-intents/pi_..." \ --header "Authorization: Bearer sk_test_..." ``` Use retrieval for reconciliation or server-side status checks. The response has the same fields without `clientSecret`, so it cannot rebuild a checkout session. ## Response fields **id** `string` *required* Lynq Payment Intent identifier. Store it with your order. **amount** `number` *required* Amount in minor currency units. **currency** `string` *required* Normalized currency code. **status** `string` *required* Current status. See [Statuses](#statuses). **orderId** `string` *required* Order identifier from the request. **clientSecret** `string` Checkout credential. Present only in the create response. **customer** `object` Customer data supplied at creation, or `null`. **paymentMethod** `object` Non-sensitive summary of the selected payment method, or `null` before selection. **lastPaymentError** `object` Latest display-safe payment failure, or `null`. **nextAction** `object` Authentication or redirect state used by hosted Checkout, or `null`. Do not drive this flow from your code. **processorPaymentId** `string` Provider payment identifier when available, or `null`. **metadata** `object` Your metadata, or `null`. **createdAt** `string` *required* Creation timestamp. **updatedAt** `string` *required* Latest update timestamp. ## Statuses | Status | What it means | What your application should do | | - | - | - | | `requires_payment_method` | Checkout is waiting for usable payment details. | Let the customer enter or replace a method. | | `requires_action` | Authentication or a wallet handoff is in progress. | Nothing: Lynq.js handles it. | | `processing` | The provider has not produced a final result. | Show pending and wait for the webhook. | | `succeeded` | Payment completed. | Fulfil after verifying the success webhook. | | `canceled` | The Payment Intent can no longer be completed. | Create a new Payment Intent if the customer tries again. | Use one Payment Intent per checkout attempt, and never reuse one that has succeeded or been canceled. > [!WARNING] > > Hosted Checkout has its own publishable-key endpoints for confirming payments. Do not call them directly; use [Lynq.js](/build/checkout-sdk). # Verify webhooks Source: https://docs.lynq.am/build/webhooks Verify signed Lynq webhook events and process them safely. Webhooks notify your server after a payment succeeds. They are the authoritative signal for fulfilment because they do not depend on the customer's browser staying open. ## Create an endpoint in Dashboard **1. Build a public HTTPS route** Add a `POST` route that can read the raw request body. The endpoint must be reachable from the public internet over HTTPS and must not include credentials in its URL. **2. Select the environment** Open **Dashboard → Webhooks** in Test mode or Live mode. Each environment has one independent endpoint and signing secret. **3. Connect the endpoint** Enter the complete HTTPS URL and choose **Connect**. Copy the signing secret immediately; it is displayed once. **4. Send a test event** In Test mode, choose **Send test event**. Confirm your route verifies the signature, records the event, and returns a successful response. Store the signing secret in the same kind of server-side secret storage used for your secret API key. Do not put it in browser code. ## Request contract Lynq sends: | Header | Use | | - | - | | `Content-Type` | `application/json` | | `User-Agent` | `Lynq/1.0 (+https://docs.lynq.am/build/webhooks)` | | `Lynq-Event-Id` | Stable delivery event ID. Use it to make handling idempotent. | | `Lynq-Signature` | `t=,v1=` | The signature is HMAC-SHA256 over `${timestamp}.${rawBody}`, using the endpoint signing secret. After you [rotate the signing secret](#rotate-a-signing-secret) with an overlap, the header carries one `v1` per active secret, newest first: `t=,v1=,v1=`. Accept the request when any `v1` matches. ## Event payload Lynq currently sends one event type, `payment_intent.succeeded`. ```json theme={null} { "id": "evt_...", "object": "event", "type": "payment_intent.succeeded", "createdAt": "2026-08-07T12:00:00.000Z", "environment": "live", "data": { "id": "pi_...", "object": "payment_intent", "amount": 2500000, "currency": "AMD", "status": "succeeded", "orderId": "order_123", "createdAt": "2026-08-07T11:59:00.000Z" } } ``` | Field | Description | | - | - | | `id` | Unique event identifier, also sent in the `Lynq-Event-Id` header. | | `type` | Event type. Record unknown types and return `2xx`; more may be added. | | `environment` | `test` or `live`. | | `data.id` | Payment Intent ID. | | `data.amount`, `data.currency` | Paid amount in minor units and its currency. | | `data.orderId` | The order identifier you supplied when creating the Payment Intent. | ## Verify the signature Verification must use the raw bytes or exact raw text received from Lynq. Parsing JSON and serializing it again can change whitespace or property formatting and invalidate the signature. ```ts server/verify-lynq-webhook.ts theme={null} import { createHmac, timingSafeEqual } from "node:crypto"; export function verifyLynqWebhook( rawBody: string, signatureHeader: string, secret: string, ) { let timestamp = Number.NaN; const signatures: string[] = []; for (const part of signatureHeader.split(",")) { const [key, value] = part.split("=", 2); if (key === "t") timestamp = Number(value); if (key === "v1" && value) signatures.push(value); } if (!Number.isSafeInteger(timestamp) || signatures.length === 0) return false; // Recommended replay protection: accept requests no older than five minutes. const ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - timestamp); if (ageSeconds > 300) return false; const expected = createHmac("sha256", secret) .update(`${timestamp}.${rawBody}`) .digest(); // During a rotation overlap there is one signature per active secret. return signatures.some((signature) => { const received = Buffer.from(signature, "hex"); return received.length === expected.length && timingSafeEqual(received, expected); }); } ``` Reject missing, malformed, stale, or mismatched signatures before processing the payload. > [!WARNING] > > Do not log the signing secret or include it in an error response. If it is exposed, rotate it in Dashboard with **Now** and update the receiver immediately. ## Process events exactly once Webhook delivery is at least once: a successful payment event may be delivered more than once if a response is lost or a retry overlaps your processing. Make the event ID unique in your database. ```ts server/handle-lynq-event.ts theme={null} async function handleLynqEvent(event: LynqWebhookEvent) { if (await webhookEvents.exists(event.id)) return; await database.transaction(async (transaction) => { await transaction.webhookEvents.insert({ id: event.id }); if (event.type === "payment_intent.succeeded") { await transaction.orders.markPaid({ orderId: event.data.orderId, paymentIntentId: event.data.id, amount: event.data.amount, currency: event.data.currency, }); } }); } ``` Before marking the order paid, compare the webhook's `orderId`, `amount`, and `currency` to your stored order. This protects against fulfilling the wrong order even when the event is validly signed. Return `2xx` only after durable processing succeeds. If processing fails temporarily, return `5xx` so Lynq retries. Avoid doing slow, unrelated work before acknowledging the event; enqueue downstream fulfilment after recording the event and payment state. ## Delivery and retries Lynq treats `2xx` as delivered. Network failures, timeouts, `408`, `429`, and `5xx` responses are retried with exponential backoff and jitter for up to 72 hours. Other `4xx` responses are treated as non-retryable. | Your response | Lynq behaviour | Use it when | | - | - | - | | `2xx` | Marks the delivery successful | The event is verified and durably recorded, including duplicates already processed. | | `400` | Does not retry | The request is permanently invalid. | | `401` or `403` | Does not retry | Signature verification failed. | | `429` | Retries | Your receiver is temporarily rate limited. | | `5xx` | Retries | Your database or processing path is temporarily unavailable. | The request timeout is 10 seconds. Keep the receiver path focused on verification and durable recording. Each delivery is listed in **Dashboard → Webhooks**, and the endpoint shows how many events failed in a row since the last success. After you fix the receiver, open a failed delivery and choose **Resend** to send the same event again, with the same event ID. Retries still stop 72 hours after the event was created, so a resent older event gets a single attempt. ## Rotate a signing secret When you rotate, choose how long Lynq keeps signing with the current secret as well: **Now**, **1 hour**, **24 hours**, or **7 days**. During that overlap every delivery carries a signature from both secrets, so the receiver keeps verifying while you deploy the new one. With **Now**, the current secret stops at once and deliveries fail verification until the receiver has the new secret. **1. Rotate in Dashboard** In **Webhooks**, choose **Rotate** next to the signing secret, pick an overlap, and copy the newly displayed secret. **2. Update and deploy** Replace the receiver secret and deploy it before the overlap ends. **3. Verify** Send a Test mode event or watch the next live delivery. Only the latest replaced secret is kept. Rotating again during an overlap ends the earlier one. See [Errors and troubleshooting](/reference/errors#webhook-deliveries) for investigating delivery failures. # Go-live checklist Source: https://docs.lynq.am/go-live/checklist Launch Card and Idram payments step by step. > [!NOTE] > > **Live payments aren't available yet.** We're working with banks and regulators to switch them on. You can build and test your whole integration in test mode today; this page describes how going live will work. Go live only after the [test plan](/go-live/testing) passes. Test and live resources are separate, so launching means replacing credentials, not changing your integration. ## 1. Connect payment methods * Get live VPOS credentials from Ameriabank for [Card payments](/payment-methods/card), merchant credentials from Idram for [Idram](/payment-methods/idram), or both. * In Live mode, enter them under **Payment methods** and enable each method. ## 2. Switch to live keys * In Live mode, open **API keys**, create the live secret key, and store it in production secret storage. It is shown once. * Put the live publishable key in your production browser configuration. * Confirm no test key remains in the production deployment. Keep your test keys working elsewhere, so you can verify future changes safely. * Pin Lynq.js to the version you tested. ## 3. Add a live webhook * In Live mode, connect a live HTTPS endpoint under **Webhooks** and store its signing secret in production secret storage. * Confirm the receiver verifies the signature, deduplicates by event ID, and compares the amount, currency, and order ID to your order. See [Webhooks](/build/webhooks). ## 4. Make a real payment **1. Create a small live order** Use an order you can identify and reconcile, not a customer's real order. **2. Pay with each enabled method** Complete a small payment on your production page with Card, Idram, or both. **3. Match the records** Check that the order, the payment in **Dashboard → Payments**, and the webhook delivery all agree. > [!TIP] > > Launch is complete when every enabled live method has produced a real payment whose verified webhook updated the intended order exactly once. After launch, watch the first payments and webhook deliveries, and read the [release notes](/resources/release-notes) before upgrading Lynq.js. For delivery problems, see [Errors and troubleshooting](/reference/errors). # Test your integration Source: https://docs.lynq.am/go-live/testing Verify the complete payment lifecycle in test mode. Test the same path your customers will use: order creation, the Payment Intent, browser Checkout, webhook verification, and fulfilment. ## Test cards Test mode is simulated by Lynq, so it needs no bank or wallet credentials. Use any future expiry date, any three-digit security code, and any cardholder name. The card number decides the result: | Card number | Result | | - | - | | `4242 4242 4242 4242` | The payment succeeds. | | `4000 0000 0000 0002` | The card is declined: `requires_payment_method` with `card_declined`. | | `4000 0000 0000 9995` | The payment stays `processing`. | Any other valid card number succeeds. Idram and Apple Pay appear in test checkouts so you can see the whole form, but they can't be paid yet. Choosing one and paying shows a notice; use a test card instead. ## Scenarios to cover | Scenario | Browser expectation | Server expectation | | - | - | - | | Successful Card payment | Checkout completes and reports `succeeded` or a pending transition before completion. | A verified success webhook marks the order paid once. | | Card declined or invalid | Checkout reports `requires_payment_method` with display-safe text. | The order remains unpaid and can accept another attempt. | | Customer closes the page | No irreversible browser-side action occurs. | A later webhook can still complete the order if payment succeeded. | | Duplicate webhook | Customer-visible state does not change twice. | The event ID is recognized and fulfilment runs once. | | Temporary webhook failure | Payment may look complete in Dashboard while delivery retries. | The receiver returns `5xx`, recovers, and processes a retry. | ## Webhook tests 1. Send a Test mode event from **Dashboard → Webhooks** and confirm the receiver verifies `Lynq-Signature` against the raw body. 2. Replay the same request and confirm it returns `2xx` without repeating fulfilment. 3. Send an invalid signature and confirm it is rejected. 4. Return `500` temporarily, confirm the delivery moves to retrying, then restore the receiver and confirm a later delivery succeeds. > [!TIP] > > Testing is complete when a payment updates the correct order exactly once through a verified webhook, including after the browser closes and after a webhook retry. Continue with the [go-live checklist](/go-live/checklist). # Accept payments with Lynq Source: https://docs.lynq.am/index Build, test, and launch a Lynq payment integration. [Lynq](https://lynq.am) gives your application a server API for creating payments, a hosted browser checkout for collecting payment details, and signed webhooks for confirming completed payments. > [!NOTE] > > **Lynq is in test mode.** Live payments aren't available yet: we're working with banks and regulators to switch them on. Everything in these docs works in test mode today, so you can build and test your whole integration now. ## Build an integration **1. Get your test API keys** [Sign up](https://dashboard.lynq.am/signup) and copy your keys from **Dashboard → API keys**. Test mode needs no bank or wallet credentials. See [API keys](/start/api-keys). **2. Create a Payment Intent on your server** Your server creates the payment and its client secret with your secret key. See [Payment Intents](/build/payment-intents). **3. Mount Checkout in the browser** Hosted Checkout collects payment details inside your page. See [Lynq.js](/build/checkout-sdk). **4. Verify the success webhook** Fulfil the order only after a verified webhook confirms the payment. See [Webhooks](/build/webhooks). **5. Go live** Once live payments are available, connect your payment methods and switch to live keys. See the [go-live checklist](/go-live/checklist). The [quickstart](/start/quickstart) walks through steps 1 to 4 with code. ## How a payment works ```mermaid theme={null} sequenceDiagram participant Customer participant Browser as Merchant browser participant Server as Merchant server participant Lynq participant Provider as Payment provider Browser->>Server: Start checkout for order Server->>Lynq: Create Payment Intent with secret key Lynq-->>Server: Payment Intent + client secret Server-->>Browser: Payment Intent ID + client secret Browser->>Lynq: Mount Checkout with publishable key Customer->>Lynq: Select method and submit payment Lynq->>Provider: Process payment Provider-->>Lynq: Payment result Lynq-->>Browser: Merchant-actionable result Lynq->>Server: Signed payment_intent.succeeded webhook Server->>Server: Verify, deduplicate, fulfil order ``` Three rules hold across every page of these docs: * **Secrets stay on your server.** The secret API key and the webhook signing secret never reach the browser. The browser gets the publishable key and one Payment Intent client secret. * **The webhook decides fulfilment.** The browser result is for customer-facing UI: browsers can close, lose connectivity, or be manipulated. Ship goods or mark an order paid only after a verified `payment_intent.succeeded` webhook. * **Test and live never mix.** Keys, Payment Intents, payment-method connections, and webhook endpoints work only within their own mode. Lynq Checkout handles Card authentication and Idram redirects itself, so your code never touches card data or drives those flows. If you have used Stripe, the model is familiar: Payment Intents, a client secret, a browser SDK, and signed webhooks. Stripe itself does not accept businesses registered in Armenia; see [Stripe in Armenia](https://lynq.am/stripe-armenia). Need help? Email [support@lynq.am](mailto:support@lynq.am). # Card payments Source: https://docs.lynq.am/payment-methods/card Activate Card payments with Ameriabank VPOS credentials. Card payments let customers pay with Visa, Mastercard, and ArCa cards in hosted Checkout. In test mode Lynq simulates them, so there is nothing to connect: see [Test cards](/go-live/testing#test-cards). For live payments, which aren't available yet, you connect VPOS credentials from Ameriabank. For an overview of how Lynq works with the VPOS you already have, see [Bank VPOS checkout without the redirect](https://lynq.am/bank-vpos). For what customers see, see [card payments](https://lynq.am/payment-methods/cards). ## What you need from Ameriabank Contact Ameriabank and request VPOS access for your merchant. | Credential | Purpose | | - | - | | Terminal ID | Identifies the VPOS terminal assigned to your merchant. | | Username | Authenticates Lynq to the VPOS service. | | Password | Authenticates Lynq to the VPOS service. | ## Connect Card payments **1. Open Card payments in live mode** In Lynq Dashboard, choose **Live mode**, go to **Payment methods**, find **Card payments**, and choose **Configure**. **2. Enter the VPOS credentials** Enter the Terminal ID, username, and password exactly as supplied. Choose **Save and enable**. **3. Confirm the connection** Card payments shows as enabled and appears in new live checkout sessions. Disable it to stop offering it; payment history is kept. Credentials are encrypted when saved and secret fields are never displayed again. To update the connection, enter all three values again. ## What the customer does 1. The customer chooses Card in Lynq Checkout and enters the cardholder name, card number, expiry date, and security code inside the Lynq-hosted iframe. 2. If the bank requires 3D Secure, Lynq.js opens and completes it. Do not implement the redirect yourself. 3. Checkout returns a result to your browser, and Lynq sends `payment_intent.succeeded` to your webhook. Your page receives only a summary such as the brand and last four digits. Card numbers and security codes never reach it. ## Troubleshooting > [accordion] Card does not appear in Checkout > > In live mode, confirm Card payments is enabled. In either mode, check that Checkout uses a publishable key from the same mode as the Payment Intent. > [accordion] Credentials cannot be saved > > Re-enter all values without leading or trailing spaces. If Ameriabank rotated the password, use the current Terminal ID, username, and new password together. # Idram payment integration Source: https://docs.lynq.am/payment-methods/idram Accept Idram wallet payments through the Lynq API. Activate Idram with your merchant EDP ID and secret key. Idram lets customers complete payment from their Idram wallet. In test mode Idram appears in Checkout but can't be paid yet. For live payments, which aren't available yet, you connect merchant credentials from Idram. For what customers see, see [Idram payments for your website](https://lynq.am/payment-methods/idram). ## What you need from Idram Contact Idram and request merchant API access. | Credential | Purpose | | - | - | | EDP ID | Identifies your merchant account to Idram. | | Secret key | Authenticates payment requests for that merchant account. | ## Connect Idram **1. Open Idram in live mode** In Lynq Dashboard, choose **Live mode**, go to **Payment methods**, find **Idram**, and choose **Configure**. **2. Enter the credentials** Enter the EDP ID and secret key exactly as supplied. Choose **Save and enable**. **3. Confirm the connection** Idram shows as enabled and appears in new live checkout sessions. Disable it to stop offering it; payment history is kept. The secret key is encrypted when saved and is not displayed again. To update the connection, enter the EDP ID and secret key together. ## What the customer does 1. The customer chooses Idram in Lynq Checkout. 2. Lynq.js hands the customer to the Idram wallet and handles the return. 3. Checkout returns a result to your browser, and Lynq sends `payment_intent.succeeded` to your webhook. Do not fulfil the order from the wallet return. Wait for the verified webhook. ## Troubleshooting > [accordion] Idram does not appear in Checkout > > Confirm Idram is enabled in live mode and that Checkout uses a live publishable key. > [accordion] Credentials cannot be saved > > Re-enter the complete EDP ID and current secret key. If the secret was rotated, replace both fields in Dashboard. > [accordion] The customer did not return from Idram > > Keep the order pending and rely on the webhook for the final state. # Lynq.js reference Source: https://docs.lynq.am/reference/checkout-sdk Public Checkout methods, events, and types. The package exports the `Checkout` class, error classes, version constant, and TypeScript types used by the public API. ```ts theme={null} import { Checkout, VERSION, ConfigError, MountError, type CheckoutConfig, type CheckoutAppearance, type PaymentResult, type LynqErrorCode, } from "@lynqpay/js"; ``` ## `CheckoutConfig` | Property | Type | Required | Default | | - | - | - | - | | `publicKey` | `string` | Yes | — | | `clientSecret` | `string` | Yes | — | | `locale` | `string` | No | `en` | | `appearance` | `CheckoutAppearance` | No | — | Test or live mode follows the publishable key. With a `pk_test_…` key the SDK writes diagnostics to the browser console; with a `pk_live_…` key it writes nothing. `appearance` fields are listed under [Customize the appearance](/build/checkout-sdk#customize-the-appearance). `locale` is a hint forwarded to hosted Checkout. ## `new Checkout(config)` Creates an unmounted browser Checkout instance. It throws `ConfigError` when `publicKey` or `clientSecret` is invalid, or when constructed outside a browser. At most one Checkout can be mounted on a page at a time. ## Lifecycle methods | Method | Returns | Description | | - | - | - | | `mount(selectorOrElement)` | `this` | Mounts the iframe into a selector or element and begins initialization. Throws `MountError` for an invalid container or lifecycle state. | | `unmount()` | `void` | Removes the iframe and resets ready/validation state. The instance remains reusable. | | `destroy()` | `void` | Permanently removes the iframe, authentication UI, message handling, and event listeners. Idempotent. | | `isMounted()` | `boolean` | Whether this instance currently owns a mounted Checkout iframe. | | `isReady()` | `boolean` | Whether hosted Checkout completed initialization. | | `isValid()` | `boolean` | Latest hosted-form `canSubmit` state. UI state only. | ## Subscription methods | Method | Payload | | - | - | | `onReady(handler)` | `ReadyEvent` | | `onValidation(handler)` | `ValidationState` | | `onPaymentResult(handler)` | `PaymentResult` | | `onCheckoutError(handler)` | `LynqError` | | `on(event, handler)` | Payload selected from `EventMap` by event name | | `off(event, handler?)` | Removes one handler or all handlers for the event | Every `on...` method returns `this` for chaining. Pass the original handler to `off` to remove one listener, or omit it to remove every listener for that event. `destroy()` clears all listeners. `onValidation` describes the form, not the payment: neither it nor `isReady()` and `isValid()` confirms that a payment completed. ## Event payloads ### `ReadyEvent` | Field | Type | Meaning | | - | - | - | | `height` | `number` | Initial iframe content height in CSS pixels. | ### `ValidationState` | Field | Type | Meaning | | - | - | - | | `canSubmit` | `boolean` | Whether the hosted form considers its current values submittable. | | `fields.name` | `boolean` | Cardholder name validity. | | `fields.number` | `boolean` | Card number validity. | | `fields.exp` | `boolean` | Expiry validity. | | `fields.cvv` | `boolean` | Security-code validity. | | `fields.brand` | `CardBrand` | Detected card brand or `unknown`. | ### `PaymentResult` `PaymentResult` is a discriminated union on `status`: * `succeeded`: includes `paymentIntentId` and optional `paymentMethod` * `processing`: includes `paymentIntentId` and optional `paymentMethod` * `requires_payment_method`: includes `paymentIntentId`, `error`, and optional `paymentMethod` `PaymentError` contains a machine-readable `code` and display-safe `message`. ## Payment-method summaries Card results may include `brand`, `last4`, `expMonth`, `expYear`, and `cardholderName`. Idram results identify the Idram provider. These summaries do not contain the full card number or security code. ## Error classes * `LynqError`: base error with `code`, `message`, and `retryable` * `ConfigError`: invalid constructor configuration or non-browser construction * `MountError`: invalid mount container or Checkout lifecycle state See [Errors and troubleshooting](/reference/errors) for every `LynqErrorCode` and recommended handling. ## `VERSION` `VERSION` is the SDK package version string. It is useful in diagnostics when reporting an integration problem. # Errors and troubleshooting Source: https://docs.lynq.am/reference/errors Resolve Checkout, payment, and webhook delivery problems. Checkout has two error paths: * Synchronous integration errors are thrown while constructing or mounting Checkout. * Hosted Checkout initialization and loading errors are sent to `onCheckoutError` as `LynqError`. Payment declines are not Checkout errors. They arrive through `onPaymentResult` with `status: "requires_payment_method"`. ## Thrown errors ### `ConfigError` Thrown when `publicKey` or `clientSecret` is missing or invalid, or `Checkout` is constructed outside a browser. Check that client-only code creates the instance after hydration and that configuration is available at that time. ### `MountError` Thrown when: * The selector does not resolve to an element * This instance is already mounted * This instance was destroyed * Another Lynq Checkout is already mounted on the page Fix the integration state before retrying; repeatedly calling `mount()` will not resolve these errors. ## `LynqErrorCode` | Code | Retryable | Recommended handling | | - | - | - | | `CONFIG_ERROR` | No | Fix the constructor configuration or browser lifecycle. | | `MOUNT_ERROR` | No | Fix the mount container or instance lifecycle. | | `CHECKOUT_CONFIGURATION_ERROR` | No | Check the publishable key and client secret. Create a fresh Payment Intent if the session is invalid. | | `CHECKOUT_SESSION_UNAVAILABLE` | No | The Payment Intent expired, was already paid, or was cancelled. Check its status on your server first: if it succeeded, show the order as paid; otherwise create a fresh Payment Intent and a new Checkout instance. | | `CHECKOUT_NETWORK_ERROR` | Yes | Keep the order unpaid and offer the customer another attempt. | | `CHECKOUT_NO_PAYMENT_METHODS` | No | In live mode, connect and enable a payment method. Test mode always offers card payments. | | `CHECKOUT_UNAVAILABLE` | Yes | Show a temporary-unavailability message and offer retry. | | `CHECKOUT_TIMEOUT` | Yes | Check network connectivity and offer a fresh attempt. | Use the runtime `error.retryable` value rather than maintaining your own retry table in application code. ## Customer-facing handling ```ts theme={null} checkout.onCheckoutError((error) => { showAlert(error.message); if (error.retryable) { showRetryAction(); } else { reportIntegrationIssue({ code: error.code }); } }); ``` The message is display-safe. Log the code, SDK `VERSION`, Payment Intent ID, environment, and occurrence time for diagnostics. Do not log keys or client secrets. ## Troubleshooting by symptom > [accordion] Checkout never becomes ready > > Handle `onCheckoutError`, confirm the container remains mounted, and check whether Content Security Policy or network controls block `https://checkout.lynq.am`. To see SDK diagnostics, reproduce the issue with a `pk_test_…` key: the SDK logs to the browser console in test mode and stays silent in live mode. > [accordion] Checkout credentials are rejected > > Create a fresh Payment Intent and confirm its environment matches the publishable key. Do not retrieve an old Payment Intent expecting to recover its client secret. > [accordion] No payment methods are available > > Check **Dashboard → Payment methods** in the Payment Intent's environment. At least one configured method must be enabled. > [accordion] The same page mounts Checkout twice > > In component frameworks, ensure cleanup calls `destroy()` and development double-render behaviour does not leave the previous instance mounted. > [accordion] The browser says success but the order is pending > > This is expected until your server verifies the webhook. Inspect the delivery in Dashboard rather than fulfilling from the browser result. ## Webhook deliveries **Dashboard → Webhooks** lists every delivery with its event ID, attempt count, latest HTTP status, and last error. | Delivery status | Meaning | | - | - | | `delivered` | Your endpoint returned `2xx`. | | `retrying` | Lynq will retry after a temporary failure. | | `failed` | The delivery is non-retryable or the retry window ended. | > [accordion] Payment succeeded but the order is still unpaid > > Find the Payment Intent in **Dashboard → Payments**, then inspect its webhook delivery. If delivery succeeded, search receiver logs by event ID and confirm the order comparison and database transaction. If delivery is retrying, restore the endpoint and let Lynq retry. > [accordion] Delivery is retrying > > Check the latest HTTP status or network error. `408`, `429`, `5xx`, timeouts, and connection failures are temporary. Fix the receiver without changing the endpoint URL unless necessary. > [accordion] Delivery failed immediately > > Check for an unreachable destination and non-retryable `4xx` responses. Confirm the URL is public HTTPS and the receiver is not rejecting valid signatures. Once it is fixed, open the delivery and choose **Resend**. > [accordion] Signature verification started failing > > Confirm the receiver uses the raw body and the current mode's signing secret, and that it accepts any matching `v1` signature. If the secret was rotated, the previous one stops signing when its overlap ends. ## Exposed credentials If a secret API key is exposed, roll it in **API keys** and choose **Now**, then install the new key in every server that uses it. Requests using the old key fail from that moment. If a webhook signing secret is exposed, rotate it with **Now** and install the new one in your receiver. ## Contact support Email [support@lynq.am](mailto:support@lynq.am) with: * Test or live mode * The Payment Intent ID, webhook event ID, or your order ID * The approximate time and your timezone * The error code, HTTP status, or delivery status, and the Lynq.js version > [!WARNING] > > Never email secret API keys, client secrets, VPOS passwords, Idram secret keys, or webhook signing secrets. # Release notes Source: https://docs.lynq.am/resources/release-notes Lynq.js release history. **2.0.1** Renamed to Lynq.js, consistent card brands and safer redirects ## Renamed to Lynq.js The Checkout SDK is now published as `@lynqpay/js`, the package for all of Lynq's browser components. Install `@lynqpay/js` and change your imports from `@lynqpay/checkout-sdk`; script tags still expose `window.Lynq`. `@lynqpay/checkout-sdk` is deprecated and receives no further releases. ## Changes * Mastercard is reported as `mastercard` in both validation events and payment results. `mc` is no longer a `CardBrand` value; if you compared against it, compare against `mastercard`. * Checkout for a Payment Intent that was already paid or cancelled now fails with `CHECKOUT_SESSION_UNAVAILABLE` (not retryable) instead of `CHECKOUT_UNAVAILABLE`. Check the payment's status on your server before creating a new one. * The SDK only follows `https:` wallet redirects and 3-D Secure pages. * README examples use test keys and the current version. ## Script tag ```html theme={null} ``` ## Before going live Test-mode 3-D Secure in this version is simulated in the browser. A later release completes it on the server; update to that release before taking live payments. **1.0.0** Initial public Checkout SDK ## Integration surfaces * ESM npm package: `@lynqpay/checkout-sdk` * Version-pinned IIFE bundle for classic script-tag integrations * Hosted Checkout mount, unmount, destroy, readiness, validation, result, and error APIs * Internal Card authentication and wallet redirect handling ## Developer experience * TypeScript declarations and TSDoc for all public APIs * Browser-only safeguard for server-rendered applications * Stable error codes with retry guidance * No source maps in the published package ## Upgrade policy Pin an exact SDK version in production. Before upgrading: 1. Read the release notes for behavioural or type changes. 2. Update the version in Test mode. 3. Run the [integration test plan](/go-live/testing). 4. Deploy the tested version to production. Patch releases fix compatible defects, minor releases add compatible features, and major releases may require integration changes. # Build with AI Source: https://docs.lynq.am/start/ai Connect your coding agent to the Lynq docs through MCP, a skill, or plain-text files. If you build with a coding agent, give it these docs directly instead of letting it answer from memory. There are three ways, from most to least integrated. ## MCP server The docs have a public MCP server. It needs no API key and lets an agent search the docs and read full pages while it works. ```text theme={null} https://docs.lynq.am/mcp ``` > [tab] Claude Code > > ```bash theme={null} > claude mcp add --transport http lynq-docs https://docs.lynq.am/mcp > ``` > [tab] Cursor and VS Code > > Open the menu next to **Copy page** at the top of any docs page and choose **Connect to Cursor** or **Connect to VS Code**. > [tab] Other clients > > Add a remote HTTP server with the URL above. The same menu has **Copy MCP Server** and **Copy MCP install command**. ## Skill The Lynq skill is a single file that tells an agent how a Lynq integration fits together: which key goes where, when to create a Payment Intent, and why fulfilment waits for the webhook. Agents connected to the MCP server get it automatically. To install it on its own: ```bash theme={null} npx skills add https://docs.lynq.am ``` You can read it at [docs.lynq.am/skill.md](https://docs.lynq.am/skill.md). ## Plain text For a chat window or a tool without MCP: | File | Contents | | - | - | | [`/llms.txt`](https://docs.lynq.am/llms.txt) | An index of every page with a one-line description. | | [`/llms-full.txt`](https://docs.lynq.am/llms-full.txt) | The full text of the docs in one file. | | Any page with `.md` added | That page as Markdown, for example [`/start/quickstart.md`](https://docs.lynq.am/start/quickstart.md). | **Copy page** at the top of each page copies the same Markdown. > [!WARNING] > > Never paste a secret API key, client secret, or webhook signing secret into an AI tool. Give the agent the name of the environment variable instead. # API keys Source: https://docs.lynq.am/start/api-keys Use test and live keys safely in the right mode. API keys identify your merchant account and select test or live mode. Each mode has one publishable key and one secret key. Find them in **Dashboard → API keys** after selecting the mode. Live mode isn't available yet, so use your test keys for now. | Key | Prefix | Where it belongs | What it can do | | - | - | - | - | | Secret key | `sk_test_` / `sk_live_` | Server-side secret storage | Create and retrieve Payment Intents. | | Publishable key | `pk_test_` / `pk_live_` | Browser configuration | Initialize hosted Checkout for a Payment Intent. | ## Get your keys **1. Copy the publishable key** Open **API keys** in Test mode. The publishable key is always shown in full, so you can copy it whenever you need it. **2. Create the secret key** Choose **Create secret key**. The complete key is shown once, so store it in your server's secret manager straight away. Afterwards the dashboard shows only its last four characters. ## Roll a secret key Roll the secret key when it may have been exposed, or on a regular schedule. **1. Choose when the current key stops working** In **API keys**, choose **Roll key** and pick **Now**, **1 hour**, **24 hours**, or **7 days**. Pick **Now** only if the key has leaked: requests using it fail immediately. **2. Store the new key** The new key is shown once. Install it in every server that uses the old one and deploy. **3. Let the old key expire** Until the time you chose, both keys work, so your servers keep accepting payments while you deploy. The dashboard shows when the old key stops working. ## Keep keys safe * Never commit a key to source control. * Never send a secret key to the browser, a mobile application, analytics, or logs. * Do not put keys or client secrets in URLs. > [!WARNING] > > A mode mismatch shows up as rejected checkout credentials or no available payment methods. The secret key, the Payment Intent, the publishable key, and the webhook endpoint must all belong to the same mode. Next, [build your first payment](/start/quickstart). # Build your first test payment Source: https://docs.lynq.am/start/quickstart Create a test payment and mount Lynq Checkout. This guide takes you from an order on your server to a mounted test checkout. It uses TypeScript and the npm package, but the same server API works from any backend language. You need a Lynq Dashboard account and its test keys; see [API keys](/start/api-keys). Test mode needs no payment-method setup: Lynq simulates card payments. **1. Store your test keys** Add the secret key to server-only configuration. The publishable key may be exposed to the browser. ```bash .env theme={null} LYNQ_SECRET_KEY=sk_test_... ``` Do not commit the environment file. Expose the publishable key using the browser-safe configuration mechanism provided by your framework. **2. Create a Payment Intent on your server** Create one Payment Intent when the customer begins paying for an order. Build the amount and order ID from server-side order data instead of trusting values sent by the browser. ```ts server/create-lynq-payment.ts theme={null} const response = await fetch("https://api.lynq.am/api/payment-intents", { method: "POST", headers: { Authorization: `Bearer ${process.env.LYNQ_SECRET_KEY}`, "Content-Type": "application/json", "Idempotency-Key": `order:${order.id}`, }, body: JSON.stringify({ amount: order.totalMinor, currency: order.currency, orderId: order.id, customer: { id: order.customer.id, email: order.customer.email, name: order.customer.name, }, }), }); if (!response.ok) { throw new Error(`Lynq returned HTTP ${response.status}`); } const paymentIntent = await response.json() as { id: string; clientSecret: string; }; return { id: paymentIntent.id, clientSecret: paymentIntent.clientSecret, }; ``` Amounts use minor currency units. For AMD 25,000.00, send `2500000`. Reuse the same idempotency key only when retrying this exact create request. **3. Install Lynq.js** ```bash theme={null} npm install @lynqpay/js ``` The package is ESM-only and includes TypeScript declarations. If your application does not use a bundler, see the [script-tag installation](/build/checkout-sdk#script-tag-installation). **4. Request the checkout session** From the browser, call your own backend route. Keep the secret key behind that route; return only the Payment Intent ID and client secret. ```ts client/start-checkout.ts theme={null} const response = await fetch(`/api/orders/${orderId}/lynq-payment`, { method: "POST", }); if (!response.ok) throw new Error("Could not start checkout"); const paymentIntent = await response.json() as { id: string; clientSecret: string; }; ``` **5. Mount Checkout** Add an empty container to the page, register event handlers, and then mount Checkout. ```html theme={null}
``` ```ts client/mount-checkout.ts theme={null} import { Checkout, type PaymentResult } from "@lynqpay/js"; const checkout = new Checkout({ publicKey: "pk_test_...", clientSecret: paymentIntent.clientSecret, }); checkout .onReady(() => hideCheckoutLoadingState()) .onPaymentResult((result: PaymentResult) => { if (result.status === "succeeded") { showOrderConfirmation(result.paymentIntentId); return; } if (result.status === "processing") { showPaymentPending(result.paymentIntentId); return; } showPaymentError(result.error.message); }) .onCheckoutError((error) => showCheckoutError(error.message)) .mount("#lynq-checkout"); ``` Register handlers before `mount()`. Destroy the instance when the page is removed and create a new instance for a new Payment Intent. **6. Complete a test payment** Pay with a [test card](/go-live/testing#test-cards). Confirm the browser shows a useful result and the payment appears under **Dashboard → Payments**. **7. Add authoritative fulfilment** Configure a test webhook endpoint, verify its signature, and update the order only after `payment_intent.succeeded`. See [Verify webhooks](/build/webhooks). > [!TIP] > > The integration is complete when Checkout accepts a test payment and your verified webhook updates the matching order exactly once. Continue with the [test plan](/go-live/testing) before switching to live mode.