Tu primer cobro en sandbox
En sandbox el cobro pasa por un simulador de proveedores: recorres el flujo completo, con webhooks incluidos, sin mover dinero real.
Lo que necesitas
| Qué | Formato | De dónde sale |
|---|---|---|
| URL base de la API | https://api.pagos.example.com | la que te entregamos al darte acceso |
| API key sandbox | pay_sb_<prefijo>_<secreto> | se entrega una vez al habilitar tu aplicación; después puedes crear más con POST /v1/admin/api-credentials |
| Id de tu comercio | mrc_… | viene junto con tu acceso |
BASE=https://api.pagos.example.com
KEY=pay_sb_…
MERCHANT=mrc_…
Todas las llamadas llevan Authorization: Bearer $KEY. Las que crean o cambian algo (crear intent,
pedir checkout, devolver) llevan además un Idempotency-Key único por operación: ver
Idempotencia.
1. Crear el payment intent
curl -s -X POST "$BASE/v1/payment-intents" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H 'Content-Type: application/json' \
-d '{
"merchant_account_id": "'"$MERCHANT"'",
"external_reference": "order-12345",
"amount": 25990,
"currency": "CLP",
"description": "Compra 12345",
"provider": "auto",
"return_url": "https://consumer.example.com/pagos/retorno",
"metadata": { "canal": "web" }
}'
{
"id": "payi_01…",
"status": "CREATED",
"amount": 25990,
"currency": "CLP",
"external_reference": "order-12345",
"created_at": "2026-09-26T18:00:00.000Z"
}
2. Pedir el checkout
curl -s -X POST "$BASE/v1/payment-intents/payi_01…/checkout" \
-H "Authorization: Bearer $KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H 'Content-Type: application/json' \
-d '{ "provider": "auto" }'
{
"payment_intent_id": "payi_01…",
"status": "REQUIRES_ACTION",
"provider": "transbank_webpay_plus",
"checkout": {
"type": "REDIRECT",
"url": "https://…",
"method": "POST",
"fields": { "token_ws": "…" }
},
"expires_at": "2026-09-26T18:10:00.000Z"
}
3. Redirigir al comprador
Lo que haces depende de checkout.method:
POST(Webpay Plus): arma un formulario con losfieldsy envíalo acheckout.url.GET(Mercado Pago): redirige el navegador acheckout.url.
<form id="pago" method="POST" action="{{checkout.url}}">
<input type="hidden" name="token_ws" value="{{checkout.fields.token_ws}}" />
</form>
<script>document.getElementById('pago').submit();</script>
Al terminar, el comprador vuelve a tu return_url con ?payment_intent_id=payi_… (y, en Mercado
Pago, también &status=…). No des el pago por bueno por el retorno: usa el webhook o consulta el
estado. En sandbox eliges el resultado en la página del simulador: ver Sandbox.
4. Enterarte del resultado
Registra una URL de tu sistema para recibir los eventos (una vez por aplicación). El secreto para verificar la firma se muestra solo en esta respuesta:
curl -s -X POST "$BASE/v1/admin/webhook-endpoints" \
-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{ "url": "https://consumer.example.com/webhooks/pagos" }'
# → { "success": true, "data": { "id": "whe_…", "url": "…", "events": [], "secret": "a3f1…", … } }
Cada evento llega firmado. Verifícalo como se explica en Webhooks. También puedes consultar el estado en cualquier momento:
curl -s "$BASE/v1/payment-intents/payi_01…" -H "Authorization: Bearer $KEY"
{
"id": "payi_01…",
"status": "AUTHORIZED",
"provider": "transbank_webpay_plus",
"amount": 25990,
"currency": "CLP",
"payment": { "id": "pay_01…", "status": "AUTHORIZED", "authorization_code": "1213", "authorized_at": "…" },
"timeline": [
{ "type": "payment_intent.created", "at": "…" },
{ "type": "payment.checkout.started", "at": "…" },
{ "type": "payment.authorized", "at": "…" }
]
}
Lo mismo en tu lenguaje
- Node.js
- Python
- Go
import { randomUUID } from 'node:crypto';
const BASE = 'https://api.pagos.example.com';
const KEY = process.env.PAGOS_API_KEY;
async function call(method, path, body) {
const res = await fetch(`${BASE}${path}`, {
method,
headers: {
Authorization: `Bearer ${KEY}`,
'Content-Type': 'application/json',
...(method === 'POST' ? { 'Idempotency-Key': randomUUID() } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message} (${data.error.request_id})`);
return data;
}
const intent = await call('POST', '/v1/payment-intents', {
merchant_account_id: process.env.PAGOS_MERCHANT,
external_reference: 'order-12345',
amount: 25990,
currency: 'CLP',
provider: 'auto',
return_url: 'https://consumer.example.com/pagos/retorno',
});
const { checkout } = await call('POST', `/v1/payment-intents/${intent.id}/checkout`, { provider: 'auto' });
// checkout.method === 'POST' → formulario con checkout.fields; 'GET' → redirect a checkout.url
import os, uuid, requests
BASE = "https://api.pagos.example.com"
HEADERS = {"Authorization": f"Bearer {os.environ['PAGOS_API_KEY']}"}
def post(path, body):
r = requests.post(f"{BASE}{path}", json=body, timeout=15,
headers={**HEADERS, "Idempotency-Key": str(uuid.uuid4())})
data = r.json()
if not r.ok:
e = data["error"]
raise RuntimeError(f"{e['code']}: {e['message']} ({e['request_id']})")
return data
intent = post("/v1/payment-intents", {
"merchant_account_id": os.environ["PAGOS_MERCHANT"],
"external_reference": "order-12345",
"amount": 25990,
"currency": "CLP",
"provider": "auto",
"return_url": "https://consumer.example.com/pagos/retorno",
})
checkout = post(f"/v1/payment-intents/{intent['id']}/checkout", {"provider": "auto"})["checkout"]
package pagos
import (
"bytes"
"crypto/rand"
"encoding/json"
"fmt"
"net/http"
"os"
)
const base = "https://api.pagos.example.com"
func newKey() string {
b := make([]byte, 16)
_, _ = rand.Read(b)
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
}
func post(path string, body, out any) error {
payload, _ := json.Marshal(body)
req, _ := http.NewRequest(http.MethodPost, base+path, bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+os.Getenv("PAGOS_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", newKey())
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode >= 300 {
var e struct{ Error struct{ Code, Message, RequestID string `json:"request_id"` } }
_ = json.NewDecoder(res.Body).Decode(&e)
return fmt.Errorf("%s: %s (%s)", e.Error.Code, e.Error.Message, e.Error.RequestID)
}
return json.NewDecoder(res.Body).Decode(out)
}
func Cobrar() (map[string]any, error) {
var intent struct{ ID string `json:"id"` }
err := post("/v1/payment-intents", map[string]any{
"merchant_account_id": os.Getenv("PAGOS_MERCHANT"),
"external_reference": "order-12345",
"amount": 25990,
"currency": "CLP",
"provider": "auto",
"return_url": "https://consumer.example.com/pagos/retorno",
}, &intent)
if err != nil {
return nil, err
}
var checkout map[string]any
err = post("/v1/payment-intents/"+intent.ID+"/checkout", map[string]any{"provider": "auto"}, &checkout)
return checkout, err
}
Si una llamada falla por red o timeout, reintenta con el mismo Idempotency-Key: obtendrás la
misma respuesta sin crear nada dos veces. En los ejemplos se genera uno nuevo por llamada para
simplificar; en producción guárdalo junto a tu orden.
Listar pagos
curl -s "$BASE/v1/payments?status=AUTHORIZED&limit=20" -H "Authorization: Bearer $KEY"
# → { "items": [ … ], "next_cursor": "…" } — pasa next_cursor en ?cursor= para la página siguiente
Filtros: status, from y to (fechas ISO 8601), cursor y limit (entre 1 y 100, por defecto 20).
Siguientes pasos
- Flujos de pago para entender cada proveedor.
- Devoluciones.
- Errores y referencia de API.