hostedPaymentUrl you forward to your customer. Your customer signs in with email magic-link, completes KYC if they haven’t already, and pays. You learn about every state transition through outbound webhooks.
When to use this vs. invoices
POST /v1/payment-intents— your CMS owns the invoice; Caratuva is the payment + compliance + settlement engine. Skips the seller-side approval step (the API call is the approval).POST /v1/invoices— Caratuva’s dashboard owns the invoice. Sellers create it, a teammate approves it if your organization requires approval, and the platform sends the magic-link. Use this if you don’t have your own invoicing system.
Prerequisites
- KYB approved. Your organization must have completed Know-Your-Business onboarding and have a virtual account provisioned. The API returns
400 KybNotApprovedotherwise. - API key. A live-mode key (
pk_live_...) — see API keys. - Webhook subscription. You’ll want one before creating intents in production — see Webhooks.
Create a payment intent
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
externalId | string (1–200) | yes | Your primary key. Unique per (organization, externalId). Re-POSTing the same value returns the existing intent — safe under network retry. |
amount | decimal string | yes | Two-decimal precision; pass as a string to avoid float drift ("12500.00", not 12500). **Minimum US10 floor, so a smaller USD amount can’t be paid and is rejected with AmountBelowMinimum. |
currency | ISO 4217 code | no | Defaults to USD. |
buyer.email | string | yes | Real address — the buyer receives a magic-link sign-in to this address. |
buyer.name, buyer.country, buyer.phone | string | no | Pre-fills the KYC form; the buyer can amend. |
returnUrl | URL | no | Where the hosted page redirects after settlement. |
display.title, display.notes, display.lineItems | varies | no | Rendered on the hosted page. Free-text — Caratuva does not validate NCM or origin codes here. |
metadata | object | no | Round-tripped verbatim on every webhook. Never shown to the buyer. |
expiresAt | RFC 3339 | no | Intent transitions to expired if not paid by this time. |
Idempotency
SendIdempotency-Key: <uuid> on every retry of the same logical create. Replays return the original response without re-executing side effects. Two distinct keys with the same externalId collapse to the same intent (per-org externalId is itself unique).
Response
hostedPaymentUrl is the only field most integrators need from the response. Forward it to your customer over your own channel (email, SMS, in-app message). Caratuva does not automatically email the buyer when a payment intent is created — that is the integrator’s responsibility on this surface, because you already own the customer relationship.
Buyer journey on the hosted page
When the buyer clickshostedPaymentUrl, they:
- Land on the buyer portal (
pay.caratuva.com/r/<publicId>) and see yourdisplayblock plus your seller name. - Enter their email; receive a magic-link; sign in (Caratuva-managed session).
- Complete buyer KYC if their
kycStatusis not yetapproved. The KYC step is mandatory — there is no fast-forward path. A returning buyer who’s already verified in another integrator’s flow reuses their KYC and skips this step. - Confirm the FX quote and complete payment.
- Caratuva runs the cross-border transfer and pays out BRL via PIX to the seller’s registered destination.
- Buyer is redirected to
returnUrlif set; otherwise sees a Caratuva confirmation page.
State machine
API-created intents skip seller-side approval (awaiting_approval / approved) and start at awaiting_buyer. From there:
failed, cancelled, expired.
Read an intent
Cancel an intent
Only valid before the BRL payout begins. Aftersettled, cancel the order in your own system instead.
Errors
| Status | error | Cause |
|---|---|---|
| 400 | KybNotApproved | Your org has no virtual account. Complete KYB first. |
| 400 | AmountBelowMinimum | The USD amount is below the US$10.00 bank-transfer funding minimum. Increase it. |
| 400 | ValidationError | Zod schema rejected the body. The message field lists the offending paths. |
| 401 | InvalidApiKey | Key is malformed, unknown, or revoked. |
| 401 | InvalidApiKeyFormat | Header is not pk_(test|live)_<id>.<secret>. |
| 409 | IdempotencyKeyConflict | The Idempotency-Key was previously used for a non-payment-intent invoice. Use a fresh key. |
| 429 | RateLimitExceeded | Per-API-key limit on POST /v1/payment-intents. Back off and retry. |
{ statusCode, error, message }.