Tickets
Guide to implementing event ticketing — mint, transfer, and verify tickets using Ticket intents.
When to Use Ticket Intents
Ticket intents are for any scenario where proof of entitlement needs to be created, transferred, and verified — concerts, conferences, gated access, NFT ticket drops. Tickets use a developer-owned free-form schema with a developer backend as the exit target.
Data Payload
pub struct FreeFormPayload {
pub kind: String, // e.g. "concert_vip_redeem", "event_ga_entry"
pub data: Vec<u8>, // Serialized application-specific fields
}
The SDK does not parse or validate the contents of data. Your application defines the schema and validation logic.
Dual-Signature Model
Tickets use a unique dual-signature envelope with three participants:
- Issuer — signs at creation (proves legitimacy)
- Device — signs the intent envelope (replay protection)
- Holder — signs at redemption (proves execution)
Signing Order
zypp_create_intent("ticket", payload)
→ IntentPayload::Ticket { signatures: DualSignatureEnvelope {
issuer_signature: vec![], // empty
holder_signature: None, // empty
nonce: 0,
}}
zypp_attach_issuer_signature(id, issuer_sig_hex, issuer_nonce)
→ writes to signatures.issuer_signature + signatures.nonce
→ guard: must be Ticket, issuer_signature must be empty
zypp_attach_signature(id, sig_hex, pk_hex, nonce)
→ writes to Intent.envelope (CryptographicEnvelope)
→ sets trust_tier = Signed
zypp_attach_holder_signature(id, holder_sig_hex)
→ writes to signatures.holder_signature
→ guard: must be Ticket, issuer_signature must be non-empty
Enforced guard: issuer signature must be non-empty before the holder can sign.
Creating a Ticket Intent
Expo SDK
import { createIntent, attachIssuerSignature, attachHolderSignature } from '@zypp-labs/expo-sdk';
// 1. Create ticket intent
const { intentId } = createIntent('concert_vip_redeem', JSON.stringify({
eventId: 'concert-2026-07-15',
seat: 'A12',
tier: 'VIP',
}));
// 2. Attach issuer signature (from your backend)
attachIssuerSignature(intentId, issuerSigHex, 1);
// 3. Attach device signature
attachSignature(intentId, signatureHex, publicKeyHex, nonce);
// 4. At redemption, attach holder signature
attachHolderSignature(intentId, holderSigHex);
Unity SDK
var intent = ZyppSdk.CreateIntent("concert_vip_redeem", "{\"seat\":\"A12\"}", ZyppTrustTier.Signed);
ZyppSdk.AttachIssuerSignature(intent.intentId, issuerSigHex, 1);
ZyppSdk.AttachSignature(intent.intentId, sigHex, pubKeyHex, nonce);
ZyppSdk.AttachHolderSignature(intent.intentId, holderSigHex);
Verification Flow
At the venue, the verifier checks:
intent.status == SyncedFinalized— the ticket has been accepted- Issuer signature is present — the ticket was legitimately issued
- Holder signature is present — the device performed the redemption
Verification works offline — cached intents can be checked without internet access.
Template: Atomic Redemption
The zypp-ticket starter template provides an atomic check-and-set redemption pattern to prevent double-spend (TOCTOU):
// Template-level code (not in SDK core)
pub async fn validate_ticket_redemption(db_pool: &DbPool, payload: FreeFormPayload)
-> Result<(), RejectionReason>
{
let result = sqlx::query!(
"UPDATE tickets SET redeemed = true, redeemed_at = NOW()
WHERE id = $1 AND redeemed = false
RETURNING winning_txn",
ticket_id
).fetch_optional(db_pool).await;
match result {
Ok(Some(_)) => Ok(()),
Ok(None) => Err(RejectionReason::ValidationError {
code: 409,
message: "Ticket already redeemed".to_string(),
details: None,
}),
Err(e) => Err(RejectionReason::ValidationError {
code: 500, message: e.to_string(), details: None,
}),
}
}
Offline Transfer
Tickets can be transferred between devices without network connectivity:
// Sender creates transfer packet
const { packetB64 } = createTransferPacket(intentId);
// Render as QR code, send via BLE, or use NFC
// ...
// Receiver decodes
const decoded = decodeTransferPacket(packetB64);
Error Handling
| Error | Cause | Action |
|---|---|---|
IssuerSignatureRequired | Holder signed before issuer | Complete issuer signing first |
ValidationError | Backend rejected the ticket | Check backend validation rules |
DuplicateInLocalQueue | Same ticket already queued | Avoid duplicate creation |
Next Steps
- See the Arcade template for another Ticket use case
- Learn about Action intents for generic Solana program interaction
- Review the Security Model for ticket integrity