Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

### Added

- Optional `iframe` field on `invoices.create` (`iframe?: boolean | number`). Like `paymentMode`, it is an unsigned passthrough — sent in the request body but excluded from the HMAC signature. Set `iframe: true` to request a checkout suitable for embedding.
- Retry with exponential backoff and full jitter for transient failures (429, 5xx, network errors, timeouts), honoring `Retry-After`. Only replay-safe requests are retried — GETs, calls carrying an `Idempotency-Key`, and pure reads such as `payments.checkStatus`. Charges are never replayed.
- `maxRetries` client option (default `2`; `0` disables).
- `PaylinkApiError.retryAfterMs` and `PaylinkApiError.isRateLimited`.
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,22 @@ const checkout = await paylink.invoices.create({
console.log(checkout.checkoutUrl, checkout.invoiceId, checkout.expiresAt);
```

Pass `iframe: true` to receive a `checkoutUrl` suitable for embedding — for
example rendering the hosted checkout inside an `<iframe>` on your own page
instead of a full-page redirect:

```ts
const embedded = await paylink.invoices.create({
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com',
orderTitle: 'Gold Plan',
orderAmount: '250.00',
currency: 'USD',
iframe: true, // optional, unsigned passthrough (like paymentMode)
});
```

Both credentials are issued in the PayLink dashboard under
**Settings → Payment Integrations**. `publicToken` is sent on every request;
`hashToken` is the secret used only to sign — it never leaves your server.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@getpayin/paylink",
"version": "0.1.1",
"version": "0.2.0",
"description": "Official server-side Node.js/TypeScript SDK for the PayLink payment integration API (checkouts, payment operations, card tokens, recurring mandates, webhook verification).",
"keywords": [
"paylink",
Expand Down
1 change: 1 addition & 0 deletions src/internal/fieldOrders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export const INVOICE_CREATE: EndpointSpec<CreateInvoiceParams> = {
{ sdk: 'webhookUrl', wire: 'webhook_url' },
{ sdk: 'orderDetails', wire: 'order_details' },
{ sdk: 'paymentMode', wire: 'payment_mode', signed: false },
{ sdk: 'iframe', wire: 'iframe', signed: false },
],
};

Expand Down
7 changes: 5 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ export interface BillingAddress {

/**
* Parameters for `invoices.create`. `redirectionUrl` and `webhookUrl` must be
* HTTPS URLs on the integration's registered domain; `paymentMode` is an
* optional passthrough that is sent to the API but excluded from the signature.
* HTTPS URLs on the integration's registered domain; `paymentMode` and `iframe`
* are optional passthroughs that are sent to the API but excluded from the
* signature. Set `iframe: true` to have the API return a checkout suitable for
* embedding.
*/
export interface CreateInvoiceParams {
firstName: string;
Expand All @@ -51,6 +53,7 @@ export interface CreateInvoiceParams {
webhookUrl?: string;
orderDetails?: string;
paymentMode?: string;
iframe?: boolean | number;
}

export interface CreateInvoiceResult {
Expand Down
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const VERSION = '0.1.1';
export const VERSION = '0.2.0';
23 changes: 23 additions & 0 deletions test/sign-request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,29 @@ describe('buildSignedBody — body construction rules', () => {
expect(withMode.signature).toBe(withoutMode.signature);
});

it('sends iframe in the body but excludes it from the signature', () => {
const withIframe = create(INVOICE_CREATE, {
firstName: 'A',
lastName: 'B',
email: 'a@b.com',
orderTitle: 'T',
orderAmount: 1,
currency: 'USD',
iframe: true,
});
const withoutIframe = create(INVOICE_CREATE, {
firstName: 'A',
lastName: 'B',
email: 'a@b.com',
orderTitle: 'T',
orderAmount: 1,
currency: 'USD',
});

expect(withIframe.iframe).toBe('1');
expect(withIframe.signature).toBe(withoutIframe.signature);
});

it('appends the US state block (us_state, postal_code) after city', () => {
const body = create(VCC_CHARGE, {
firstName: 'S',
Expand Down
1 change: 1 addition & 0 deletions test/type-safety.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ describe('field registry type-linking', () => {
fields: [
{ sdk: 'orderTitle', wire: 'order_title' },
{ sdk: 'paymentMode', wire: 'payment_mode', signed: false },
{ sdk: 'iframe', wire: 'iframe', signed: false },
],
};

Expand Down