Cards and operations
Draws, sealed packs, refunds, buybacks, and redemption.
Every change to cards or balance is an operation, created with POST /api/v2/operations. This guide covers the off-chain operations; minting is covered in On-chain cards. For the whole flow in one place, with retries, see Off-chain lifecycle.
Each request needs an Idempotency-Key header. See Errors and retries.
Draw a pack
{
"kind": "order",
"customerId": "cus_…",
"vendingMachineId": 1,
"quantity": 3,
"reveal": "on_create",
"metadata": { "cartId": "c_981" }
}
quantityis 1 to 10. The charge ispriceUsd × quantity, returned aschargedUsd.metadatais any JSON up to 20 KB. It is stored on the operation and returned as is.- The drawn cards are in the response's
cardsarray.
Possible failures: 409 insufficient_balance, 409 out_of_stock, and 404 not_found for an unknown customer or an unavailable vending machine.
Sealed packs
With "reveal": "on_create" (the default), cards are revealed at once and cannot be refunded.
With "reveal": "sealed", cards stay hidden: sealed is true, their name, image, and buybackUsd are null, and refundableUntil shows when the refund window closes. This lets you build an "open the pack" moment, or let users change their mind.
Reveal cards when the user opens them. Revealing cannot be undone.
curl -X POST "$PACKFLIP_BASE_URL/api/v2/customers/cus_…/cards/reveal" \
-H "Authorization: Bearer $PACKFLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "cardIds": ["card_…", "card_…"] }'
Refund sealed cards before they are revealed and before refundableUntil. The cards return to stock, and each card's share of the pack price is credited back to the bucket (cash or bonus) it was paid from.
{ "kind": "refund", "customerId": "cus_…", "cardIds": ["card_…"] }
A sealed card whose window has passed counts as revealed. Buyback, redemption, and minting all require a revealed card; a sealed one returns 409 conflict. See what you can do with a card for the full rule.
List a customer's cards
GET /api/v2/customers/{customerId}/cards?limit=50 returns cards newest first, up to 200 per request.
For images, use image as the default source and imageSrcset for responsive sizes:
<img src="{image}" srcset="{imageSrcset}" sizes="(min-width: 768px) 320px, 50vw" alt="{name}" />
buybackUsd is the current market value Packflip pays for the card. It follows the market and can change between two requests.
Buy back held cards
{ "kind": "buyback", "mode": "offchain", "customerId": "cus_…", "cardIds": ["card_…"] }
The operation completes immediately. The sum of the cards' current buyback values is credited to your cash balance and returned as creditedUsd. Up to 10 cards per operation.
What you pay your user for a buyback is up to you; Packflip credits your balance.
For cards that have been minted, use "mode": "onchain". See On-chain cards.
Redeem cards for shipping
Redemption ships the physical cards to your customer. Check where you can ship, and the fee, first:
curl "$PACKFLIP_BASE_URL/api/v2/shipping-countries" \
-H "Authorization: Bearer $PACKFLIP_API_KEY"
{ "data": [{ "country": "Japan", "feeUsd": "3.000000" }] }
country is the English country name Packflip ships to. Send it back verbatim as shipmentInfo.country; an ISO 3166-1 alpha-2 code such as JP also works.
{
"kind": "redemption",
"mode": "offchain",
"customerId": "cus_…",
"cardIds": ["card_…"],
"shipmentInfo": {
"country": "JP",
"name": "Hanako Yamada",
"phone": "+81 …",
"address1": "…",
"city": "…",
"postalCode": "…"
}
}
shipmentInfo.countryis required: an English country name or an ISO 3166-1 alpha-2 code. Unknown values return400.shipmentInfois a JSON object up to 20 KB, stored with the shipment. The API validates onlycountry; Packflip's fulfilment team reads the rest, so send these fields:
| Field | Required to ship | Notes |
|---|---|---|
country | Yes (validated) | From GET /api/v2/shipping-countries, or an alpha-2 code. |
name | Yes | Recipient's full name, in the script the carrier expects for that country. |
phone | Yes | With country code, for example +81 90 1234 5678. Carriers call it on delivery problems. |
address1 | Yes | Street address. |
address2 | No | Building, apartment, or unit. |
city | Yes | City or locality. |
state | Where used | State, province, or prefecture. |
postalCode | Where used | Required in countries that use postal codes. |
Collect all of them in your checkout. If Packflip cannot deliver to the address, the shipment moves to exception and statusReason says why.
- The shipping fee is charged to your balance.
- Add
notificationsto have Packflip email about the shipment (optional):
"notifications": {
"channels": ["customer_email", "partner_email"],
"customerEmail": "fan@example.com"
}
customer_email emails customerEmail, which is required with that channel. partner_email emails the notification address set in the Console under Webhooks; without one the request returns 400. Emails go out when the shipment is created, shipped, delivered, or hits an exception, at most once per status. Customer emails name your organization and come from Packflip; replies go to Packflip support. The sandbox only sends partner_email.
The operation's shipment has a status of created, shipped, in_transit, delivered, or exception, plus carrier, trackingNumber, and trackingUrl once known. Packflip ships the cards and updates the shipment; each change emits shipment.updated. Shipment events do not repeat the recipient's address.
When a shipment cannot go out
If Packflip cannot ship, for example because the address is incomplete, the shipment moves to exception and statusReason says what to fix. You get shipment.updated, and the notification email set in the Console receives an email even if the redemption did not ask for partner_email.
Correct the address while the shipment is created or exception:
curl -X PUT "$PACKFLIP_BASE_URL/api/v2/operations/op_…/shipment/address" \
-H "Authorization: Bearer $PACKFLIP_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "shipmentInfo": { "country": "Japan", "name": "Hanako Yamada", "phone": "+81 …", "address1": "…", "city": "…", "postalCode": "…" } }'
- Send the complete
shipmentInfo; it replaces the stored one. - The country cannot change, because the shipping fee was charged for it. Returns
400otherwise; contact support to ship elsewhere. - The shipment returns to
created,statusReasonis cleared,addressUpdatedAtis set, andshipment.updatedis sent. Packflip ships it from there. - Once the shipment is
shipped, the call returns409 conflict.
shipment.updated is always sent, whether or not you ask for emails. The operation's shipment.notifications.emails shows which emails went out.
In the sandbox nobody ships anything, so move a test shipment along yourself with PUT /api/v2/operations/{operationId}/shipment. Production returns 403 for that call.
Look up an operation
GET /api/v2/operations/{operationId} returns the operation with its cards (cards it created) and affectedCards (cards it acted on).