Skip to main content
A hold (also called a pre-authorization) reserves money on a customer’s saved card without taking it. The funds leave the customer’s available balance immediately, but nothing is charged until you capture the hold. If you never capture it, the money goes back to the customer. Holds use the same charge endpoint you already know, with one extra field: 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
The typical flow is: block the funds for up to 7 natural days, then either take the money at checkout (all of it, or only part of it) or release it.
The window is counted in natural days, not in hours. The day you place the hold counts as day 1, and the time of day you place it does not change the deadline. A hold placed Monday expires on Sunday, whether you placed it at 10:00 or at 23:59. See The Hold Window for the exact rule and a worked example.

Hold Lifecycle

A hold is a payment like any other, with its own status: The path a hold can take:
  1. Place the hold: POST /api/payment-method/{id}/payment with holdFunds: true. On approval the payment becomes HELD.
  2. Then exactly one of:
    • Capture: POST .../capture takes the money, all of it or part of it. The payment becomes CONFIRMED, exactly like a regular charge. You get one capture per hold. See Capturing a Hold.
    • Release: POST .../release gives the money back. The payment becomes HOLD_RELEASED.
    • Expiry: you do nothing until the window closes. Compago releases the hold and the payment becomes HOLD_EXPIRED.
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 charge permission to place a hold, and the capture and release permissions 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

API reference: 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

The 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

API reference: POST /payment-method/{id}/payment/{paymentId}/capture
There is exactly one capture per hold, and Compago never releases the difference. These are the two rules that catch integrators out, so read them before you capture anything:
  1. One capture, ever. The moment a capture succeeds, the hold is finished. You cannot capture 200MXNtodayandtheremaining200 MXN today and the remaining 1,000 MXN tomorrow: a second capture is refused with 409 PAYMENT_NOT_HELD and there is no way back to the rest of the money. If you might need more later, capture the full amount, or agree a new charge with the customer. Multiple captures against one authorization work on some other processors. They do not work here.
  2. Compago issues no release for the remainder. Capturing 200MXNofa200 MXN of a 1,200 MXN hold does not free the other $1,000 MXN on our side: we send the bank nothing for it. The customer’s issuing bank frees it on its own schedule, usually within a few days, and neither you nor Compago can make that happen sooner. Tell the customer this when you capture less than you held, especially on large deposits.
Capture stops at 23:00 on the last day. From 23:00 America/Mexico_City on the last day of the window, capture is refused with 409 HOLD_WINDOW_CLOSED, even if the automatic release has not physically run yet. Compago refuses rather than let a capture be in flight while the bank is closing its day at 23:30. Your last minute to capture is 22:59 on the last day. See The Hold Window.

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_HOLD before the bank is contacted. To take more, place a separate charge.
  • Sending amount equal to heldAmount is identical to sending no body.
  • 0, a negative number, or a non-numeric value is refused with 400 INVALID_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’s amount to the amount you captured, and heldAmount keeps the amount the bank originally authorized. Hold 1,200MXN,capture1,200 MXN, capture 200 MXN, and the payment reports:
Read the two fields like this: The rule, in one line: the authorization is heldAmount, the money that moved is amount, never the reverse.
amount changes when you capture part of a hold. It is 1,200MXNwhilethepaymentis‘HELD‘and1,200 MXN while the payment is `HELD` and 200 MXN after a $200 MXN capture. Anything you recorded before the capture, an exported report, a notification you already sent, your own copy of the order, will disagree with the payment afterwards. Show both numbers on a partially captured payment, never just one.
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

This is a capture in full, so 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

API reference: 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.
The customer’s bank decides when the money reappears. Compago reverses the authorization immediately, but issuers can take a few business days to restore the available balance. Tell customers this upfront, especially on large deposits.
A hold does not have to start from a card you already have. A card-saving link created with collectFunds: "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, with heldAt, 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.
The amount held is the 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:
See Collecting Funds With the Link for how to create one.

Checking Whether a Hold Is Still Live

Both payment lookups return the hold fields, so you never have to keep the state yourself:
API reference: 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:
  • status is HELD
  • holdExpired is false
  • holdReleasedAt is null
Use 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.
Compago publishes the deadline as 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.
One minute of wall clock across local midnight moves the deadline by a whole day. A hold placed Monday 3 March at 23:59 dies on Sunday 9 March. A hold placed one minute later, Tuesday 4 March at 00:00, dies on Monday 10 March, a full day further out. The boundary that matters is midnight in America/Mexico_City, not the hour you happen to call the API, so a job that runs “late at night” can produce holds a day apart in length depending on which side of midnight each request lands. If the length of the window matters to your process, pin the placement to a known hour of the day rather than to the end of one.
Counting the days out:

Automatic Release

If nobody captures the hold, Compago releases it automatically and the payment becomes HOLD_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.
Do not wait for the sweep to decide whether you can capture. From 23:00 on the last day, capture is refused with 409 HOLD_WINDOW_CLOSED whether or not the sweep has physically reached your payment yet. The refusal is a rule, not a race: Compago will not send a capture into the bank’s 23:30 cutoff. Plan to capture during the day, not in the last half hour.
Card issuers may drop a hold on their own before the 7 natural days are up. This is common on debit cards, where some banks release reservations after a few days regardless of what the merchant intended. When that happens the capture is declined with 402 and the funds were already freed. There is nothing to recover: the payment stays HELD on Compago’s side, and your options are to release it explicitly or to place a new charge with the customer’s agreement. For long windows on debit cards, capture as early as you reasonably can.
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 3,500MXNdepositleaves3,500 MXN deposit leaves 7,000 MXN of the customer’s money blocked. With it, the retry returns the hold you already placed.
This is not the payment method externalId. They are two different fields, one screen apart in these docs:
  • The payment method externalId, sent to POST /api/payment-method and documented in Manage Payment Methods, is your reference for the saved card or customer.
  • The payment externalId, sent to POST /api/payment-method/{id}/payment and documented here, is your idempotency key for one hold or charge.
They live in separate namespaces, so the same string may safely be used for both. They are never compared with each other.

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 1,200MXNrequestaftercapturing1,200 MXN request after capturing 200 MXN of it returns the original payment rather than a 409 mismatch, so keep sending the amount you sent the first time.
A declined attempt spends the externalId. This surprises people, so it is worth stating plainly: if a hold is declined by the bank, replaying the same externalId returns that decline again, it does not try the card a second time. To retry a declined hold you must send a new externalId, for example deposit-R-4821-2.This is deliberate. An idempotency key names one attempt, not one intention. The endpoint cannot tell “my HTTP client retried” from “my operator pressed the button again”, and freeing the key on a decline would let a duplicate retry fire a second authorization at the issuer, which feeds their fraud heuristics and can produce a genuine double hold when the bank actually approved a request we recorded as declined.
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.
The key is echoed back as 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 an externalId, 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.
Capture and release do not have this problem. Both are keyed on a specific 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

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.
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.
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.
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.
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.
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.
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.