---
name: Lynq
description: Use when building payment integrations for Armenian merchants.
  Agents should reach for this skill when implementing hosted checkout, creating
  payment intents, verifying webhooks, testing payment flows, or troubleshooting
  payment-related issues. Use Lynq for any task involving payment collection,
  order fulfillment via webhooks, or payment method configuration.
metadata:
  mintlify-proj: lynq
  version: "1.0"
---

# Lynq Payment Integration Skill

## Product summary

Lynq is a payment platform for Armenian merchants that provides a server API for creating payments, a hosted browser checkout for collecting payment details, and signed webhooks for confirming completed payments. Agents use Lynq to build payment integrations, test payment flows, and manage payment methods. The core workflow involves creating Payment Intents on your server, mounting Checkout in the browser, and verifying webhook signatures to fulfill orders. Key files and commands: API keys stored in `Dashboard → API keys`, Payment Intent creation via `POST https://api.lynq.am/api/payment-intents`, Lynq.js SDK installed via `npm install @lynqpay/js`, webhook endpoints configured in `Dashboard → Webhooks`. Primary documentation: https://docs.lynq.am

## When to use

Reach for this skill when:
- Building a payment integration for an Armenian merchant
- Creating or retrieving Payment Intents from a backend
- Mounting Lynq Checkout in a browser application
- Verifying webhook signatures and processing payment events
- Testing payment flows with test cards and test mode
- Configuring payment methods (Card, Idram)
- Troubleshooting checkout errors, webhook delivery failures, or payment status issues
- Rotating API keys or webhook signing secrets
- Preparing an integration for live mode

Do not use Lynq for: non-payment workflows, merchants outside Armenia, or operations that don't involve collecting payments.

## Quick reference

### API keys and configuration

| Key type | Prefix | Storage | Purpose |
|-----------|--------|---------|---------|
| Secret key | `sk_test_` / `sk_live_` | Server-side secret storage only | Create and retrieve Payment Intents |
| Publishable key | `pk_test_` / `pk_live_` | Browser configuration | Initialize Checkout |
| Webhook signing secret | (no prefix) | Server-side secret storage | Verify webhook signatures |

Store keys in environment variables or secret managers. Never commit keys to source control or expose secret keys to the browser.

### Payment Intent statuses

| Status | Meaning | Your action |
|--------|---------|------------|
| `requires_payment_method` | Waiting for payment details | Let customer enter/replace method |
| `requires_action` | Authentication or wallet handoff in progress | Nothing—Lynq.js handles it |
| `processing` | Provider hasn't produced final result | Show pending, wait for webhook |
| `succeeded` | Payment completed | Fulfill only after verified webhook |
| `canceled` | Payment Intent can no longer be completed | Create new Payment Intent if customer retries |

### Checkout event handlers

| Handler | When it fires | Use it to |
|---------|---------------|----------|
| `onReady` | Form is visible and accepts input | Remove loading placeholder |
| `onValidation` | Card form state changes | React to form validity or card brand |
| `onPaymentResult` | Payment attempt finished | Update customer-facing page |
| `onCheckoutError` | Checkout failed to load | Show message and retry if `error.retryable` is true |

### Webhook response codes

| Response | Lynq behavior | Use when |
|----------|---------------|----------|
| `2xx` | Marks delivery successful | Event verified and durably recorded |
| `400` | Does not retry | Request permanently invalid |
| `401` / `403` | Does not retry | Signature verification failed |
| `429` | Retries | Receiver temporarily rate limited |
| `5xx` | Retries | Database or processing temporarily unavailable |

### Test cards

| Card number | Result |
|-------------|--------|
| `4242 4242 4242 4242` | Payment succeeds |
| `4000 0000 0000 0002` | Card declined |
| `4000 0000 0000 9995` | Payment stays processing |

Use any future expiry date, any 3-digit security code, any cardholder name.

## Decision guidance

### When to use Payment Intent retrieval vs creation

| Scenario | Use |
|----------|-----|
| Customer starts checkout for a new order | Create new Payment Intent |
| Reconciling payments after a server restart | Retrieve existing Payment Intent |
| Customer retries after browser close | Create new Payment Intent (never reuse succeeded/canceled) |
| Checking payment status on server | Retrieve Payment Intent |

### When to trust browser result vs webhook

