Sandbox

Go to production
PackflipPartner

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" }
}
  • quantity is 1 to 10. The charge is priceUsd × quantity, returned as chargedUsd.
  • metadata is any JSON up to 20 KB. It is stored on the operation and returned as is.
  • The drawn cards are in the response's cards array.

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.country is required: an English country name or an ISO 3166-1 alpha-2 code. Unknown values return 400.
  • shipmentInfo is a JSON object up to 20 KB, stored with the shipment. The API validates only country; Packflip's fulfilment team reads the rest, so send these fields:
FieldRequired to shipNotes
countryYes (validated)From GET /api/v2/shipping-countries, or an alpha-2 code.
nameYesRecipient's full name, in the script the carrier expects for that country.
phoneYesWith country code, for example +81 90 1234 5678. Carriers call it on delivery problems.
address1YesStreet address.
address2NoBuilding, apartment, or unit.
cityYesCity or locality.
stateWhere usedState, province, or prefecture.
postalCodeWhere usedRequired 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 notifications to 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 400 otherwise; contact support to ship elsewhere.
  • The shipment returns to created, statusReason is cleared, addressUpdatedAt is set, and shipment.updated is sent. Packflip ships it from there.
  • Once the shipment is shipped, the call returns 409 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).