Forloy API

v1

Recording Transactions

This is the core integration workflow — looking up a customer's card and recording their purchase. Every transaction flows through two steps: identify the card, then record the purchase.

Transaction Flow

The diagram below illustrates the end-to-end flow from the moment a customer presents their card to when the transaction result is displayed.

Loading diagram...

Step 1: Look Up Card

Use POST /api/v1/cards/lookup to retrieve information about a customer's card. There are two ways to identify a card:

  • qr_code_data The base64-encoded payload from scanning the customer's QR code
  • barcode_value The numeric display ID shown as a barcode on the card

Lookup by QR Code

Request — QR Code
POST /api/v1/cards/lookup
Content-Type: application/json
Authorization: Bearer <access_token>

{
  "qr_code_data": "eyJjYXJkX2luc3RhbmNlX2lkIjoiYTFiMmMzZDQtZTVmNi..."
}

Lookup by Barcode

Request — Barcode
POST /api/v1/cards/lookup
Content-Type: application/json
Authorization: Bearer <access_token>

{
  "barcode_value": "90001234567890"
}

Response

The response includes the card_instance_id, card type, title, merchant and customer names, status, and the current balance. Below is an example response for a punch card:

Response — Punch Card
{
  "success": true,
  "data": {
    "card_instance_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "card_type": "punch",
    "card_title": "Coffee Rewards",
    "merchant_name": "Bean & Brew",
    "customer_name": "Jane Doe",
    "status": "active",
    "balance": {
      "current_punches": 7,
      "punch_requirement": 10,
      "pending_rewards": 0,
      "lifetime_punches": 27
    }
  },
  "meta": {
    "request_id": "req_abc123",
    "timestamp": "2026-01-15T10:30:00.000Z"
  }
}

The balance object varies depending on the card type. Punch cards include current_punches, punch_requirement, pending_rewards. Cashback cards include the current monetary balance. Discount cards include the current discount rate and tier information.

Important: The lookup endpoint is read-only — it does NOT create a transaction. Use it to display card information to the cashier before recording a purchase.

Step 2: Record Purchase

Use POST /api/v1/transactions/record to record a purchase transaction. You can identify the card by qr_code_data or by card_instance_id (returned from the lookup step).

Required Fields

  • location_id The location where the transaction is taking place
  • staff_user_id The staff member processing the transaction
  • purchase_amount The total purchase amount

Optional Fields

  • punch_count Number of punches to add (default: 1, punch cards only)
  • transaction_notes Free-text notes about the transaction
  • idempotency_key Unique key to prevent duplicate transactions
  • pos_reference_id Your POS system's internal transaction reference
  • pos_terminal_id The terminal or register identifier
  • client_timestamp ISO 8601 timestamp from the client device (useful for offline sync)

Punch Cards

For punch cards, include punch_count to specify how many punches to add (defaults to 1). The response tells you the new punch count and whether a reward was earned.

Request — Punch Card
POST /api/v1/transactions/record
Content-Type: application/json
Authorization: Bearer <access_token>

{
  "qr_code_data": "eyJjYXJkX2luc3RhbmNlX2lkIjoiYTFiMmMzZDQtZTVmNi...",
  "location_id": "loc_001",
  "staff_user_id": "staff_042",
  "purchase_amount": 4.50,
  "punch_count": 1,
  "idempotency_key": "txn_20260115_103000_pos3"
}
Response — No Reward
{
  "success": true,
  "data": {
    "transaction_id": "txn_9f8e7d6c-5b4a-3210-fedc-ba0987654321",
    "card_type": "punch",
    "punches_added": 1,
    "new_punch_count": 8,
    "reward_earned": false,
    "reward": null
  },
  "meta": {
    "request_id": "req_def456",
    "timestamp": "2026-01-15T10:30:05.000Z"
  }
}

When the customer reaches the punch threshold, the response includes reward details:

Response — Reward Earned
{
  "success": true,
  "data": {
    "transaction_id": "txn_1a2b3c4d-5e6f-7890-abcd-ef1234567890",
    "card_type": "punch",
    "punches_added": 1,
    "new_punch_count": 10,
    "reward_earned": true,
    "reward": {
      "reward_id": "rwd_abc123",
      "title": "Free Coffee",
      "description": "One free drink of your choice",
      "status": "pending_claim"
    }
  },
  "meta": {
    "request_id": "req_ghi789",
    "timestamp": "2026-01-15T10:35:00.000Z"
  }
}

Note: There is a 1-minute cooldown between punch transactions for the same card. Attempting to record another punch within the cooldown period will return a COOLDOWN_ACTIVE error (HTTP 429).

Cashback Cards

