Birthday Add-On API Contract
This document describes the contract consumed by the Book-Bite birthday add-on screen.
Implementation target repository: hungryhub-team/book-bite.
Backend contract source: src/HHServer/superpowers/plans/2026-07-30-birthday-add-on-hh-server.md.
Transport
- Base URL:
APP_BASE_API_URL, normallyhttps://partners-api.hungryhub.com/api/partner/v1/. - Authentication:
Authorization: Bearer <partner-token>. - The request interceptor converts JavaScript camelCase to API snake_case and the response interceptor converts snake_case back to camelCase.
- Create and update requests are
multipart/form-databecause image fields are uploaded files.
Endpoints
List
GET /birthday_add_ons
The selected restaurant request sends:
restaurant_id=<restaurant-id>
page=1
per_page=10
filter[id]=<restaurant-add-on-id>
filter[name]=<name>
filter[status]=active|inactive
filter[tag]=family_birthday|festive_birthday|romantic_birthday
filter[created_date_from]=YYYY-MM-DD
filter[created_date_to]=YYYY-MM-DD
filter[created_date]=YYYY-MM-DD
filter[updated_date_from]=YYYY-MM-DD
filter[updated_date_to]=YYYY-MM-DD
filter[updated_date]=YYYY-MM-DD
order_by[col]=name|price|tag|id|created_at|updated_at
order_by[sort_by]=asc|desc
The current screen sends restaurant_id, page, per_page, and only the populated filter[...] values. The selected restaurant must be accessible to the authenticated partner.
Response rows are flat attributes inside the restaurant group:
{
"data": {
"restaurant_id": 42,
"restaurant_name": "Example Restaurant",
"free": {
"id": 61,
"add_on_id": 99,
"add_on_type": "birthday",
"is_free_birthday_add_on": true,
"item_type": "items_list",
"price_cents": 0,
"price_currency": "THB",
"pricing_type": "per_item",
"name_en": "Free Birthday",
"cover_image": "https://cdn.example/birthday-cover.jpg",
"birthday_add_on_items": []
},
"premium": [],
"meta": {
"premium": {
"current_page": 1,
"per_page": 10,
"total_pages": 1,
"total_count": 0
}
}
}
}
is_free_birthday_add_on is the authoritative free-row indicator. Book-Bite keeps a price fallback only for compatibility with an older server during rollout.
Detail
GET /birthday_add_ons/:restaurant_add_on_id
The detail response is JSON:API:
{
"data": {
"id": "61",
"type": "birthday_add_on",
"attributes": {
"restaurant_id": 42,
"add_on_id": 99,
"add_on_type": "birthday",
"is_free_birthday_add_on": true,
"item_type": "items_list",
"price_cents": 0,
"price_currency": "THB",
"name_en": "Free Birthday",
"description_en": "Birthday surprise",
"birthday_add_on_items": [
{
"id": 1,
"birthday_item": "cake",
"display_name_en": "Cake",
"description_en": "Birthday cake",
"image": "https://cdn.example/cake.jpg",
"index_number": 1
}
],
"add_on_menus": null
}
}
}
Payment type options
Book-Bite must use the existing partner payment-type endpoint instead of receiving the full payment catalog in every birthday add-on response:
GET /restaurant_packages/payment_types
Response:
{
"data": [
{ "id": 1, "name": "credit_card" },
{ "id": 4, "name": "promptpay" }
]
}
The endpoint applies the restaurant-currency filtering on the server. The birthday add-on serializer returns only the selected payment_types rows needed to hydrate/edit the form; it does not return valid_payment_types or charge-internal fields.
Create
POST /birthday_add_ons
Content-Type: multipart/form-data
The server resolves the restaurant through default_restaurant: an explicit top-level restaurant_id is accepted when present and access-checked, while an omitted value falls back to the authenticated staff member’s default restaurant. For booking create/update, a nested reservation[restaurant_id]=null is also normalized to that default restaurant before the reservation service runs. Do not use a nested add-on restaurant field as request-level context.
Partner booking requests do not need to send dining_occasion_id. When a birthday add-on is selected without an occasion, hh-server automatically assigns the Birthday occasion before applying the shared birthday booking rules. Paid birthday add-ons may still be booked with another explicit occasion; an explicit occasion is preserved.
add_on[name_en]=Premium Birthday
add_on[name_th]=...
add_on[description_en]=Birthday surprise
add_on[description_th]=...
add_on[tag]=family_birthday
add_on[item_type]=items_list
add_on[price_currency]=THB
add_on[original_price_cents]=59900
add_on[limit_per_day]=0
add_on[minimum_booking_time_in_advance]=0
add_on[require_prepayment]=false
add_on[add_on_payment_types_attributes][0][payment_type_id]=<payment-type-id>
add_on[cover_image]=<image-file>
add_on[pricing_attributes][price_cents]=59900
add_on[pricing_attributes][price_currency]=THB
add_on[pricing_attributes][pricing_type]=per_item
add_on[birthday_add_on_items_attributes][0][birthday_item]=cake
add_on[birthday_add_on_items_attributes][0][display_name_en]=Cake
add_on[birthday_add_on_items_attributes][0][description_en]=Birthday cake
add_on[birthday_add_on_items_attributes][0][image]=<image-file>
add_on[birthday_add_on_items_attributes][0][index_number]=1
For item_type=items_image, omit birthday_add_on_items_attributes and send add_on_menus_attributes[index][image] plus index_number. A free add-on uses price_cents=0; only one live, public free birthday add-on is allowed per restaurant. A conflict returns HTTP 409 with code BIRTHDAY_FREE_ADDON_MAX_ONE_REACHED.
Payment settings and time-in-advance
These fields must use the same value contract as the Admin Add-On Settings screen:
| Admin UI state | Wire key | Value contract |
|---|---|---|
requirePrepayment | add_on[require_prepayment] | Boolean. false by default. |
| selected payment types | add_on[add_on_payment_types_attributes] | Repeatable nested attributes; multiple payment types are allowed. |
minBookingTimeInAdvance | add_on[minimum_booking_time_in_advance] | Integer minutes. 0 means Follow Restaurant Level. |
Payment type IDs are integer IDs from GET /restaurant_packages/payment_types; the FE must not submit payment type names or gateway codes in place of IDs. When require_prepayment=true, the form must require at least one selected payment type before submitting. When it is false, the form must submit _destroy=true for existing payment-type junction rows so turning prepayment off also removes stale selections.
Create example with multiple payment types:
restaurant_id=42
add_on[require_prepayment]=true
add_on[add_on_payment_types_attributes][0][payment_type_id]=1
add_on[add_on_payment_types_attributes][1][payment_type_id]=4
add_on[minimum_booking_time_in_advance]=30
Edit example removing an existing type while retaining another:
add_on[add_on_payment_types_attributes][0][id]=17
add_on[add_on_payment_types_attributes][0][payment_type_id]=1
add_on[add_on_payment_types_attributes][1][id]=22
add_on[add_on_payment_types_attributes][1][_destroy]=true
The detail/create/update response exposes the selected junction rows. Options are fetched from the separate payment-type endpoint above:
{
"require_prepayment": true,
"minimum_booking_time_in_advance": 30,
"payment_types": [
{ "id": 17, "payment_type_id": 1, "name": "Credit Card" }
]
}
payment_types[].id is the payment-type junction ID and must be preserved on edit; payment_types[].payment_type_id is the selected option ID. The FE should populate the multi-select from GET /restaurant_packages/payment_types, preserve selected junction IDs, and use _destroy for removed existing rows to avoid duplicate junction records.
The time-in-advance select must mirror Admin’s option builder: 0 (Follow Restaurant Level), then 15/30/45 minutes and hourly values with 15-minute increments. The normal select does not include the Admin-only 1000000 (Unlimited) sentinel unless the product explicitly enables that display mode. The backend resolves 0 to the restaurant’s dine_in_min_booking_time_in_advance on create; a non-zero value remains an add-on-specific override.
Update
PATCH /birthday_add_ons/:restaurant_add_on_id
Content-Type: multipart/form-data
The nested add_on fields use the same names as create. To update active state, send:
add_on[restaurant_add_ons_attributes][0][id]=61
add_on[restaurant_add_ons_attributes][0][active]=false
To delete an existing item or menu image, send its ID with _destroy=true:
add_on[birthday_add_on_items_attributes][0][id]=1
add_on[birthday_add_on_items_attributes][0][_destroy]=true
Delete
DELETE /birthday_add_ons/:restaurant_add_on_id
Successful response:
{ "success": true, "data": null }
Display-mode contract
item_type=items_listusesbirthday_add_on_itemsin the partner response.item_type=items_imageusesadd_on_menusin the partner response.cover_image, itemimage, and menuimageare full URLs. Book-Bite must render them directly and must not prepend another base URL.
Validation failures return HTTP 422 with code=VALIDATION_FAILED. Unauthenticated requests return 401; inaccessible restaurant or record lookups return 404.