@apogeopay/client
v0.2.0
Published
Official TypeScript client for ApogeoPay — payment orchestration over MercadoPago, PayPal, and Stripe.
Maintainers
Readme
@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 forsubscriptions,plans,payments, andtenants, plus webhook verification. HonorsRetry-After. CJS + ESM dual build. Types are hand-maintained for now; auto-generation fromopenapi.jsonlands in a follow-up release.License: MIT.
Install
npm install @apogeopay/clientRequires 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: '[email protected]', 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 publishto the@apogeopayscope (currentlyprivate: true).- Real publish CI pipeline (currently
--dry-runonly). signal: AbortSignalopt-in for caller-driven cancellation.onRequest/onResponsehooks for consumer logging.
See tasks/task-016b-client-sdk-hardening.md for the current scope and DoD.
