diff --git a/src/tjts5901/app.py b/src/tjts5901/app.py
index e75ff7dca31f826ec1477968efe3c5e4ee0e0b89..09bcb2ff81bc758e63207836faac4f5948622642 100644
--- a/src/tjts5901/app.py
+++ b/src/tjts5901/app.py
@@ -62,6 +62,7 @@ def create_app(config: Optional[Dict] = None) -> Flask:
      # Register blueprints
     from . import views  
     flask_app.register_blueprint(views.bp, url_prefix='')
+    flask_app.register_blueprint(views.api)
 
     # a simple page that says hello
     @flask_app.route('/hello')
diff --git a/src/tjts5901/views.py b/src/tjts5901/views.py
index cd7002eaab2ece09901b2ae0da5d755e9e506008..43a1f6379cc653bcd493795948fecd84296d23a2 100644
--- a/src/tjts5901/views.py
+++ b/src/tjts5901/views.py
@@ -5,17 +5,66 @@ Basic views for Application
 
 import base64
 import random
+import logging
+from typing import Optional
 from datetime import datetime, timedelta
-from flask import Blueprint, flash, redirect, render_template, request, url_for
-from .models import Item, User
+from flask import Blueprint, flash, redirect, render_template, request, url_for, jsonify
+from .models import Item, User, Bid
 from .auth import login_required, current_user
 
 # Main blueprint.
 bp = Blueprint('views', __name__)
+api = Blueprint('api', __name__, url_prefix='/api')
+
+logger = logging.getLogger(__name__)
 
 # Blueprint for documentation.
 docs_bp = Blueprint('docs', __name__)
 
+MIN_BID_INCREMENT = 1
+
+
+def get_winning_bid(item: Item) -> Optional[Bid]:
+    """
+    Return the (currently) winning bid for the given item.
+
+    If there are no bids, or the item is not yet closed, return None.
+
+    :param item: The item to get the winning bid for.
+    :return: The winning bid, or None.
+    """
+
+    winning_bid = None
+    try:
+        winning_bid = Bid.objects(item=item) \
+            .filter(created_at__lt=item.closes_at) \
+            .order_by('-amount') \
+            .first()
+    except Exception as exc:
+        logger.warning("Error getting winning bid: %s", exc, exc_info=True, extra={
+            'item_id': item.id,
+        })
+
+    return winning_bid
+
+
+def get_item_price(item: Item) -> int:
+    """
+    Return the current price of the given item.
+
+    If there are no bids, return the starting bid.
+
+    :param item: The item to get the price for.
+    :return: The current price.
+    """
+
+    winning_bid = get_winning_bid(item)
+    if winning_bid:
+        return winning_bid.amount + MIN_BID_INCREMENT
+    else:
+        return item.starting_bid
+
+
 @bp.route("/")
 def index() -> str:
     """
@@ -137,7 +186,7 @@ def bid(id):
     amount = request.form['amount']
 
     if amount < min_amount:
-        flash(_("Bid must be at least %(min_amount)s", min_amount))
+        flash(("Bid must be at least %(min_amount)s", min_amount))
         return redirect(url_for('views.list_bid'))
 
     if item.closes_at < datetime.utcnow():
@@ -163,3 +212,96 @@ def bid(id):
     return "ok"
 
 
+@api.route('<id>/bids', methods=('GET',))
+@login_required
+def api_item_bids(id):
+    """
+    Get the bids for an item.
+
+    :param id: The id of the item to get bids for.
+    :return: A JSON response containing the bids.
+    """
+
+    item = Item.objects.get_or_404(id=id)
+    bids = []
+    for bid in Bid.objects(item=item).order_by('-amount'):
+        bids.append(bid.to_json())
+
+    return jsonify({
+        'success': True,
+        'bids': bids
+    })
+
+
+@api.route('<id>/bids', methods=('POST',))
+@login_required
+def api_item_place_bid(id):
+    """
+    Place a bid on an item.
+
+    If the bid is valid, create a new bid and return the bid.
+    Otherwise, return an error message.
+    
+    Only accepts `REF_CURRENCY` bids.
+
+    :param id: The id of the item to bid on.
+    :return: A JSON response containing the bid.
+    """
+    
+    item = Item.objects.get_or_404(id=id)
+    min_amount = get_item_price(item)
+
+    try:
+        amount = int(request.form['amount'])
+    except KeyError:
+        return jsonify({
+            'success': False,
+            'error': ("Missing required argument %(argname)s", argname:='amount')
+             })
+    except ValueError:
+        return jsonify({
+            'success': False,
+            'error': ("Invalid value for argument %(argname)s", argname:='amount')
+        })
+    except Exception as exc:
+        return jsonify({
+            'success': False,
+            'error': ("Error parsing argument %(argname)s: %(exc)s", argname:='amount', exc:=exc)
+        })
+
+    if amount < min_amount:
+        return jsonify({
+            'success': False,
+            'error': ("Bid must be at least %(min_amount)s", min_amount:=min_amount)
+        })
+
+    if item.closes_at < datetime.utcnow():
+        return jsonify({
+            'success': False,
+            'error': ("This item is no longer on sale.")
+        })
+
+    try:
+        bid = Bid(
+            item=item,
+            bidder=current_user,
+            amount=amount,
+        )
+        bid.save()
+    except Exception as exc:
+        logger.error("Error placing bid: %s", exc, exc_info=True, extra={
+            'item_id': item.id,
+            'bidder_id': current_user.id,
+            'amount': amount,
+        })
+
+        return jsonify({
+            'success': False,
+            'error': ("Error placing bid: %(exc)s", exc:=exc)
+        })
+
+    print(type(bid))
+    return jsonify({
+        'success': True,
+        'bid': bid.to_json()
+    })