Quickstart
Your first test payment, step by step
Run a test payment after onboarding and initial setup in the Stage environment.
- Stage: no real money
- cURL and Node.js
- Fictitious test data
Overview
Three phases, six steps
Everything below runs on Stage, the sandbox that mirrors production. Production access comes at the end, after the checks in step 6.
- 01
Set up access
Receive Stage credentials, store them in a secrets manager and allow-list your server IPs.
Steps 1–2
- 02
Make your first calls
Get a token, create a test pay-in and verify the webhook that confirms it.
Steps 3–5
- 03
Go live
Complete the security review and business verification (KYB), then switch to production credentials.
Step 6
Step 1 of 6
Get your Stage credentials
Stage credentials, an API user and password for api.stage.localpayment.com, are issued when your partnership with Localpayment starts. Treat them like production secrets from day one.
- Load credentials from a secrets manager or environment variables — never commit them
- Stage and Production use different credentials and base URLs
- Use fictitious test data only, never real customer data
// Load Stage credentials from AWS Secrets Manager at boot (AWS SDK v3).
// Any vault works: the point is that secrets never live in source code.
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
const client = new SecretsManagerClient({ region: process.env.AWS_REGION });
export async function loadMerchantSecrets() {
const { SecretString } = await client.send(
new GetSecretValueCommand({ SecretId: 'localpayment/stage/api' }),
);
// Expected JSON: { "username": "…", "password": "…", "webhookSecret": "…" }
return JSON.parse(SecretString);
}Step 2 of 6
Allow-list your server IPs
The API only accepts calls from IP addresses you have allow-listed. Add the outbound IPs of your servers in the Stage Business Portal before your first request.
- Business Portal → Whitelist your IP
- Requests from any other IP are rejected
- Serverless or autoscaling? Route outbound traffic through a fixed egress IP (NAT)
- Repeat the setup for Production before go-live
- PCI DSS v4.0.1
- SOC 2 Type II
- ISO 27001
- AML/KYC
- TLS 1.2+ · AES-256
Step 3 of 6
Get an access token
Exchange your API user and password for a JWT pair. Send the access token as a bearer token on every call, and refresh it before it expires.
- Access token: valid for 5 minutes
- Refresh token: valid for 24 hours — use POST /api/token/refresh/
- Keep tokens in memory and never write them to logs
# Get an access token on Stage (valid for 5 minutes)
curl -X POST https://api.stage.localpayment.com/api/token/ \
-H "Content-Type: application/json" \
-d "{\"username\": \"$LP_API_USER\", \"password\": \"$LP_API_PASSWORD\"}"Step 4 of 6
Create a test pay-in
This example creates a Pix pay-in in Brazil. The response returns a QR code and a Pix Copia e Cola string, with status 100 INPROGRESS until the payer completes the payment.
- Generate a fresh UUID as externalId for every transaction
- Retrying with the same externalId returns 409 Conflict instead of creating a second pay-in
- Amount in USD: the response returns the BRL amount the payer pays and the FX quote applied
# Illustrative Pix pay-in on Stage (Brazil). Fictitious values;
# see docs.localpayment.com for the full contract.
curl -X POST https://api.stage.localpayment.com/api/payin/ \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"paymentMethod": { "type": "BankTransfer", "code": "1314", "flow": "DIRECT" },
"externalId": "7c1e2f7a-0000-4000-8000-000000000001",
"country": "BRA",
"currency": "USD",
"amount": 100,
"accountNumber": "<your-account-number>",
"conceptCode": "0003",
"merchant": { "type": "COMPANY", "name": "Nortia Commerce Ltda" },
"payer": {
"type": "INDIVIDUAL",
"name": "Maria",
"lastname": "Silva",
"document": { "type": "CPF", "id": "000.000.000-00" }
}
}'Step 5 of 6
Handle the webhook
When the payment completes, Localpayment posts a signed notification to your HTTPS endpoint. Verify the signature on the raw body, answer 200 right away and do the work asynchronously.
- Register your URL in Business Portal → Configurations → Clients → Notifications
- Compare x-Signature with your own HMAC-SHA256 using a timing-safe check
- Deliveries without a 200 response are retried with exponential backoff
// Express receiver for Localpayment webhooks (Node.js 18+).
import crypto from 'node:crypto';
import express from 'express';
import { jobQueue } from './queue.js'; // your own async job queue
const app = express();
// Load the webhook secret from your secrets manager at boot. Never hardcode it.
const WEBHOOK_SECRET = process.env.LP_WEBHOOK_SECRET;
if (!WEBHOOK_SECRET) throw new Error('LP_WEBHOOK_SECRET is not configured');
function hasValidSignature(rawBody, header) {
if (typeof rawBody !== 'string') return false;
// Header format: "x-Signature: HMAC-SHA256 <hex>". Strip the prefix first.
const received = String(header ?? '').replace(/^HMAC-SHA256\s+/i, '').trim();
if (!/^[0-9a-f]{64}$/i.test(received)) return false;
const expected = crypto.createHmac('sha256', WEBHOOK_SECRET).update(rawBody, 'utf8').digest();
return crypto.timingSafeEqual(Buffer.from(received, 'hex'), expected);
}
// The body is JSON sent as Content-Type text/plain: keep the raw text for the HMAC.
app.post('/webhooks/localpayment', express.text({ type: '*/*', limit: '1mb' }), (req, res) => {
if (!hasValidSignature(req.body, req.get('x-signature'))) return res.sendStatus(401);
let event;
try {
event = JSON.parse(req.body);
} catch {
return res.sendStatus(400);
}
res.sendStatus(200); // acknowledge fast, before any heavy work
// Process asynchronously. Deduplicate on internalId/externalId + status
// so a retried delivery is applied only once. Never log the full payload.
jobQueue.enqueue(event);
});Step 6 of 6
Go-live checklist
Production credentials are issued only after these checks. Plan for them early: they help protect every merchant on the platform, including you.
Complete Stage testing
Cover success, decline, pending and error paths for every method and country you will launch.
Pass the security review
Our team reviews and certifies your integration before production access is granted.
Complete business verification
Compliance verifies your company, ownership and use case (KYB) as part of onboarding.
Store production credentials safely
Keep them in a secrets manager, separate from Stage, and limit access to the services that need them.
Allow-list production IPs
Add the outbound IPs of your production servers in the production Business Portal.
Configure production webhooks
Register your production HTTPS endpoint and confirm signature checks pass before the first live transaction.
Troubleshooting
Common first-call issues
I get 401 Unauthorized. What should I check?
Access tokens expire after 5 minutes. Refresh with POST /api/token/refresh/ or request a new token, and make sure the header reads Authorization: Bearer followed by the access token.
My request is rejected even with a valid token.
Check that the outbound IP of the calling server is allow-listed in the Business Portal for that environment. Stage and Production are configured separately.
Why did I receive 409 Conflict?
That externalId was already used. Query the existing transaction with GET /api/transactions/{externalId}/status instead of creating a new one.
The pay-in stays in 100 INPROGRESS.
That is expected for asynchronous methods such as Pix, bank transfers and virtual accounts: the status changes when the payer pays, and the final status arrives by webhook.
My webhook signature does not match.
Compute the HMAC over the raw request body exactly as received, before any JSON parsing, and strip the "HMAC-SHA256 " prefix from the header before comparing.
Need Stage credentials?
Tell us about your use case and markets. Our team will guide you through onboarding and issue Stage access when your partnership starts.

Stage: no real money