For cashback cards, the purchase_amount is used to calculate the cashback earned based on the configured rate. The response includes the cashback earned and the new balance.

Request — Cashback Card
POST /api/v1/transactions/record
Content-Type: application/json
Authorization: Bearer <access_token>

{
  "card_instance_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "location_id": "loc_001",
  "staff_user_id": "staff_042",
  "purchase_amount": 85.00,
  "idempotency_key": "txn_20260115_104500_pos3"
}
Response — Cashback Card
{
  "success": true,
  "data": {
    "transaction_id": "txn_cb001122-3344-5566-7788-99aabbccddee",
    "card_type": "cashback",
    "purchase_amount": 85.00,
    "cashback_rate": 0.05,
    "cashback_earned": 4.25,
    "new_balance": 32.75,
    "tier_changed": false
  },
  "meta": {
    "request_id": "req_jkl012",
    "timestamp": "2026-01-15T10:45:00.000Z"
  }
}

Discount Cards

For discount cards, the response includes the discount rate applied, the discount amount, and the final amount the customer should pay.

Request — Discount Card
POST /api/v1/transactions/record
Content-Type: application/json
Authorization: Bearer <access_token>

{
  "card_instance_id": "d4e5f6a7-b8c9-0123-4567-890abcdef012",
  "location_id": "loc_001",
  "staff_user_id": "staff_042",
  "purchase_amount": 120.00,
  "idempotency_key": "txn_20260115_110000_pos3"
}
Response — Discount Card
{
  "success": true,
  "data": {
    "transaction_id": "txn_dc112233-4455-6677-8899-aabbccddeeff",
    "card_type": "discount",
    "purchase_amount": 120.00,
    "discount_rate": 0.10,
    "discount_amount": 12.00,
    "final_amount": 108.00
  },
  "meta": {
    "request_id": "req_mno345",
    "timestamp": "2026-01-15T11:00:00.000Z"
  }
}

Idempotency

Use the idempotency_key field to prevent duplicate transactions caused by network retries or connectivity issues. If the same key is sent twice, the API returns the original transaction result instead of creating a duplicate.

Idempotent Response (duplicate request)
{
  "success": true,
  "data": {
    "transaction_id": "txn_9f8e7d6c-5b4a-3210-fedc-ba0987654321",
    "card_type": "punch",
    "punches_added": 1,
    "new_punch_count": 8,
    "reward_earned": false,
    "reward": null,
    "idempotent": true
  },
  "meta": {
    "request_id": "req_pqr678",
    "timestamp": "2026-01-15T10:30:05.000Z"
  }
}

When a duplicate request is detected, the response includes "idempotent": true along with the original transaction data.

Recommendation: Generate a UUID or timestamp-based key for each transaction attempt. A good pattern is txn_{date}_{time}_{terminal_id} to ensure uniqueness while remaining debuggable.

Transaction History

Use GET /api/v1/transactions/history to retrieve past transactions for a specific card. This is useful for displaying a customer's transaction history or for reconciliation purposes.

Query Parameters

  • card_instance_id Required. The card to retrieve history for
  • page Page number (default: 1)
  • page_size Results per page (default: 20)
  • transaction_type Filter by type (e.g., punch, cashback, discount)
  • from_date ISO 8601 start date filter
  • to_date ISO 8601 end date filter
Request
GET /api/v1/transactions/history?card_instance_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890&page=1&page_size=10
Authorization: Bearer <access_token>
Response
{
  "success": true,
  "data": {
    "transactions": [
      {
        "transaction_id": "txn_9f8e7d6c-5b4a-3210-fedc-ba0987654321",
        "transaction_type": "punch",
        "purchase_amount": 4.50,
        "punches_added": 1,
        "created_at": "2026-01-15T10:30:05.000Z",
        "location_name": "Downtown Branch",
        "staff_name": "Alex M."
      },
      {
        "transaction_id": "txn_1a2b3c4d-5e6f-7890-abcd-ef1234567890",
        "transaction_type": "punch",
        "purchase_amount": 5.00,
        "punches_added": 1,
        "created_at": "2026-01-14T09:15:00.000Z",
        "location_name": "Airport Kiosk",
        "staff_name": "Sam K."
      }
    ],
    "pagination": {
      "page": 1,
      "page_size": 10,
      "total_count": 27,
      "total_pages": 3
    }
  },
  "meta": {
    "request_id": "req_stu901",
    "timestamp": "2026-01-15T12:00:00.000Z"
  }
}

Next Steps

Now that you can look up cards and record transactions, explore these related guides:

  • Offline Sync How to batch sync transactions when connectivity is intermittent
  • Rewards & Cashback How to claim earned rewards and redeem cashback balances