Skip to main content
A Purchase Order records a completed or pending ticket transaction. Orders can be created by authenticated users or completely anonymously — no account required. Each order is linked to a Visit date and one or more TicketsPurchaseOrder line items. After creation, a QR code is automatically generated (via a Django signal) and a confirmation email is sent via EmailJS.

Endpoints

MethodPathAuthDescription
POST/api/purchase_orders/anonymous/PublicCreate an anonymous purchase
GET/api/purchase_orders/validate_qr/PublicValidate a single QR code
POST/api/purchase_orders/validate-bulk-qr/PublicValidate multiple QR codes
GET/api/purchase_orders/AuthList orders (admin: all; user: own)
GET/api/purchase_orders/{id}/AuthRetrieve a single order
POST/api/purchase_orders/create/AuthCreate an authenticated order
PUT/api/purchase_orders/{id}/update/AuthUpdate an order
DELETE/api/purchase_orders/{id}/delete/AuthDelete an order
GET/api/tickets_purchase_orders/AdminList ticket line items

POST /api/purchase_orders/anonymous/

The primary public endpoint for ticket purchases. No authentication or account is needed — only an email address. The service layer validates stock and visit capacity, creates the order atomically, decrements occupied_slots on each ticket, and increments occupied_slots on the selected visit.
A QR image is generated automatically after the order is saved (via post_save signal). The qr_image URL will be available when you retrieve the order by ID.

Request body

email
string
required
Buyer’s email address. Used as the sole identifier for anonymous orders. Must contain @.
tickets
array
required
One or more ticket line items to purchase.
visit_id
integer
required
ID of the Visits record for the selected date. Create or look up a visit first using POST /api/visits/create/.
payment_method_id
integer
required
Numeric ID of the payment method:
  • 1 — Card
  • 2 — PayPal
  • 3 — Cash

curl example

curl -X POST https://api.parquemarino.cr/api/purchase_orders/anonymous/ \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "tickets": [
      {"ticket_id": 1, "quantity": 2},
      {"ticket_id": 3, "quantity": 1}
    ],
    "visit_id": 15,
    "payment_method_id": 1
  }'

JavaScript example

const response = await fetch('/api/purchase_orders/anonymous/', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    email: '[email protected]',
    tickets: [
      { ticket_id: 1, quantity: 2 },
      { ticket_id: 3, quantity: 1 }
    ],
    visit_id: 15,
    payment_method_id: 1
  })
});

const data = await response.json();
console.log(data.confirmation_code); // "ANON-42"

Response — 201 Created

success
boolean
true on successful order creation.
message
string
Human-readable confirmation message.
order_id
integer
The newly created PurchaseOrders primary key.
confirmation_code
string
Unique code for this order, formatted as ANON-{order_id}. Use this to track or reference the purchase.
total
decimal
Combined total price across all currencies (simplified sum). For per-currency breakdowns, retrieve the order by ID.
email
string
The email address confirmed for this order.
{
  "success": true,
  "message": "Compra realizada exitosamente",
  "order_id": 42,
  "confirmation_code": "ANON-42",
  "total": "38000.00",
  "email": "[email protected]"
}

Error responses

ScenarioStatusResponse
Missing email400{"error": "Email es requerido para compras anónimas"}
No tickets selected400{"error": "Debe seleccionar al menos un ticket"}
Invalid ticket ID400{"error": "Ticket con ID X no existe"}
Quantity <= 0400{"error": "Cantidad debe ser mayor a 0 para el ticket ..."}
Ticket out of stock400{"error": "Stock insuficiente para ... Disponible: N, Solicitado: M"}
Visit not found400{"error": "La visita con ID X no existe"}
Visit capacity exceeded400{"error": "No hay cupos suficientes en la visita. Disponibles: N, Solicitados: M"}
Invalid payment method400{"error": "Método de pago con ID X no existe"}

GET /api/purchase_orders/validate_qr/

