Skip to content

REST API Reference

Overview

The Gavekort API is exposed via Google Cloud Functions HTTP endpoints with Firebase ID token authentication.

All REST endpoints require: - Authorization: Bearer <firebase-id-token> header - Content-Type: application/json for POST requests

Base URL

https://your-project-region-gcp.cloudfunctions.net/

Authentication

Firebase ID tokens are obtained from Firebase Authentication. Pass the token in the Authorization header:

curl -H "Authorization: Bearer $(firebase auth:sign-in-as --email user@example.com)" \
  https://your-project-region.cloudfunctions.net/giftcards_issue \
  -d '{"productId": "giftcard-100nok", ...}'

Health Check

Endpoint: GET /healthCheck
Authentication: None required
Role: Public

Response

{
  "status": "ok",
  "timestamp": "2024-01-15T10:30:00.000Z"
}

Giftcard Management

Issue Giftcard

Endpoint: POST /giftcards_issue
Authentication: Required (Firebase ID token)
Role: ADMIN only

Issues a new giftcard to a user. Supports optional claim codes for gift distribution.

Request

{
  "idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
  "productId": "giftcard-100nok",
  "targetUserId": "user-recipient-id",
  "claimCode": "XXXX-XXXX-XXXX-XXXX",
  "reason": "Birthday gift"
}
Field Type Required Description
idempotencyKey UUID Unique idempotency key for exactly-once semantics
productId string Product ID (e.g., giftcard-100nok, token-10count)
targetUserId string Target user (defaults to requester if omitted)
claimCode string Optional claim code for giftcard distribution
reason string Reason for issuance (logged in audit trail)

Response

{
  "success": true,
  "data": {
    "walletItemId": "item-550e8400-e29b-41d4",
    "ledgerTxId": "tx-550e8400-e29b-41d4",
    "claimCodeHash": "sha256(code+pepper)"
  }
}

Errors

Status Code Message
400 INVALID_ARGUMENT Missing required field or invalid product
401 UNAUTHENTICATED Invalid or missing Firebase ID token
403 PERMISSION_DENIED User must have ADMIN role
500 INTERNAL Server error

Example

curl -X POST https://your-project.cloudfunctions.net/giftcards_issue \
  -H "Authorization: Bearer $ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
    "productId": "giftcard-100nok",
    "targetUserId": "user-john-doe",
    "reason": "Birthday gift"
  }'

Wallet Operations

Redeem Value

Endpoint: POST /wallet_redeem
Authentication: Required (Firebase ID token)
Role: END_USER (own items), STAFF, ADMIN (any items)

Deduct value from a wallet item (giftcard, token, or ticket).

Request

{
  "idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
  "walletItemId": "item-550e8400",
  "amount": 50,
  "reason": "Greens fee payment"
}
Field Type Required Description
idempotencyKey UUID Unique idempotency key
walletItemId string Wallet item ID to redeem from
amount number Amount to redeem (NOK for giftcards, count for tokens)
reason string Redemption reason

Response

{
  "success": true,
  "data": {
    "ledgerTxId": "tx-550e8400-e29b-41d4",
    "remainingBalance": 50
  }
}

Errors

Status Code Message
400 INVALID_ARGUMENT Missing required field or invalid amount
401 UNAUTHENTICATED Invalid or missing Firebase ID token
403 PERMISSION_DENIED User cannot redeem other users' items
404 NOT_FOUND Wallet item not found
500 INTERNAL Server error

Example

curl -X POST https://your-project.cloudfunctions.net/wallet_redeem \
  -H "Authorization: Bearer $ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
    "walletItemId": "item-550e8400",
    "amount": 50,
    "reason": "Course payment"
  }'

Claim Code

Endpoint: POST /wallet_claim
Authentication: Required (Firebase ID token)
Role: END_USER

End user claims a giftcard using a code. Creates a new wallet item for the user.

Request

