Skip to main content

Payment Links

Payment links let you collect payments without building a custom checkout. Create a link, share it via any channel, and the buyer completes payment through the hosted Wava checkout page.

Use cases

  • Invoicing: Send a payment link with a fixed amount for an invoice.
  • Social commerce: Share links via WhatsApp, Instagram, or email.
  • Donations: Create dynamic links where the buyer enters the amount.
  • Recurring manual payments: Generate a new link for each billing cycle.

Request fields

Response

expires_at is the UTC timestamp when the link expires, or null if no ttl_minutes was provided. wallet_id echoes the wallet the link was bound to, or null when it was not bound to one. expires_at and wallet_id are present when the request actually created a link. They are not returned when the request was answered with an already-existing link — but that is incidental to how that code path builds its response, not a contract you can rely on to detect reuse. See Order key semantics. hash is the link identifier embedded in the URL. It is returned to requests authenticated with a merchant-key; other callers may get a response without it. Use link as the shareable URL. Share the link with your buyer. When they open it, they see the payment amount and can select their preferred payment method (Nequi, Daviplata, Breb, or Stripe if enabled).

Order key semantics

order_key is the identifier of the order on your side, not a free-form reference field. One order_key = one order = one link = one payment. Wava treats it as an idempotency key, scoped to (store, order_key). Sending the same order_key again does not reliably create a new link — that is the anti-double-charge guarantee, not a side effect. Design your integration around it. Wava looks up the most recently created link of that store carrying that order_key, and applies this policy:

Can you tell reuse from creation?

Not reliably, and you should not build on it. Both cases answer 200, and no field says “reused”. On POST /v1/links the two responses do happen to differ today — the reuse path returns only link (plus hash, when hash is returned at all) and omits expires_at and wallet_id, which a fresh creation always includes:
That difference is incidental, not a contract. It is a by-product of how the two code paths build their response, it is not part of the documented API, and it can disappear without notice. Note also that both fields are frequently null on a genuine creation (no ttl_minutes, no wallet binding), so any check would have to test for presence of the key rather than for a value — which is exactly the kind of brittle inference you should not ship.On POST /links/tiendanube there is no difference at all: reuse and creation return byte-identical bodies (201 {"redirect_url": "..."} on integration version v2, {"result": {"link": "..."}} on v1).Design your integration so it does not need to know. Treat a 200 as “this order_key now has exactly one link, and here it is.”
A dedicated 409 PAYMENT_LINK_ALREADY_EXISTS for the reuse cases is planned behind the LINKS_CONFLICT_409_ENABLED feature flag. It is not live today — do not write code that depends on it.

Use one order_key per buyer

For concurrent buyers, generate a unique order_key per buyer, per order. Never key it on a product, a SKU, or an event. The mistake happens most often with a single store selling the same item at the same amount to many people at once — race registrations for the same distance are the canonical case:
The first runner creates the link. Once their payment is confirmed, every later request for race-10k gets that same link back instead of a new one — the runners behind them end up paying into an order that is already closed, or cannot pay at all.
Omitting order_key does not fix this. Links are stored keyed on the store plus a hash of the request body, so two buyers sending an identical body — same amount, same description, no order_key — resolve to the same link. Because order_key is part of the hashed body, a unique order_key per buyer is what makes each request produce its own link.

What order_key does not guarantee

  • It is not the link’s hash, and the hash cannot be derived from it. The hash is computed from the stored request body as a whole — order_key is only one of the fields in it. Treat the hash and the returned link as opaque: never build, guess, or reconstruct a checkout URL from an order_key.
  • It is not enforced as unique by storage. Uniqueness comes from the creation policy above, applied at request time.
  • It is scoped to a single store. The same order_key under two different merchants refers to two unrelated links.
  • It is optional. Omit it only for links genuinely meant to be paid by many different buyers (donations, tip jars) — and read the note above about identical bodies before you do.
Use ttl_minutes to create time-limited links:
Once a link passes its expires_at timestamp:
  • The Wava checkout page returns a 410 error with code PAYMENT_LINK_EXPIRED.
  • Attempting to place an order on the link is blocked.
  • The error response includes redirect URLs (same fields as PAYMENT_LINK_INACTIVE) so you can send buyers to a custom expiration page.
In-flight payments are honored: if a buyer starts payment before the link expires and the gateway confirms afterward, the payment is accepted. Expiration only blocks new payment attempts.

Redirect URLs

After the buyer completes (or cancels) the payment, they are redirected to the URL you specified: All redirect fields are optional. If omitted, the buyer stays on the Wava checkout confirmation page.

Routing payments to a wallet

A wallet is the merchant’s balance account inside Wava — where their money sits until it is paid out. (Not to be confused with Nequi or Daviplata, which are also called digital wallets; those are how the buyer pays.) Most merchants have one wallet per currency and can ignore this section entirely. Merchants who have multiple wallets enabled can route a link’s payments to a specific one by passing wallet_id when creating it:
Behaviour:
  • Omitted, or null — payments credit the merchant’s default wallet for the link’s currency. This is the default and what happens for every merchant with a single wallet.
  • A wallet the merchant does not own (or one that does not exist) — rejected with 404 WALLET_NOT_FOUND.
  • A wallet whose currency differs from the link’s — rejected with 400 CURRENCY_MISMATCH.
  • A wallet that is suspended or closed — rejected with 422 WALLET_NOT_ACTIVE. Only active wallets can receive payments.
  • Passed by a merchant without multiple wallets enabled — ignored silently, and the response echoes wallet_id: null. The payment credits their only wallet, so nothing is lost; check the response if you need to know whether the binding took effect.
The binding is durable: once a link is bound, every payment on it credits that wallet. The link_paid webhook reports it back as wallet_id — see Webhook Events.
Payment links created from the Wava merchant dashboard (app.wava.co) do not trigger webhook notifications. Webhooks are only sent for payment links created via the API. If you need webhook notifications for payments, create links programmatically using this endpoint.

Error codes