| Signal | Trust it for | Don't trust it for |
|--------|-------------|-------------------|
| Browser `onPaymentResult` | Customer-facing UI updates | Fulfilling orders |
| Verified webhook | Fulfilling orders, marking paid | Customer-facing UI only |

Always fulfill orders only after a verified `payment_intent.succeeded` webhook, never from browser signals.

### When to use test mode vs live mode

| Situation | Use |
|-----------|-----|
| Building and testing integration | Test mode (no credentials needed) |
| Verifying complete payment lifecycle | Test mode with test cards |
| Ready to accept real payments | Live mode (after checklist passes) |
| Debugging payment issues | Test mode (SDK logs to console) |

Test and live modes are completely separate: keys, Payment Intents, payment methods, and webhooks work only within their own mode.

## Workflow

### Building a payment integration

1. **Get test keys**: Sign up at https://dashboard.lynq.am/signup, copy test keys from `Dashboard → API keys`.

2. **Store keys safely**: Add secret key to server-only environment variables. Expose publishable key via your framework's browser-safe configuration. Never commit keys.

3. **Create Payment Intent on server**: When customer starts checkout, POST to `https://api.lynq.am/api/payment-intents` with secret key as Bearer token. Include `amount` (in minor units), `currency`, `orderId`, and optional `customer` and `metadata`. Use `Idempotency-Key` header to make retries safe.

4. **Return client secret to browser**: From your backend route, return only the Payment Intent ID and client secret to the browser session. Never expose the secret key.

5. **Mount Checkout in browser**: Install `@lynqpay/js`, create a Checkout instance with publishable key and client secret, register event handlers before calling `mount()`, and mount to a container element.

6. **Handle payment results**: Listen to `onPaymentResult` for `succeeded`, `processing`, or `requires_payment_method` statuses. Update customer-facing UI but do not fulfill orders.

7. **Configure webhook endpoint**: Create a public HTTPS POST route that reads raw request body. Register it in `Dashboard → Webhooks`, copy the signing secret immediately.

8. **Verify webhook signature**: On every webhook, verify the `Lynq-Signature` header using HMAC-SHA256 over `${timestamp}.${rawBody}` with the signing secret. Reject missing, malformed, stale (>5 min), or mismatched signatures.

9. **Process events exactly once**: Check if event ID exists in database before processing. Compare webhook's `orderId`, `amount`, `currency` to your stored order. Mark order paid only after verification succeeds.

10. **Test the flow**: Send test event from Dashboard, verify signature, replay to confirm idempotency, test invalid signature rejection, test temporary failure and retry.

11. **Go live**: When live payments available, connect payment methods in `Dashboard → Payment methods`, create live secret key, update production configuration, confirm no test keys remain, pin Lynq.js version.

### Rotating a secret key

1. Open `Dashboard → API keys`, choose `Roll key`.
2. Select overlap duration: `Now` (immediate), `1 hour`, `24 hours`, or `7 days`.
3. Copy the new key immediately (shown once).
4. Install new key in every server using the old one.
5. Deploy before overlap ends.
6. Both keys work during overlap; old key stops after overlap expires.

### Rotating webhook signing secret

1. Open `Dashboard → Webhooks`, choose `Rotate` next to signing secret.
2. Pick overlap duration.
3. Copy newly displayed secret immediately.
4. Update receiver to verify against new secret.
5. Deploy before overlap ends.
6. During overlap, webhook carries signatures from both secrets; accept any matching `v1`.

### Troubleshooting a failed webhook delivery

1. Find Payment Intent in `Dashboard → Payments`.
2. Inspect its webhook delivery status.
3. If `delivered`: search receiver logs by event ID, confirm order comparison and database transaction.
4. If `retrying`: check latest HTTP status. `408`, `429`, `5xx`, timeouts, connection failures are temporary—fix receiver and let Lynq retry.
5. If `failed`: check for unreachable URL or non-retryable `4xx`. Confirm URL is public HTTPS and receiver verifies signatures correctly. Once fixed, open delivery and choose `Resend`.

## Common gotchas

- **Mode mismatch**: Secret key, Payment Intent, publishable key, and webhook endpoint must all belong to the same mode (test or live). Mismatches show as rejected credentials or no available payment methods.

