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.
Creating a payment link
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.
When a link already exists for that (store, order_key)
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 answer200, 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:
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:
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 thehashcannot be derived from it. Thehashis computed from the stored request body as a whole —order_keyis only one of the fields in it. Treat thehashand the returnedlinkas opaque: never build, guess, or reconstruct a checkout URL from anorder_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_keyunder 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.
Link expiration (TTL)
Usettl_minutes to create time-limited links:
expires_at timestamp:
- The Wava checkout page returns a
410error with codePAYMENT_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.
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 passingwallet_id when creating it:
- 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.
link_paid webhook reports it back as wallet_id — see Webhook Events.