API Overview
The Zelta Pay API provides a RESTful interface for creating and managing payment links. This overview covers the essential information you need to get started.
Base URLs
Section titled “Base URLs”Production
Section titled “Production”https://api-pay.zelta.devAPI Versioning
Section titled “API Versioning”All endpoints are versioned using the URL path:
/v1/payment-linksBreaking changes will be introduced under new version paths (e.g., /v2/payment-links).
Authentication
Section titled “Authentication”The Zelta Pay API uses API key authentication. Include your API key in the X-API-Key header:
X-API-Key: your-api-key-hereWhere to get your API key:
- Log in to your Zelta Pay Dashboard
- Open API Key in the sidebar (shown to the admin and owner roles only)
- Type an alias in the Nueva API key card and click Crear (the button stays disabled until the account has an active payment provider)
- Click Ver llave on the new row, then Copiar
- Store it securely (never commit to version control)
JWT Authentication (Protected Endpoints)
Section titled “JWT Authentication (Protected Endpoints)”Protected endpoints require JWT access token in the Authorization header:
Authorization: Bearer <your_access_token>Additional header required for account-scoped operations:
X-Account-ID: <your_account_id>Rate Limiting
Section titled “Rate Limiting”- Limit: 60 requests per window per API key
- Window: fixed 60-second window that starts at the key’s first request after the previous window ended
- Scope: one budget per API key, shared by every
/v1endpoint (payment links, products, customers, subscriptions, collections and charges)
Headers (sent on /v1 responses once the API key has been validated; a 401, 403 or 404 authentication failure carries none of them):
RateLimit-Policy: The policy,60;w=60RateLimit-Limit: Maximum requests allowed in the window (60)RateLimit-Remaining: Requests remaining in the current windowRateLimit-Reset: Seconds until the window resets (for example37), not a timestamp. Omitted when it reaches0Retry-After: Seconds to wait before retrying (only on a429)
A 429 response has a plain-text body (Content-Type: text/plain), not the JSON envelope, and no error code:
HTTP/1.1 429 Too Many RequestsContent-Type: text/plain; charset=UTF-8Retry-After: 37RateLimit-Policy: 60;w=60RateLimit-Limit: 60RateLimit-Remaining: 0RateLimit-Reset: 37
Too many requests, please try again later.Check response.status === 429 before calling response.json(), which throws on this body.
Response Format
Section titled “Response Format”All API responses follow a consistent JSON structure:
Success Response
Section titled “Success Response”{ "success": true, "data": { "paymentLink": { "id": "7312345678901234567", "paymentLinkUrl": "https://pay.zelta.dev/Xk3v9Q_aB7mN2pL8rT4wYz1c", "customerName": "John Doe", "concept": "Consulting Service", "amount": 2500, "status": "pending", "createdAt": "2024-01-15T10:30:00.000Z", "expiresAt": "2024-01-18T10:30:00.000Z", "isTest": true } }, "message": "Payment link created successfully", "timestamp": "2024-01-15T10:30:00.000Z"}Response Fields:
success: Alwaystruefor successful responsesdata: The response payload, always an object (single items come wrapped, e.g.{ paymentLink }; lists return{ <items>, total }, e.g.{ paymentLinks, total })message: Always present. Endpoints without a specific message return"Operation completed successfully"timestamp: ISO 8601 timestamp of the response
IDs such as id, accountId or projectId are numeric strings (for example "7312345678901234567") with no prefix. The last path segment of paymentLinkUrl is the link’s 24-character hashUrl (letters, digits, - and _), which is what GET /v1/payment-links/{hashUrl} and the cancel endpoint take.
Error Response
Section titled “Error Response”{ "success": false, "message": "Error description", "error": { "code": "ERROR_CODE", "details": "Additional error details" }, "timestamp": "2024-01-15T10:30:00.000Z"}Error Fields:
success: Alwaysfalsefor errorsmessage: User-friendly error descriptionerror.code: Machine-readable error codeerror.details: (Optional) Technical detailstimestamp: ISO 8601 timestamp
There are two exceptions to this shape:
- Request validation errors (
400, an invalid body or query parameter) return the validation result, witherror.issueslisting the failing fields, and nomessage,error.codeortimestamp. See Validation Errors. - Rate limit errors (
429) have a plain-text body. See Rate Limiting.
A body that is not valid JSON does use the standard envelope:
{ "success": false, "message": "Malformed JSON in request body", "error": { "code": "HTTPException" }, "timestamp": "2024-01-15T10:30:00.000Z"}Send the body with a JSON Content-Type (e.g. application/json). Without it the body is ignored, and every required field (concept, amount, customerName, isTest) is reported as Required in error.issues.
HTTP Status Codes
Section titled “HTTP Status Codes”| Code | Description |
|---|---|
200 | OK - Request successful |
201 | Created - Resource created successfully |
202 | Accepted - Request accepted; final result delivered asynchronously (e.g. a charge still settling — see the Integration API) |
400 | Bad Request - Invalid request data or validation error |
401 | Unauthorized - Missing X-API-Key header, or invalid JWT |
403 | Forbidden - Account suspended, project inactive, or insufficient permissions |
404 | Not Found - Resource not found, or unknown API key (an invalid API key returns 404, not 401) |
409 | Conflict - The resource is busy (e.g. cancelling a link whose payment is being confirmed) |
429 | Too Many Requests - Rate limit exceeded (plain-text body, see Rate Limiting) |
500 | Internal Server Error - Server error |
502 | Bad Gateway - Service temporarily unavailable |
503 | Service Unavailable - Service maintenance or overload |
504 | Gateway Timeout - Request timeout |
Error Codes
Section titled “Error Codes”Authentication Errors
Section titled “Authentication Errors”| Code | HTTP | Description |
|---|---|---|
ERR_API_KEY_MISSING | 401 | The X-API-Key header is missing (message "Missing X-API-Key header") |
ERR_ACCOUNT_API_KEY_NOT_FOUND | 404 | Unknown, regenerated or deleted API key (message "Api key not found") |
ERR_ACCOUNT_NOT_FOUND | 404 | The key’s account no longer exists |
ERR_ACCOUNT_SUSPENDED | 403 | Account is suspended |
ERR_PROJECT_NOT_FOUND | 404 | Project-scoped key whose project no longer exists |
ERR_PROJECT_INACTIVE | 403 | Project-scoped key whose project is not active |
ERR_API_KEY_NOT_PROJECT_SCOPED | 403 | An account-level key was used on /v1/products, /v1/customers, /v1/subscriptions, /v1/collections or /v1/charges |
ERR_UNAUTHORIZED | 401 | Invalid JWT token (dashboard endpoints) |
Validation Errors
Section titled “Validation Errors”An invalid body or query parameter (a missing concept, an amount of 0, negative, non-integer or above 1000000, invalid metadata, an expiresInDays outside 1-60, a lim above 100, an unknown body field…) returns 400 without an error code. error.issues lists the failing fields: path names the field, message explains the problem and code is the kind of check that failed (invalid_type, too_small, too_big, unrecognized_keys, custom…). Unknown body fields are rejected with one unrecognized_keys issue whose path is [] and whose keys lists the unknown field names; unknown query parameters are ignored. The cross-field check (customerEmail together with requestCustomerEmail: true) is usually reported together with the other issues, but some field errors stop it from running (for example a missing field or an invalid paymentMethods value), so it can appear on a later request after you fix them.
An amount from 1 to 99 cents passes this validation and is rejected afterwards with the coded ERR_AMOUNT_BELOW_NMI_MINIMUM error (see Business Logic Errors).
{ "success": false, "error": { "issues": [ { "code": "too_small", "minimum": 0, "type": "number", "inclusive": false, "exact": false, "message": "Amount must be a positive integer", "path": ["amount"] } ], "name": "ZodError" }}Business Logic Errors
Section titled “Business Logic Errors”| Code | HTTP | Description |
|---|---|---|
ERR_NO_ACTIVE_PAYMENT_PROVIDER | 400 | No enabled payment provider for the link’s environment: an isTest: true link needs an enabled sandbox provider, an isTest: false link an enabled production provider |
ERR_INACTIVE_PAYMENT_METHOD | 400 | Specified payment method is not active on the account for the link’s environment |
ERR_AMOUNT_BELOW_NMI_MINIMUM | 400 | amount is between 1 and 99 cents. Message: "El monto mínimo para un link de pago es 1.00 USD (100 centavos); no se puede generar el link de pago". Checked after the provider and payment-method checks. A project-scoped key also gets a payment_link.rejected webhook |
ERR_INVALID_FIELDS | 400 | The fields query parameter names an unknown column (see Field Selection) |
ERR_INVALID_RELATIONS | 400 | expand names a relation the endpoint does not expand (payment links have none; charges accept customer and product) |
HTTPException | 400 | The request body is not valid JSON (message "Malformed JSON in request body") |
ERR_PAYMENT_LINK_NOT_FOUND | 404 | Payment link does not exist, or the API key cannot reach it (another account’s link or, with a project key, a link outside the project) |
ERR_INVALID_STATE | 400 | Payment link is in a state that doesn’t allow the operation |
ERR_PAYMENT_IN_PROGRESS | 409 | A payment for the link is being confirmed (link status processing), so it cannot be cancelled yet |
ERR_INTERNAL_SERVER_ERROR | 500 | Unexpected server error (message "Internal Server Error") |
A 429 rate-limit response has no error code: its body is the plain text Too many requests, please try again later. (see Rate Limiting).
Public Endpoints
Section titled “Public Endpoints”Base Path: /v1/payment-links
Section titled “Base Path: /v1/payment-links”All public endpoints require the X-API-Key header and are rate-limited to 60 requests per 60-second window per API key.
Payment Links Management
Section titled “Payment Links Management”| Method | Endpoint | Description |
|---|---|---|
GET | /v1/payment-links | List payment links |
GET | /v1/payment-links/{hashUrl} | Retrieve a single payment link |
POST | /v1/payment-links | Create a payment link |
PATCH | /v1/payment-links/{hashUrl}/cancel | Cancel a payment link |
| Parameter | Type | Description |
|---|---|---|
lim | integer | Page size, 1-100 (default 10) |
off | integer | Offset, non-negative (default 0) |
Example
Section titled “Example”curl -X GET "https://api-pay.zelta.dev/v1/payment-links?lim=10&off=20" \ -H "X-API-Key: your-api-key-here"Field Selection
Section titled “Field Selection”GET /v1/payment-links accepts a fields parameter that limits which columns are read. It is not supported on other endpoints.
curl -X GET "https://api-pay.zelta.dev/v1/payment-links?fields=id,hashUrl,amount,status,expiresAt" \ -H "X-API-Key: your-api-key-here"- Values are the payment link’s column names in camelCase (
id,hashUrl,amount,status,concept,customerName,customerEmail,expiresAt,allowedPaymentMethods…), not the response field names. A response name such aspaymentLinkUrlorpaymentMethodsreturns400 ERR_INVALID_FIELDS. - Fields you don’t select are left out of each item.
paymentLinkUrlis always present, and ends in/undefinedunless you selecthashUrl.- Unless you select both
expiresAtandstatus, the list does not mark overdue links asexpired(see Status Values). expandis not supported: any value returns400 ERR_INVALID_RELATIONS.
Data Types
Section titled “Data Types”Amounts
Section titled “Amounts”All monetary amounts are specified in cents (the smallest currency unit):
100= $1.002500= $25.0050000= $500.00
Timestamps
Section titled “Timestamps”All timestamps are in ISO 8601 format with UTC timezone:
2024-01-15T10:30:00.000ZStatus Values
Section titled “Status Values”Payment links can have these statuses:
pending- Waiting for paymentprocessing- A payment is being confirmed (with PayPal this can last up to several days while PayPal reviews the payment)completed- Payment successfulcancelled- Manually cancelledexpired- Past itsexpiresAtand stored as expired
Links are not expired proactively and no webhook is sent on expiry. A pending link past its expiresAt is stored as expired the first time a list call (GET /v1/payment-links or the dashboard) returns it on a page; that same response still shows it as pending, and later reads show expired. Until then GET /v1/payment-links/{hashUrl} keeps returning pending, a status=pending filter can return overdue links and status=expired can miss them. To know whether a link can still be paid, compare expiresAt with the current time: payment attempts after expiresAt are rejected (“Payment link has expired”).
Payment Methods
Section titled “Payment Methods”Payment links can offer these payment methods (the paymentMethods field). Each must be active on your account, configured in the dashboard sidebar under Métodos de pago (admin and owner roles only), for the link’s environment (sandbox or production):
| Method | Provider | Customer experience | Available on |
|---|---|---|---|
card | Your card processor (NMI or Emetec) | Credit/debit card form | All links |
yappy | Yappy | Pay with Yappy | One-time payment links |
paypal | PayPal | Pay with a PayPal account, or with a debit/credit card through PayPal’s guest checkout (no PayPal account needed) | One-time payment links |
Webhooks report the provider in transaction.paymentProvider (nmi, emetec, yappy, paypal) and the method in transaction.paymentMethod (credit_card, yappy, paypal). See Webhook Events.
Metadata
Section titled “Metadata”Payment links support custom metadata for storing application-specific information.
Metadata Constraints
Section titled “Metadata Constraints”- Keys: 1-40 characters (any characters)
- Values: String (max 255), number, boolean, or null
- Maximum keys: 20 per payment link
- Maximum total size: 8,192 bytes when JSON serialized
Metadata Example
Section titled “Metadata Example”{ "metadata": { "orderId": "ORD-001", "customerId": 12345, "isVip": true, "notes": null }}Pagination
Section titled “Pagination”List endpoints support pagination using query parameters:
| Parameter | Type | Description |
|---|---|---|
lim | integer | Items per page, 1-100 (default 10; a value above 100 returns 400) |
off | integer | Offset from start, non-negative (default 0) |
Results come newest first (createdAt descending). The response carries the page and the number of matching items, { paymentLinks, total }. To fetch every link, keep requesting pages with off increased by the page size until you have total items:
let off = 0;const all = [];while (true) { const res = await fetch(`https://api-pay.zelta.dev/v1/payment-links?lim=100&off=${off}`, { headers: { 'X-API-Key': process.env.ZELTA_PAY_API_KEY } }); const { data } = await res.json(); all.push(...data.paymentLinks); off += data.paymentLinks.length; if (data.paymentLinks.length === 0 || off >= data.total) break;}Example
Section titled “Example”curl -X GET "https://api-pay.zelta.dev/v1/payment-links?lim=10&off=20" \ -H "X-API-Key: your-api-key-here"Webhooks
Section titled “Webhooks”Event Types
Section titled “Event Types”Payment link events:
payment.success- A customer completed a paymentpayment.failed- A payment attempt ended without payment (decline, error, cancelled or expired wallet payment)receipt.created- The PDF receipt of a payment was generatedwebhook.ping- Sent when you test your webhook endpoint from the dashboardpayment_link.rejected- Only for links requested with a project-scoped key: the link was refused becauseamountis under 100 cents
Integration projects also receive customer.created, charge.succeeded, charge.failed, charge.deferred, charge.unknown, subscription.*, invoice.paid and invoice.payment_failed. See Webhook Events for every payload.
There is no per-event subscription: the webhook receives every event of its account (or project). Ignore the types you don’t handle.
Webhook Security
Section titled “Webhook Security”-
Public HTTPS only - HTTP URLs, internal hostnames and private IP addresses are rejected
-
Signature verification - All webhooks include HMAC-SHA256 signatures
-
Timestamp validation - Prevent replay attacks
-
Idempotency support - Use
eventId(a UUID, also sent as theZeltapay-Event-Idheader) to prevent duplicate processing -
Delivery: HTTPS POST; failed deliveries are retried up to 5 times, 15 seconds apart (see Delivery & Retries)
-
Security: HMAC-SHA256 signature verification
-
Idempotency: Event ID prevents duplicate processing
SDKs and Libraries
Section titled “SDKs and Libraries”While Zelta Pay provides a REST API, you can use any HTTP client:
JavaScript/Node.js
Section titled “JavaScript/Node.js”const fetch = require('node-fetch');// orconst axios = require('axios');Python
Section titled “Python”import requests# orimport httpx// Built-in cURL// or Guzzle HTTPimport "net/http"// orimport "github.com/go-resty/resty/v2"Testing
Section titled “Testing”Test Mode
Section titled “Test Mode”Use isTest: true when creating payment links for testing:
{ "concept": "Test Payment", "amount": 100, "customerName": "Test User", "isTest": true}Support
Section titled “Support”Documentation
Section titled “Documentation”- Quick Start Guide - Get started in 5 minutes
- Authentication Guide - API key management
- Webhook Guide - Real-time notifications
- Use Cases - Real-world integration examples
Contact
Section titled “Contact”- Email: support@zelta.dev
Changelog
Section titled “Changelog”For detailed version history and API changes, see the complete changelog.
Version 1.3.0 (Current)
Section titled “Version 1.3.0 (Current)”- Optional
expiresInDays(integer, 1-60; default 3 days) onPOST /v1/payment-links
Version 1.2.0
Section titled “Version 1.2.0”- PayPal payment method (
"paypal"): PayPal wallet and guest card checkout, one-time payment links only paypalvalue for thepaymentProviderlist parameter, andprocessingvalue for thestatusfilter- Cancel returns
409 ERR_PAYMENT_IN_PROGRESSwhile a payment is being confirmed
Version 1.1.0
Section titled “Version 1.1.0”- Payment method selection (
paymentMethodsparameter) - Request customer email flow (
requestCustomerEmailparameter) paymentProviderquery parameter on the list endpoint. It is accepted (an unknown value returns400) but currently ignored for API-key requests: the list andtotalare not filtered by provider
Version 1.0.0
Section titled “Version 1.0.0”- Initial API release
- Payment link CRUD operations
- Webhook support
- Rate limiting
- Metadata support
Next Steps
Section titled “Next Steps”- Payment Links API - Full endpoint reference
- Webhooks - Real-time payment notifications
- Quick Start - Your first payment link
- Changelog - Version history and API changes