{
  "idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
  "code": "XXXX-XXXX-XXXX-XXXX",
  "externalAccountId": "member-12345"
}
Field Type Required Description
idempotencyKey UUID Unique idempotency key
code string Claim code (e.g., ABCD-EFGH-IJKL-MNOP)
externalAccountId string Club membership ID (for linking)

Response

{
  "success": true,
  "data": {
    "walletItemId": "item-550e8400-e29b-41d4",
    "ledgerTxId": "tx-550e8400-e29b-41d4"
  }
}

Errors

Status Code Message
400 INVALID_ARGUMENT Missing required field
401 UNAUTHENTICATED Invalid or missing Firebase ID token
404 NOT_FOUND Code not found, already claimed, or expired
500 INTERNAL Server error

Example

curl -X POST https://your-project.cloudfunctions.net/wallet_claim \
  -H "Authorization: Bearer $ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
    "code": "ABCD-EFGH-IJKL-MNOP"
  }'

Wallet Projection

Get Wallet Projection

Endpoint: GET /wallet_projections/:userId
Authentication: Required (Firebase ID token)
Role: END_USER (own wallet), STAFF, ADMIN (any wallet)

Get aggregated wallet balance and recent transactions.

Path Parameters

Parameter Type Description
userId string User ID to retrieve wallet for

Response

{
  "success": true,
  "data": {
    "userId": "user-john-doe",
    "totalGiftcardBalance": 500,
    "totalRangeTokens": 10,
    "greenfeeTickets": {
      "9_HOLES": 5,
      "18_HOLES": 2
    },
    "activeWalletItems": 8,
    "recentTransactions": [
      {
        "txId": "tx-550e8400",
        "type": "REDEEM",
        "amount": 50,
        "timestamp": "2024-01-15T10:30:00.000Z"
      }
    ],
    "lastUpdatedAt": "2024-01-15T10:30:00.000Z",
    "ledgerVersion": 42
  }
}

Errors

Status Code Message
401 UNAUTHENTICATED Invalid or missing Firebase ID token
403 PERMISSION_DENIED Cannot access this user's wallet
404 NOT_FOUND Wallet projection not found
500 INTERNAL Server error

Example

curl -X GET https://your-project.cloudfunctions.net/wallet_projections/user-john-doe \
  -H "Authorization: Bearer $ID_TOKEN" \
  -H "Content-Type: application/json"

Idempotency & Retries

All POST endpoints support idempotent retries via the idempotencyKey field.

If a request fails, retry with the same idempotencyKey. The server will: 1. Return the cached result if the operation completed 2. Return a failed-precondition if still processing 3. Retry once more if infrastructure error occurred

Example retry logic:

async function issueGiftcardWithRetry(data, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(
        'https://your-project.cloudfunctions.net/giftcards_issue',
        {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${idToken}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(data)
        }
      );

      if (response.ok) return await response.json();

      // 409 conflict or 429 rate limit - retry with same idempotencyKey
      if (response.status === 409 || response.status === 429) {
        await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
        continue;
      }

      throw new Error(`HTTP ${response.status}`);
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
    }
  }
}

Error Handling

All endpoints return standard error responses:

{
  "code": "INVALID_ARGUMENT|UNAUTHENTICATED|PERMISSION_DENIED|NOT_FOUND|INTERNAL|...",
  "message": "Human-readable error description"
}

Error Codes

  • INVALID_ARGUMENT: Request validation failed
  • UNAUTHENTICATED: Firebase ID token invalid or missing
  • PERMISSION_DENIED: User lacks required role
  • NOT_FOUND: Resource not found
  • INTERNAL: Server-side error

OpenAPI/Swagger Specification

Full OpenAPI 3.0 specification available in openapi.yaml:

# Validate schema
npx @stoplight/spectacle-cli build -o ./docs/openapi.html openapi.yaml

# Generate SDK (JavaScript example)
npx openapi-generator-cli generate -i openapi.yaml -g javascript -o ./sdk