Validates a scanned QR code and returns the order status and access decision. No authentication required — intended for use at the park entrance. Supports three QR formats:
  • JSON (primary): {"order_id": 42, "email": "...", "verification": "abc123..."} — includes SHA-256 integrity check
  • Legacy full: Order ID: 42 | Email: [email protected]
  • Simple: 42 (order ID only)

Query parameters

data
string
required
The raw string content from the scanned QR code. For JSON format, pass the JSON string directly.

curl example

# JSON format (recommended)
curl "https://api.parquemarino.cr/api/purchase_orders/validate_qr/?data=%7B%22order_id%22%3A42%2C%22email%22%3A%22visitante%40ejemplo.com%22%2C%22verification%22%3A%22a1b2c3d4%22%7D"

# Simple format
curl "https://api.parquemarino.cr/api/purchase_orders/validate_qr/?data=42"

Response

valid
boolean
Whether the QR data is structurally valid and the order exists.
access_granted
boolean
true only when status == "PAID". Use this field to make the gate decision.
status
string
Current order status: PAID, PENDING, CANCELLED, or FAILED.
detail
string
Human-readable message describing the access decision.
order_id
integer
The matched order ID.
email
string
Email address on the order.
visit_date
string
ISO date of the visit (YYYY-MM-DD).
total_tickets
integer
Total number of tickets across all line items.
total_price_crc
number
Total amount in Costa Rican colones.
total_price_usd
number
Total amount in US dollars.
tickets_detail
array
Per-ticket breakdown.
verification_passed
boolean
true if the QR was in JSON format and the SHA-256 hash matched. Always false for legacy formats.
Paid order — access granted:
{
  "valid": true,
  "access_granted": true,
  "status": "PAID",
  "detail": "QR válido. Orden pagada correctamente. Acceso autorizado.",
  "order_id": 42,
  "email": "[email protected]",
  "visit_date": "2025-05-20",
  "total_tickets": 3,
  "total_price_crc": 38000.0,
  "total_price_usd": 0.0,
  "tickets_detail": [
    {
      "ticket_name": "Entrada General",
      "quantity": 2,
      "price_per_ticket": 15000.0,
      "subtotal": 30000.0,
      "currency": "CRC"
    },
    {
      "ticket_name": "Entrada Niño",
      "quantity": 1,
      "price_per_ticket": 8000.0,
      "subtotal": 8000.0,
      "currency": "CRC"
    }
  ],
  "verification_passed": true
}
Pending order — access denied:
{
  "valid": true,
  "access_granted": false,
  "status": "PENDING",
  "payment_required": true,
  "detail": "Orden válida pero pendiente de pago. Acceso denegado."
}

POST /api/purchase_orders/validate-bulk-qr/

Validates up to 100 QR codes in a single request. Useful for pre-checking a group booking or auditing a batch of tickets at the entrance.

Request body

qr_codes
array
required
List of raw QR strings to validate. Maximum 100 items per request. Supports all three QR formats (JSON, legacy full, simple).
curl -X POST https://api.parquemarino.cr/api/purchase_orders/validate-bulk-qr/ \
  -H "Content-Type: application/json" \
  -d '{
    "qr_codes": [
      "{\\"order_id\\": 42, \\"email\\": \\"[email protected]\\", \\"verification\\": \\"abc123\\"}",
      "Order ID: 43 | Email: [email protected]",
      "44"
    ]
  }'

Response

