Offers
An offer is the complete negotiation between a buyer and seller. Each opening price or counterproposal is a proposal. Read offers with offers:read; send or respond to them with offers:write.
Send an offer
POST /v1/offers accepts 1–1,000 distinct listings from one seller. Quantities and listing membership stay fixed throughout the negotiation. Supply exactly one price instruction for the entire item subtotal: total as Money in decimal major units, or discountPercentage (20 means 20% off). Per-item prices are not accepted.
curl https://api.cardnexus.com/v1/offers \
-H "Authorization: Bearer $CARDNEXUS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 13ae842b-a8ea-4ab0-8ee7-2777774987bf" \
-d '{
"items": [{
"listingId": "665f3a2b1c8d4e9f7a6b5c52",
"quantity": 2
}],
"total": { "amount": 25, "currency": "EUR" }
}'
The request is validated as a whole. Creation leaves your cart unchanged and does not reserve stock. Your account and the seller must be eligible for offers, the requested stock must be available, and the seller must allow negotiations. The original subtotal must be at least 20 in the listing currency. The offered total must be between 60% and 100% of that subtotal: a discount greater than 40%, a total below the minimum, or a total above the original subtotal returns 422.
For a percentage offer, replace total with "discountPercentage": 20. The server allocates the resulting subtotal across the items using the same rounding as the website. Quantities can prevent an exact whole-cent allocation: three units originally priced at €7.01 with a 40% discount result in €12.63. currentProposal.total is the actual proposed subtotal. Allocated unit prices appear only as read-only values in proposal history.
An existing live offer with this seller returns 409 CONFLICT, with its offerId in data. An open offer must be cancelled explicitly before creating a replacement.
Counter, accept, or decline
You can counter, accept, or decline a price proposed by the other participant. Either participant can cancel a pending or countered offer.
First, fetch the offer and review currentProposal.total. Send its currentProposal.id as proposalId in your accept, decline, or counter request. This identifies the price you are responding to.
For example, to accept the price in that proposal:
{
"proposalId": "665f3a2b1c8d4e9f7a6b5c60"
}
If the other participant sends a new price before your request arrives, the API returns 409 Conflict without applying your response. Fetch the offer again and review the new price before responding. This prevents accepting a price you have not seen.
A counter supplies a new total or percentage for the same items and quantities:
{
"proposalId": "665f3a2b1c8d4e9f7a6b5c60",
"total": { "amount": 26, "currency": "EUR" }
}
Accept and decline take only proposalId. The buyer can submit three proposals in one negotiation, including the opening proposal. A further buyer counter automatically declines the offer and returns a proposal-limit error.
Acceptance reserves the stock for 48 hours. It does not create a paid order or add items to your cart. Reservation failure leaves the offer open.
Add an accepted offer to your cart
The buyer calls POST /v1/offers/{offerId}/add-to-cart with both offers:write and cart:write. The offer must be accepted, unexpired, and unspent.
The negotiated quantities are restored without duplicating them on repeat calls. Other listings remain in your cart. If you have no cart yet, your profile country determines the delivery country; the seller must ship there.
After restoration, removing a negotiated item or reducing its quantity below the agreement voids the offer. Before restoration, cart edits do not affect an offer created through this API. If restoration fails, that offer is voided. GET /v1/cart returns the cart contents.
Read your offers
GET /v1/offers returns your sent and received offers, newest first. It includes every status by default. Filters are role=buyer|seller, repeatable status, createdFrom, and createdTo.
Follow pagination.nextCursor until it is null. The default limit is 50; the maximum is 100. GET /v1/offers/{offerId} adds the fixed items and chronological proposal history. Accounts outside the negotiation receive 404.
Each item includes its numeric productId, or null when its catalogue reference is unavailable. The original listing ID, quantities, prices and proposal history remain available.
Mutations return a summary with the current proposal, totals, timestamps, and actions. Actions reflect your role, account eligibility, and the scopes on your credential.
| Status | Meaning |
|---|---|
pending |
The opening proposal awaits a response. |
countered |
A counterproposal awaits a response. |
accepted |
The current proposal was accepted. Redemption is subject to its deadline and whether it has already been spent. |
declined |
The current proposal was rejected. |
cancelled |
The negotiation was withdrawn or cancelled by CardNexus. |
voided |
The offer can no longer be redeemed. |
Limits and retries
Creation has a limit of five requests per hour per account, shared across your API keys and connected applications. The window resets on the hour. Counterproposals and other operations do not count toward it. The marketplace also limits opening offers to ten per rolling hour across the website, mobile app, and API.
All mutations support an optional Idempotency-Key. Repeating the same key returns the cached response for 24 hours. Requests, including retries, count toward rate limits before the cached response is returned.
Webhooks
offer.created reports an opening proposal. offer.updated reports countered, accepted, declined, cancelled, or voided. Both participants receive events, including changes made outside the API. Connected applications need offers:read.
Each event contains the offer and proposal IDs, your recipient role, the current total, the change timestamp, and any redemption deadline. Updates also contain the previous status and reason. Event IDs and timestamps remain the same on retries.
Void reasons are basket_changed, listing_changed, seller_unavailable, seller_paused, offers_disabled, and expired. Events contain no message text or moderation details. Payment is reported through the existing order events.