holdFunds: true.
Overview
Holds are useful whenever you need to know the money is there before you commit:- Deposits: hold a security deposit for equipment or vehicle rentals, then release it when the item comes back intact
- Bookings and reservations: hold the amount when a reservation is made, capture it at check-in or check-out
- Verifying funds before fulfilment: hold the order total, confirm stock or availability, then capture only if you can actually fulfil it
- Security holds: block funds against damages or overages, and give them back when nothing was owed
Hold Lifecycle
A hold is a payment like any other, with its own status:
The path a hold can take:
- Place the hold:
POST /api/payment-method/{id}/paymentwithholdFunds: true. On approval the payment becomesHELD. - Then exactly one of:
- Capture:
POST .../capturetakes the money, all of it or part of it. The payment becomesCONFIRMED, exactly like a regular charge. You get one capture per hold. See Capturing a Hold. - Release:
POST .../releasegives the money back. The payment becomesHOLD_RELEASED. - Expiry: you do nothing until the window closes. Compago releases the hold and the payment becomes
HOLD_EXPIRED.
- Capture:
HOLD_RELEASED and HOLD_EXPIRED are terminal. A released hold cannot be captured, and it cannot be refunded either (there is nothing to refund). To charge the customer after a release, place a new hold or a regular charge.
A hold the bank declines does not become a hold. It is recorded as a
CANCELLED payment, and the API responds with 402. Nothing is reserved on the card.Prerequisites
Before placing a hold:- The payment method must have a status of
ACTIVE(the customer has saved their card) - You must have the payment method
id - The card must not be expired or removed by the customer
- Your API key needs the payment method
chargepermission to place a hold, and thecaptureandreleasepermissions to finish it
Placing a Hold
These examples call the production host. While you build, replace
https://api-harmony.compago.com with https://demo-api-harmony.compago.com and use a key from the Demo dashboard. See Environments.Endpoint
POST /payment-method/{id}/payment
Authentication
Include your API key in the request headers:Path Parameters
string
required
The unique identifier of the payment method to hold funds on (UUID).
Request Body
Field Descriptions
Response
heldAt above is 2025-03-04 10:20 in Mexico City, so the placement day is Tuesday 4 March. Six calendar days later is Monday 10 March, and the window closes at 23:00 that day, which is 2025-03-11T05:00:00.000Z in UTC.
These hold fields are present on every payment response for a saved card, including regular charges and lookups. On a regular charge they are
null (and holdExpired is false), so you can read the same shape everywhere.Code Examples
Capturing a Hold
Capture turns the held funds into a real charge. Do this at checkout, at check-in, when the order ships, or whenever you have decided to keep the money. You can capture the whole held amount or only part of it. Capturing in full is the default: send no request body and the full held amount is taken.Endpoint
POST /payment-method/{id}/payment/{paymentId}/capture
Path Parameters
string
required
The unique identifier of the payment method (UUID).
string
required
The unique identifier of the held payment to capture (UUID).
Request Body
The body is optional. Send no body at all and the hold is captured in full, which is what an integration that never asks for a partial capture should keep doing. To capture part of the hold, send the amount you want to take:
The bounds are simply
0 < amount <= heldAmount:
- Capturing less than you held is allowed, and it is the point of the field. The customer is charged less than the amount that was blocked.
- Capturing more than you held is impossible. It is refused with 400
CAPTURE_AMOUNT_EXCEEDS_HOLDbefore the bank is contacted. To take more, place a separate charge. - Sending
amountequal toheldAmountis identical to sending no body. 0, a negative number, or a non-numeric value is refused with 400INVALID_CAPTURE_AMOUNT.- The 1 MXN minimum that applies to a charge does not apply to a capture. A capture is a settlement instruction against an authorization that already cleared that floor, so small captures are accepted.
Capturing Part of a Hold
A partial capture rewrites the payment’samount to the amount you captured, and heldAmount keeps the amount the bank originally authorized. Hold 200 MXN, and the payment reports:
The rule, in one line: the authorization is
heldAmount, the money that moved is amount, never the reverse.
heldAmount is returned by every saved-card payment endpoint (the hold itself, capture, release, list, and detail). It is also on the payments list and payment detail in the Compago dashboard, and in the payments CSV export as the Monto retenido column. It is null on any payment that was never held.Response
amount and heldAmount agree. A captured hold ends up as a CONFIRMED payment, indistinguishable from a regular charge apart from its heldAmount and its heldAt and capturedAt timestamps. holdDaysRemaining becomes null because the payment is no longer HELD.
A captured hold’s refund window runs from the day it was captured, not from the day it was placed. A hold placed on Monday and captured on Sunday is refundable through Sunday’s bank cutoff, six days later than you might expect. Refunding a partially captured payment returns the captured amount (
amount), not the held one. See Process Refunds.Code Examples
Capture is safe to call twice. The second call does not reach the bank: it sees the payment is no longer
HELD and answers 409. That means a 409 is not proof of failure, it can also mean your first call already succeeded. Read the payment back to find out which.Releasing a Hold
Release gives the money back before the window closes. Use it as soon as you know you will not be charging the customer: the funds stay unavailable to them until the release reaches their bank.Release has no time cutoff. Unlike capture, release is accepted at any time while the payment is
HELD, including after 23:00 on the last day and after the window has closed. Releasing an expired hold still works: you are giving money back, not taking it, so there is no race with the bank to avoid. A hold you release yourself ends as HOLD_RELEASED, even if the automatic sweep would have reached it minutes later.Endpoint
POST /payment-method/{id}/payment/{paymentId}/release
Path Parameters
string
required
The unique identifier of the payment method (UUID).
string
required
The unique identifier of the held payment to release (UUID).
Response
Code Examples
A release never checks the card. Even if the customer removed the card or it expired since the hold was placed, the money still goes back.
Holds From a Saved-Card Link
A hold does not have to start from a card you already have. A card-saving link created withcollectFunds: "HOLD" blocks the money at the moment the customer enters their card, so one link both saves the card and holds the amount, instead of saving the card first and calling this endpoint afterwards.
What the link produces is an ordinary held payment, so everything on this page applies to it unchanged:
- It is
HELD, withheldAt,holdExpiresAt, and the same 7 natural day window. - It is captured through the same capture endpoint, partial capture included.
- It is released through the same release endpoint, and it expires the same way if nobody acts.
amount you set on the link, which is the number the customer saw on the checkout page. Capturing less than that is fine, the customer simply ends up charged less than they were shown. Capturing more is impossible.
To find the payment the link created, list the payment method’s payments and filter by status:
Checking Whether a Hold Is Still Live
Both payment lookups return the hold fields, so you never have to keep the state yourself:GET /payment-method/{id}/payment/{paymentId} and GET /payment-method/{id}/payment
A hold is live, and therefore capturable, when all three of these are true:
statusisHELDholdExpiredisfalseholdReleasedAtisnull
holdDaysRemaining to drive reminders, for example a warning when it drops to 1 so an operator can decide before the window closes. Remember that 1 means “today”, not “another 24 hours”: an operator who sees 1 at 22:00 has 59 minutes left. Use holdExpiresAt when you need the exact deadline instead of whole days.
The Hold Window
A hold lives for 7 natural days. Banks count days, not elapsed hours, and Compago counts them the same way:- The day you place the hold is day 1. The clock time at which you place it is irrelevant to when it dies.
- The hold dies at the end of the 7th natural day. Place it on a Monday and the last day you can capture is Sunday.
- The bank closes its day at 23:30 local time (America/Mexico_City). Compago closes the hold at 23:00, half an hour earlier, so that a capture is never in flight while the bank is dropping the authorization.
- Your last minute to capture is 22:59 on the last day.
holdExpiresAt on every response, so you never have to compute it yourself.
Worked Example: a Hold Placed on a Monday
Three holds placed on the same Monday, almost fourteen hours apart, expire at exactly the same instant:
The time of day makes no difference to the deadline. Every instant of the placement day belongs to the same cohort: a hold placed at 00:00 and one placed at 23:59 on the same day expire together. 10:00, 23:29 and 23:59 are all the same Monday, so all three holds run out on the same Sunday, at the same 23:00.
This is deliberate. The window is standardized on whole natural days, the unit the banks themselves count in, and a hold placed late in the day gets less usable time out of its first day. That is the rule rather than an accident of it. The practical consequence is what you should build around: a hold placed at 23:59 reports
holdDaysRemaining: 6 from the moment it is placed, because its own placement day is already spent, while the one placed at 10:00 reports 7.
Counting the days out:
Automatic Release
If nobody captures the hold, Compago releases it automatically and the payment becomesHOLD_EXPIRED. The sweep that does this runs twice each night, at 23:00 and again at 23:15 America/Mexico_City. Each run releases every hold whose window has already closed.
The 23:15 run is a catch-up: if the 23:00 run is slow or fails, it picks up whatever was left, and it still lands before the bank closes its day at 23:30. When the 23:00 run did its work, the second run finds nothing to do and changes nothing.
Neither run changes what you can do. Capture is still refused from 23:00 on the last day, and 22:59 on the last day is still your last minute to capture. The second run gets the customer’s money back sooner when the first one stumbles, it does not extend anyone’s window.
A declined capture leaves the payment
HELD. You can retry the capture, release the hold yourself, or leave it to the nightly sweep.Idempotency (externalId)
POST /api/payment-method/{id}/payment accepts an optional externalId: your own key for this operation, up to 255 characters, unique across all payments in your organization. It is the supported way to make this endpoint safe to retry, and we recommend sending one on every hold.
Without it, a retry after a timeout places a second hold on the customer’s card for the full amount again. One blind retry of a 7,000 MXN of the customer’s money blocked. With it, the retry returns the hold you already placed.
Replay Behaviour
A partially captured hold still replays correctly. The replay compares the
amount you send against the amount that was authorized (heldAmount), not against the payment’s current amount. Retrying your original 200 MXN of it returns the original payment rather than a 409 mismatch, so keep sending the amount you sent the first time.Holds and regular charges share one
externalId namespace, because both are created by the same endpoint. order-1 cannot exist as a hold and also as a charge. Replaying a key with a different holdFunds value than the original returns the original payment rather than an error, so keep the flag stable across your retries.externalId on every payment response for a saved card (the hold itself, capture, release, refund, list and detail), so you can reconcile our payment id against your own order reference without keeping a mapping table.
Retrying Safely Without a Key
If you cannot send anexternalId, a timeout or a dropped connection is unknown, not failed. The bank may well have approved the hold your client never saw the response for. The safe sequence is:
1
Do not retry blindly
Treat any network timeout, gateway error, or aborted request as an unknown outcome. Never send the same hold again just because the first call did not return.
2
Look for an existing hold
Call
GET /api/payment-method/{id}/payment?status=HELD and check for a HELD payment matching the amount you sent, created around the time of your request.3
Reuse it, or retry once
If a matching hold exists, store its
id and carry on: the operation succeeded. Only if nothing matches should you send the hold again.4
Release duplicates immediately
If you find more than one hold for the same operation, release the extras with the release endpoint. Do not leave a duplicate to expire on its own: that leaves the customer’s money blocked for the rest of the window for nothing, and it is the most common complaint holds generate.
paymentId and only act on a payment that is still HELD, so a repeat call answers 409 instead of touching the card a second time. Neither endpoint accepts an externalId.
Error Handling
The API answers with the HTTP status and a plain text message. The error codes below name each failure so you can match a response with its cause and with the API reference.Placing a Hold
Capturing
Releasing
Best Practices
Hold Only What You Need
Hold Only What You Need
The held amount is the ceiling on what you can capture: you can take less, never more. But taking less does not hand the difference back promptly, because Compago issues no release for it and the customer’s issuer frees it on its own schedule. Padding a deposit “just in case” blocks money the customer cannot use. Hold the real figure, and when the final amount turns out to be far lower than the hold, consider releasing the hold and placing a charge for the correct amount instead of capturing a small part of a large one.
Release as Soon as the Answer Is No
Release as Soon as the Answer Is No
The moment a booking is cancelled or a rental comes back clean, release the hold. Waiting for the automatic expiry keeps the customer’s money blocked for up to 7 natural days and generates support tickets you could have avoided with one API call.
Store the Payment ID Immediately
Store the Payment ID Immediately
Persist the
id from the hold response before you do anything else. It is the only handle to capture or release that hold, and losing it means waiting out the full window.Reconcile Against the API, Not Your Own Timer
Reconcile Against the API, Not Your Own Timer
Do not track expiry with your own clock, and never recompute the deadline from
heldAt yourself. The natural-day rule depends on the Mexico City calendar day and on the 23:00 cutoff, so heldAt plus seven times twenty-four hours is wrong by up to a day and a half. Read holdExpired, holdExpiresAt, and holdDaysRemaining from the payment: they are computed by the same rule the automatic sweep uses, so they cannot drift apart from it.Capture Before the Final Day
Capture Before the Final Day
Both risks that can cost you the money (the 23:00 cutoff on the last day, and the issuer dropping the hold early) grow as you approach day 7. If your business process allows it, capture in the first few days. Never build a process that captures late in the evening of the last day: 22:59 is the last minute, and a retry after a network error at 22:58 may not get a second chance.
Place Holds Early in the Day
Place Holds Early in the Day
The placement day is day 1 whatever the clock says, so a hold placed at 23:59 starts life with
holdDaysRemaining: 6 while one placed that morning starts with 7. When you control the timing, for example in a nightly batch, place holds early in the day rather than in the last minutes of it, and remember that a batch straddling local midnight produces holds whose windows end a full day apart.Send an externalId on Every Charge and Hold
Send an externalId on Every Charge and Hold
It costs one field and it is the difference between a safe retry and a duplicate hold on your customer’s card. Use your own order reference, and remember that a declined attempt spends the key: a genuine second attempt at the same order needs a new one.
Next Steps
Charge a Saved Card
Charge a customer’s saved card immediately, with no hold step.
Process Refunds
Refund a captured hold, and see why a live hold must be released instead.
Manage Payment Methods
List payments, filter by hold status, and inspect the hold fields.
Payment Methods Overview
Review payment method statuses, payment statuses, and use cases.