total_processed
integer
Number of QR codes submitted.
valid_count
integer
Number of QR codes that resolved to a PAID order.
invalid_count
integer
Number of QR codes that failed validation.
success_rate
number
Percentage of valid QRs, rounded to 2 decimal places.
results
array
Per-QR result objects.
{
  "total_processed": 3,
  "valid_count": 2,
  "invalid_count": 1,
  "success_rate": 66.67,
  "results": [
    {
      "index": 0,
      "order_id": 42,
      "valid": true,
      "access_granted": true,
      "status": "PAID",
      "detail": "✅ QR válido. Acceso autorizado.",
      "email": "[email protected]",
      "visit_date": "2025-05-20",
      "total_tickets": 2,
      "total_price_crc": 30000.0,
      "qr_format": "JSON",
      "verification_passed": true
    },
    {
      "index": 1,
      "order_id": 43,
      "valid": true,
      "access_granted": true,
      "status": "PAID",
      "detail": "✅ QR válido. Acceso autorizado.",
      "qr_format": "Legacy",
      "verification_passed": true
    },
    {
      "index": 2,
      "order_id": null,
      "valid": false,
      "status": null,
      "detail": "Orden no encontrada.",
      "qr_format": "Legacy"
    }
  ]
}
The bulk endpoint has a hard limit of 100 QR codes per request. Requests with more than 100 items return 400 Bad Request.

GET /api/tickets_purchase_orders/

Lists all TicketsPurchaseOrder line items (the join table between an order and a ticket type). Primarily used by admins to audit what was purchased in each order.
This endpoint is at the URL prefix /api/tickets_purchase_orders/, separate from /api/purchase_orders/.

Response fields

id
integer
Line item primary key.
purchase_order
integer
Foreign key to the parent PurchaseOrders record.
ticket
integer
Foreign key to the Tickets record.
amount
integer
Quantity purchased for this ticket type in this order.
subtotal
decimal
Computed read-only property: amount × ticket.price.
[
  {
    "id": 1,
    "purchase_order": 42,
    "ticket": 1,
    "amount": 2,
    "subtotal": "30000.00"
  },
  {
    "id": 2,
    "purchase_order": 42,
    "ticket": 3,
    "amount": 1,
    "subtotal": "8000.00"
  }
]

Model reference

PurchaseOrders

FieldTypeDescription
idintegerAuto-generated primary key
order_datedateSet automatically on creation
purchase_datedateSet automatically on creation
emailstringBuyer’s email (max 50 chars)
statusstringPENDING | PAID | CANCELLED | FAILED
total_pricedecimal(12,2)Combined total across all currencies
total_crcdecimal(12,2)Subtotal for CRC-denominated tickets
total_usddecimal(12,2)Subtotal for USD-denominated tickets
qr_imageimageAuto-generated QR image path (under qr_codes/)
visitFK → VisitsThe selected visit date
userFK → User | nullnull for anonymous orders

TicketsPurchaseOrder (line items)

FieldTypeDescription
idintegerAuto-generated primary key
purchase_orderFK → PurchaseOrdersParent order
ticketFK → TicketsTicket type
amountintegerQuantity purchased
subtotaldecimalComputed: amount × ticket.price (read-only property)

QR code format

Each order generates a QR image automatically via a post_save Django signal. The QR encodes a JSON payload:
{
  "order_id": 42,
  "confirmation_code": "ANON-42",
  "customer_email": "[email protected]",
  "visit_date": "2025-05-20T00:00:00Z",
  "total_price": 38000.00,
  "status": "PAID",
  "created_at": "2025-05-15T14:30:00Z",
  "verification_hash": "a1b2c3d4e5f6g7h8"
}
The verification_hash is a 16-character prefix of the SHA-256 digest computed from {order_id}-{email}-{purchase_date}-{total_price_crc}. The validate_qr endpoint re-computes this hash on every request to detect altered QR data.
Only QR codes in the JSON format carry a verification_hash. Legacy and simple formats skip integrity verification. For production gate control, only accept JSON-format QRs.

Anonymous purchase flow

The complete end-to-end flow for a guest buyer:
  1. Get available ticketsGET /api/tickets/available/
  2. Create or retrieve a visit for the desired date — POST /api/visits/create/
  3. Submit the purchasePOST /api/purchase_orders/anonymous/
  4. Receive confirmation — the response includes confirmation_code and order_id
  5. QR generated automatically — available on the order record as qr_image
  6. Email invoice sent — the frontend triggers EmailJS after a successful response
  7. Entry validation — staff scan the QR at the gate using GET /api/purchase_orders/validate_qr/

Build docs developers (and LLMs) love