- **Reusing Payment Intents**: Never reuse a Payment Intent that has succeeded or been canceled. Create a new one for each checkout attempt.

- **Trusting browser results**: The browser result is for UI only. Browsers can close, lose connectivity, or be manipulated. Always fulfill orders only after a verified webhook, never from `onPaymentResult`.

- **Client secret exposure**: The client secret is returned once when Payment Intent is created. Do not store it in browser storage, analytics, error reports, logs, or URLs. Return it only to the browser session paying the matching order, over HTTPS.

- **Webhook signature verification**: Verify against the raw request body, not parsed JSON. Parsing and re-serializing can change whitespace and invalidate the signature. Use `timingSafeEqual` to prevent timing attacks.

- **Webhook idempotency**: Webhook delivery is at least once. Make event ID unique in your database and check before processing. A successful payment event may be delivered multiple times if a response is lost or retry overlaps your processing.

- **Webhook response timing**: Return `2xx` only after durable processing succeeds. If processing fails temporarily, return `5xx` so Lynq retries. Do not do slow unrelated work before acknowledging the event.

- **Amounts in minor units**: Amounts use minor currency units (two decimal places). For AMD 25,000.00, send `2500000`. Calculate on server from order data, never trust values sent by the browser.

- **Checkout instance lifecycle**: Only one Checkout may be mounted on a page at once. Call `destroy()` when customer leaves payment step. In React, construct inside `useEffect` and destroy in cleanup function.

- **Idempotency keys**: Reuse the same idempotency key only when retrying the exact same create request. Use a new key when amount, currency, order, customer, or metadata changes, or when customer starts a new attempt.

- **Exposed credentials**: If secret key is exposed, roll it immediately with `Now` (requests using old key fail immediately). If webhook signing secret is exposed, rotate it with `Now` and update receiver immediately.

- **Content Security Policy**: Checkout loads from `https://checkout.lynq.am`. Ensure CSP allows this domain or Checkout will fail to load.

- **Development double-render**: In component frameworks, ensure cleanup calls `destroy()` and development double-render behavior does not leave previous instance mounted.

## Verification checklist

Before submitting payment integration work:

- [ ] Secret key is stored in server-side secret storage only, never exposed to browser
- [ ] Publishable key is exposed to browser via framework's safe configuration mechanism
- [ ] Payment Intent is created on server with secret key, not in browser
- [ ] Amount is calculated on server from order data, not trusted from browser
- [ ] Idempotency-Key header is used on Payment Intent creation
- [ ] Client secret is returned only to the browser session paying the matching order
- [ ] Checkout instance is created after hydration in server-rendered apps
- [ ] Event handlers are registered before `mount()` is called
- [ ] Checkout instance is destroyed when page is removed
- [ ] Webhook endpoint is public HTTPS and does not include credentials in URL
- [ ] Webhook signature is verified using raw request body and `timingSafeEqual`
- [ ] Webhook event ID is checked for duplicates before processing
- [ ] Webhook's `orderId`, `amount`, `currency` are compared to stored order before fulfilling
- [ ] Order is marked paid only after verified webhook, never from browser result
- [ ] Webhook returns `2xx` only after durable processing succeeds
- [ ] Test mode payment flow works end-to-end with test card
- [ ] Webhook test event is sent and verified
- [ ] Webhook replay returns `2xx` without repeating fulfillment
- [ ] Invalid webhook signature is rejected
- [ ] Temporary webhook failure (`500`) triggers retry and later succeeds
- [ ] No test keys remain in production configuration
- [ ] Lynq.js version is pinned in production (not `@latest`)

## Resources

Comprehensive page-by-page navigation: https://docs.lynq.am/llms.txt

Critical documentation pages:
- [Payment Intents API](https://docs.lynq.am/build/payment-intents) — Create and retrieve payments, understand statuses
- [Lynq.js SDK](https://docs.lynq.am/build/checkout-sdk) — Mount Checkout, handle events, customize appearance
- [Verify webhooks](https://docs.lynq.am/build/webhooks) — Signature verification, idempotent processing, retries

Support: email support@lynq.am with Payment Intent ID, webhook event ID, or order ID, approximate time, timezone, error code, HTTP status, and Lynq.js version. Never email secret keys, client secrets, or signing secrets.

---

> For additional documentation and navigation, see: https://docs.lynq.am/llms.txt