npm package: @linkzly/affiliate-node
Description: Node.js SDK for Linkzly affiliate S2S conversions, reversals, and webhook verification
Latest version: 1.1.0

# @linkzly/affiliate-node

Server-side Node.js SDK for Linkzly affiliate S2S conversions, reversals, and webhook verification.

## Installation

```bash
npm install @linkzly/affiliate-node
```

Requires **Node.js >= 18**.

## Quick Start

```typescript
import { LinkzlyAffiliate } from '@linkzly/affiliate-node';

const client = new LinkzlyAffiliate({
  apiSecret: process.env.LINKZLY_API_SECRET!,
  programId: process.env.LINKZLY_PROGRAM_ID!,
});

// Track a conversion
const result = await client.trackConversion({
  order_id: 'ORD-12345',
  order_value: 99.99,
  currency: 'USD',
  click_id: req.body.lz_click_id, // From Web SDK
});

if (result.success) {
  console.log('Commission:', result.data?.commission_amount);
}
```

## API Reference

### `new LinkzlyAffiliate(config)`

| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| `apiSecret` | `string` | Yes | — | Program API secret from Linkzly console |
| `programId` | `string` | Yes | — | Your program ID |
| `baseUrl` | `string` | No | Production URL | Affiliate service base URL |
| `timeout` | `number` | No | `10000` | Request timeout in ms |
| `debug` | `boolean` | No | `false` | Enable debug logging |

### `trackConversion(request)`

Send a server-to-server conversion. Automatically signs with HMAC-SHA256.

```typescript
const result = await client.trackConversion({
  order_id: 'ORD-12345',        // Required — unique order ID
  order_value: 99.99,            // Required — total value
  currency: 'USD',               // Required — ISO 4217
  click_id: 'lz_click_id_here', // From URL param (primary attribution)
  cookie_id: 'lz_aff_cookie',   // From cookie (secondary attribution)
  customer_id: 'cust_abc',       // Your internal customer ID
  coupon_code: 'SAVE20',         // Coupon used (tertiary attribution)
  line_items: [                   // For category-based commission
    { product_id: 'SKU-1', amount: 49.99, category: 'electronics' },
    { product_id: 'SKU-2', amount: 50.00, category: 'clothing' },
  ],
});
```

**Idempotent:** Sending the same `order_id` returns the existing conversion.

### `reverseConversion(request)`

Reverse a conversion (refund/chargeback). Creates negative ledger entry.

```typescript
const result = await client.reverseConversion({
  order_id: 'ORD-12345',
  reason: 'Customer refund',
});
```

### `verifyWebhookSignature(payload, signature, timestamp)`

Verify an incoming webhook signature:

```typescript
const isValid = client.verifyWebhookSignature(
  rawBody,
  req.headers['x-linkzly-signature'],
  req.headers['x-linkzly-timestamp']
);
```

### `parseWebhookEvent(payload, signature, timestamp)`

Verify and parse a webhook event in one call:

```typescript
const event = client.parseWebhookEvent(rawBody, signature, timestamp);
if (event) {
  switch (event.type) {
    case 'conversion.created':
      // Handle new conversion
      break;
    case 'conversion.reversed':
      // Handle reversal
      break;
    case 'payout.created':
      // Handle payout
      break;
  }
}
```

### `sign(body)`

Generate signature for custom requests:

```typescript
const { signature, timestamp } = client.sign(JSON.stringify(data));
```

## Express Middleware

```typescript
import { LinkzlyAffiliate, webhookMiddleware } from '@linkzly/affiliate-node';

const client = new LinkzlyAffiliate({
  apiSecret: process.env.LINKZLY_API_SECRET!,
  programId: process.env.LINKZLY_PROGRAM_ID!,
});

// Use raw body for signature verification
app.post('/webhooks/linkzly',
  express.raw({ type: 'application/json' }),
  webhookMiddleware(client),
  (req, res) => {
    const event = req.linkzlyEvent;
    console.log('Webhook:', event.type, event.data);
    res.json({ received: true });
  }
);
```

## Integration Examples

### Next.js API Route

```typescript
// app/api/conversions/route.ts
import { LinkzlyAffiliate } from '@linkzly/affiliate-node';

const client = new LinkzlyAffiliate({
  apiSecret: process.env.LINKZLY_API_SECRET!,
  programId: process.env.LINKZLY_PROGRAM_ID!,
});

export async function POST(req: Request) {
  const body = await req.json();

  const result = await client.trackConversion({
    order_id: body.orderId,
    order_value: body.total,
    currency: 'USD',
    click_id: body.lz_click_id,
    cookie_id: body.lz_cookie_id,
    customer_id: body.customerId,
  });

  return Response.json(result);
}
```

### Express + Web SDK

```typescript
// Frontend sends attribution from @linkzly/affiliate-web
// Backend receives and forwards to Linkzly via this SDK

app.post('/api/checkout', async (req, res) => {
  // Process payment...
  const order = await processPayment(req.body);

  // Track affiliate conversion
  const result = await client.trackConversion({
    order_id: order.id,
    order_value: order.total,
    currency: order.currency,
    click_id: req.body.click_id,      // From Web SDK
    cookie_id: req.body.cookie_id,    // From Web SDK
    customer_id: order.customerId,
  });

  res.json({ order, affiliate: result });
});
```

## HMAC Signature Format

The SDK signs requests as:
```
message = "{programId}.{timestamp}.{body}"
signature = HMAC-SHA256(message, apiSecret) → hex
```

Headers sent:
- `X-Linkzly-Signature`: hex-encoded HMAC
- `X-Linkzly-Program-Id`: your program ID
- `X-Linkzly-Timestamp`: Unix timestamp (seconds)

Timestamp must be within 5 minutes of server time.

## License

MIT