Skip to content

ApogeoPay/client

Repository files navigation

@apogeopay/client

Official TypeScript client for ApogeoPay — a payment orchestration platform that fronts MercadoPago, PayPal, and Stripe behind a unified REST API.

Status: 0.2.0-rc.1 — full API surface for subscriptions, plans, payments, and tenants, plus webhook verification. Honors Retry-After. CJS + ESM dual build. Types are hand-maintained for now; auto-generation from openapi.json lands in a follow-up release.

License: MIT.


Install

npm install @apogeopay/client

Requires Node.js 20+ (uses native fetch).

Quickstart

import { ApogeopayClient, verifyWebhookSignature } from '@apogeopay/client';

const client = new ApogeopayClient({
  apiKey: process.env.APOGEOPAY_API_KEY!,
  baseUrl: process.env.APOGEOPAY_URL ?? 'https://api.apogeopay.com',
});

// Create a subscription. The client auto-generates an Idempotency-Key
// unless you pass one.
const sub = await client.subscriptions.create({
  planId: 'plan_pro_monthly',
  user: { id: 'usr_123', email: 'alice@example.com', country: 'AR' },
  metadata: { sale_intent: 'si_abc' },
}, { idempotencyKey: 'si_abc' });

console.log(sub.id, sub.status); // 'sub_xxx', 'PENDING'
if (sub.checkoutUrl) {
  // Redirect the user to authorize the subscription with the provider.
  console.log('Redirect →', sub.checkoutUrl);
}

Webhook verification

ApogeoPay signs every outbound webhook with HMAC-SHA256 over the raw body using your tenant's webhookSecret. The hex digest travels in X-ApogeoPay-Signature.

import express from 'express';
import { verifyWebhookSignature } from '@apogeopay/client';

const app = express();

// IMPORTANT: register the raw-body parser BEFORE express.json() for the
// webhook route — the HMAC is computed over the exact bytes received.
app.post(
  '/webhooks/apogeopay',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const sig = req.header('x-apogeopay-signature');
    if (!sig) return res.status(401).end();

    const valid = verifyWebhookSignature(
      req.body as Buffer,
      sig,
      process.env.APOGEOPAY_WEBHOOK_SECRET!,
    );
    if (!valid) return res.status(401).end();

    const event = JSON.parse((req.body as Buffer).toString('utf8'));
    // ... process event idempotently by `event.delivery_id` ...

    res.status(200).end();
  },
);

The verifier uses crypto.timingSafeEqual for the comparison — safe against timing-side-channel attacks.

Reference

ApogeopayClient

new ApogeopayClient({
  apiKey: string,                 // required — `apgpay_*`
  baseUrl: string,                // required — `https://api.apogeopay.com`
  timeoutMs?: number,             // default 10_000
  maxRetries?: number,            // default 3 (retries 5xx, 429, network errors)
  retryDelayMs?: number,          // default 250 (exponential backoff with jitter)
  fetchImpl?: typeof fetch,       // inject a custom fetch (tests, polyfills)
  userAgent?: string,
})

client.subscriptions

Method Endpoint
create(input, opts?) POST /v1/subscriptions
getById(id) GET /v1/subscriptions/:id
getCurrent({ externalUserId }) GET /v1/subscriptions/current?externalUserId=…
cancel(id, input?, opts?) DELETE /v1/subscriptions/:id
pause(id, input?, opts?) POST /v1/subscriptions/:id/pause
resume(id, opts?) POST /v1/subscriptions/:id/resume
changePlan(id, input, opts?) POST /v1/subscriptions/:id/change-plan
revertScheduledChange(id, opts?) DELETE /v1/subscriptions/:id/scheduled-change
cancelAtPeriodEnd(id, opts?) POST /v1/subscriptions/:id/cancel-at-period-end

client.plans

Method Endpoint
create(input, opts?) POST /v1/plans
list() GET /v1/plans
getById(id) GET /v1/plans/:id
delete(id, opts?) DELETE /v1/plans/:id

client.payments

Method Endpoint
create(input, opts?) POST /v1/payments
list(input?) GET /v1/payments (query params: externalUserId, status, limit, cursor)
getById(id) GET /v1/payments/:id
refund(id, input?, opts?) POST /v1/payments/:id/refund (omit amountCents for full refund)

client.tenants

Method Endpoint
me() GET /v1/tenants/me (returns the tenant resolved by the API key)

All mutating methods accept { idempotencyKey?: string } as the last arg; when omitted, the client generates a UUID.

Retry-After

If the server returns a Retry-After header on 429 or 503, the client honors it (overrides the local exponential backoff). Both forms are supported: integer seconds (Retry-After: 5) and HTTP-date (Retry-After: Tue, 01 Jan 2030 12:00:00 GMT). Unparseable values fall back to the default backoff.

Errors

The client only throws three error types — every HTTP failure surfaces as an ApogeopayError:

import { isApogeopayError, isRetryableError } from '@apogeopay/client';

try {
  await client.subscriptions.create(/* ... */);
} catch (err) {
  if (isApogeopayError(err)) {
    console.error(err.code, err.httpStatus, err.details, err.requestId);
    if (isRetryableError(err)) {
      // The client already retried up to `maxRetries`. Give up or alert.
    }
  } else {
    throw err; // not from the SDK
  }
}
Subclass When httpStatus Retryable?
ApogeopayError Server returned 4xx/5xx with a JSON error.code body the server's status depends — see isRetryableError
NetworkError DNS, connect refused, socket hangup 0 yes
TimeoutError timeoutMs exceeded — aborted via AbortController 0 yes

The retry helper considers 5xx, 429, network failures, and timeouts as transient. 4xx errors (except 429) propagate immediately — fix the input.

Roadmap

  • Auto-generated types from openapi.json (drift-free).
  • npm publish to the @apogeopay scope (currently private: true).
  • Real publish CI pipeline (currently --dry-run only).
  • signal: AbortSignal opt-in for caller-driven cancellation.
  • onRequest / onResponse hooks for consumer logging.

See tasks/task-016b-client-sdk-hardening.md for the current scope and DoD.

About

Official TypeScript SDK for ApogeoPay — payment orchestration over MercadoPago, PayPal, and Stripe. MIT.

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors