Bonaventure OgetoBy Bonaventure Ogeto|

Build a Photography Booking and Deposit Flow

Build a photography booking system by collecting session details (date, location, type), requiring a 50% deposit via Paystack to confirm the booking, and sending a balance payment link when the gallery is ready for delivery. Store the customer authorization_code from the deposit to charge the balance without making them re-enter card details.

Booking Form and Deposit Collection

Tables: bookings (client_name, client_email, client_phone, session_type, session_date, location, total_price, deposit_amount, balance_due, status, paystack_deposit_ref, authorization_code, gallery_url).

async function createBookingAndDeposit(clientData, sessionDate, sessionType, totalPrice) {
  var deposit = Math.ceil(totalPrice * 0.5); // 50% deposit
  var reference = 'PHOTO-DEP-' + Date.now();

  var booking = await db.bookings.create({
    ...clientData,
    session_date: sessionDate,
    session_type: sessionType,
    total_price: totalPrice,
    deposit_amount: deposit,
    balance_due: totalPrice - deposit,
    status: 'pending',
    paystack_deposit_ref: reference,
  });

  var res = await fetch('https://api.paystack.co/transaction/initialize', {
    method: 'POST',
    headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email: clientData.email,
      amount: deposit,
      currency: 'NGN',
      reference,
      callback_url: 'https://yoursite.com/bookings/confirmed/' + booking.id,
      metadata: { booking_id: booking.id, payment_type: 'deposit', session_type: sessionType },
      label: 'Photography Deposit - ' + sessionType,
    }),
  });
  return (await res.json()).data.authorization_url;
}

// Webhook: confirm deposit and save auth code
if (event.event === 'charge.success' && event.data.metadata.payment_type === 'deposit') {
  var bookingId = event.data.metadata.booking_id;
  await db.bookings.update(bookingId, {
    status: 'confirmed',
    authorization_code: event.data.authorization.authorization_code, // save for balance charge
  });
  await sendBookingConfirmationEmail(bookingId);
}

Balance Collection on Gallery Delivery

// Photographer marks gallery as ready — triggers balance collection
async function deliverGallery(bookingId, galleryUrl) {
  var booking = await db.bookings.findById(bookingId);
  await db.bookings.update(bookingId, { gallery_url: galleryUrl, status: 'gallery_ready' });

  // Option 1: charge balance automatically using saved auth code
  if (booking.authorization_code) {
    var res = await fetch('https://api.paystack.co/charge/authorization', {
      method: 'POST',
      headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
      body: JSON.stringify({
        email: booking.client_email,
        amount: booking.balance_due,
        authorization_code: booking.authorization_code,
        metadata: { booking_id: bookingId, payment_type: 'balance' },
      }),
    });
    var data = await res.json();
    // If auto-charge fails, fall back to payment link email
    if (!data.status) await sendBalancePaymentLink(booking);
  } else {
    // Option 2: send a payment link for the balance
    await sendBalancePaymentLink(booking);
  }
}

async function sendBalancePaymentLink(booking) {
  var res = await fetch('https://api.paystack.co/transaction/initialize', {
    method: 'POST',
    headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email: booking.client_email,
      amount: booking.balance_due,
      metadata: { booking_id: booking.id, payment_type: 'balance' },
    }),
  });
  var link = (await res.json()).data.authorization_url;
  await sendEmail(booking.client_email, 'Your gallery is ready!', { link, gallery_preview: booking.gallery_url });
}

The gallery link can be gated behind payment: only show the full resolution photos after balance_due = 0 in your database. Show a watermarked preview with a payment prompt until the balance is cleared.

Learn More

See build a hotel booking and deposit system for the same deposit-then-balance pattern in accommodation.

Sign up for the McTaba newsletter

Key Takeaways

  • A 50% deposit protects against no-shows and last-minute cancellations — collect it at booking confirmation.
  • Store the authorization_code from the deposit to charge the balance without a second checkout flow.
  • Send the balance payment link only when the gallery is ready, not immediately after the session.
  • Implement a cancellation policy: deposit is non-refundable within 72 hours of the session date.
  • For photography marketplaces with multiple photographers, use subaccounts to route each photographer's earnings.

Frequently Asked Questions

Should I gate gallery access behind balance payment?
Yes. Only deliver the full-resolution gallery link after the balance is paid. Show watermarked previews or low-resolution samples to the client after the session to demonstrate the quality, but hold back the deliverable files until payment clears. This is standard practice in photography and protects against clients disappearing after receiving their photos.
What if the client cancels after paying the deposit?
Apply your cancellation policy. If the client cancels more than 72 hours before the session, refund 50% of the deposit (you keep 50% for time already invested in planning). Under 72 hours, the deposit is non-refundable. Use the Paystack Refund API with the deposit transaction reference. Clearly state the cancellation policy in your booking confirmation email and terms.
Can I use this system for a photography marketplace with multiple photographers?
Yes. Create a Paystack subaccount for each photographer. When initializing the deposit or balance payment, include the photographer's subaccount. The photographer gets their share automatically at settlement. Your platform keeps the platform fee. Each photographer can have different commission rates and pricing.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.

Apply to the McTaba Marathon