top of page

Build the Simpay/Syntch gateway integration with the following architecture and starter code.

IMPORTANT:
- Do not store raw card data.
- Frontend should tokenize the card with Syntch.
- Backend should only receive tokens.
- Store both paymentMethodToken and storedToken.
- Create/find a customer before recurring.
- Store gateway customerKey, recurringScheduleId, firstTransactionId, nextBillingDate.
- Every successful recurring charge must create its own payment/donation record.
- Add idempotency protection.
- Add webhook handling.
- Add reconciliation so missed recurring payments are backfilled.

DATABASE TABLES:

CREATE TABLE payment_customers (
  id BIGSERIAL PRIMARY KEY,
  organization_id BIGINT NOT NULL,
  donor_email TEXT,
  donor_first_name TEXT,
  donor_last_name TEXT,
  donor_phone TEXT,
  gateway TEXT NOT NULL DEFAULT 'syntch',
  gateway_customer_key TEXT NOT NULL,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW(),
  UNIQUE (organization_id, gateway, gateway_customer_key)
);

CREATE TABLE payment_methods (
  id BIGSERIAL PRIMARY KEY,
  organization_id BIGINT NOT NULL,
  customer_id BIGINT REFERENCES payment_customers(id),
  gateway TEXT NOT NULL DEFAULT 'syntch',
  payment_method_token TEXT NOT NULL,
  stored_token TEXT NOT NULL,
  card_brand TEXT,
  last4 TEXT,
  exp_month TEXT,
  exp_year TEXT,
  created_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE payments (
  id BIGSERIAL PRIMARY KEY,
  organization_id BIGINT NOT NULL,
  customer_id BIGINT REFERENCES payment_customers(id),
  payment_method_id BIGINT REFERENCES payment_methods(id),
  recurring_schedule_id BIGINT,
  gateway TEXT NOT NULL DEFAULT 'syntch',
  gateway_transaction_id TEXT UNIQUE,
  amount_cents INTEGER NOT NULL,
  status TEXT NOT NULL,
  source TEXT,
  donor_email TEXT,
  donor_first_name TEXT,
  donor_last_name TEXT,
  donor_phone TEXT,
  created_at TIMESTAMP DEFAULT NOW(),
  settled_at TIMESTAMP
);

CREATE TABLE recurring_schedules (
  id BIGSERIAL PRIMARY KEY,
  organization_id BIGINT NOT NULL,
  customer_id BIGINT REFERENCES payment_customers(id),
  payment_method_id BIGINT REFERENCES payment_methods(id),
  gateway TEXT NOT NULL DEFAULT 'syntch',
  gateway_recurring_schedule_id TEXT NOT NULL,
  amount_cents INTEGER NOT NULL,
  frequency TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'active',
  next_billing_date TIMESTAMP,
  donor_email TEXT,
  donor_first_name TEXT,
  donor_last_name TEXT,
  donor_phone TEXT,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW(),
  UNIQUE (organization_id, gateway, gateway_recurring_schedule_id)
);

CREATE TABLE idempotency_keys (
  id BIGSERIAL PRIMARY KEY,
  organization_id BIGINT NOT NULL,
  idempotency_key TEXT NOT NULL,
  request_hash TEXT NOT NULL,
  response_json JSONB,
  created_at TIMESTAMP DEFAULT NOW(),
  UNIQUE (organization_id, idempotency_key)
);


TYPES:

type SyntchCreateRecurringInput = {
  organizationId: string;
  amountCents: number;
  baseAmount?: number;
  serviceFeeAmount?: number;
  donorCoveredFee?: boolean;

  donorFirstName: string;
  donorLastName: string;
  donorEmail: string;
  donorPhone?: string;

  frequency: 'weekly' | 'biweekly' | 'monthly' | 'quarterly' | 'annually';

  paymentMethodToken: string;
  storedToken: string;
  cardBrand?: string;
  cardType?: string;
  last4?: string;
  expMonth?: string;
  expYear?: string;

  selectedFundIds?: string[];
  source?: string;
};

type SyntchCreateRecurringResponse = {
  donationId?: string;
  firstTransactionId?: string;
  recurringScheduleId: string;
  nextBillingDate?: string;
  message?: string;
};


HELPERS:

function sanitizeCustomerKey(value: string): string {
  return value.replace(/[^a-zA-Z0-9_-]/g, '');
}

function normalizePhone(phone?: string): string | undefined {
  if (!phone) return undefined;
  return phone.replace(/\D/g, '');
}

function requireIdempotencyKey(req: any): string {
  const key = req.headers['idempotency-key'];
  if (!key || typeof key !== 'string') {
    throw new Error('Missing Idempotency-Key header');
  }
  return key;
}

async function withIdempotency({
  db,
  organizationId,
  idempotencyKey,
  requestHash,
  handler,
}: {
  db: any;
  organizationId: string;
  idempotencyKey: string;
  requestHash: string;
  handler: () => Promise<any>;
}) {
  const existing = await db.queryOne(
    `
    SELECT response_json, request_hash
    FROM idempotency_keys
    WHERE organization_id = $1 AND idempotency_key = $2
    `,
    [organizationId, idempotencyKey]
  );

  if (existing) {
    if (existing.request_hash !== requestHash) {
      throw new Error('Idempotency key reused with different request body');
    }

    return existing.response_json;
  }

  const response = await handler();

  await db.query(
    `
    INSERT INTO idempotency_keys (
      organization_id,
      idempotency_key,
      request_hash,
      response_json
    )
    VALUES ($1, $2, $3, $4)
    `,
    [organizationId, idempotencyKey, requestHash, JSON.stringify(response)]
  );

  return response;
}


SYNTH CLIENT:

class SyntchClient {
  private baseUrl: string;
  private apiKey: string;

  constructor() {
    this.baseUrl = process.env.SYNTCH_BASE_URL!;
    this.apiKey = process.env.SYNTCH_API_KEY!;
  }

  private async post(path: string, body: any) {
    const safeLogBody = {
      ...body,
      paymentMethodToken: body.paymentMethodToken ? '[REDACTED]' : undefined,
      storedToken: body.storedToken ? '[REDACTED]' : undefined,
    };

    console.log('Syntch request', {
      path,
      body: safeLogBody,
    });

    const response = await fetch(`${this.baseUrl}${path}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${this.apiKey}`,
      },
      body: JSON.stringify(body),
    });

    const text = await response.text();

    let json: any;
    try {
      json = text ? JSON.parse(text) : {};
    } catch {
      json = { raw: text };
    }

    console.log('Syntch response', {
      path,
      status: response.status,
      body: json,
    });

    if (!response.ok) {
      throw new Error(
        `Syntch error ${response.status}: ${JSON.stringify(json).slice(0, 1000)}`
      );
    }

    return json;
  }

  async createOrUpdateCustomer(input: {
    customerKey: string;
    firstName: string;
    lastName: string;
    email: string;
    phone?: string;
  }) {
    return this.post('/public/customers/create-or-update', {
      customerKey: sanitizeCustomerKey(input.customerKey),
      firstName: input.firstName,
      lastName: input.lastName,
      email: input.email,
      phone: normalizePhone(input.phone),
    });
  }

  async createRecurring(input: SyntchCreateRecurringInput & { customerKey: string }) {
    return this.post('/public/payments/recurring/create', {
      amountCents: input.amountCents,
      baseAmount: input.baseAmount,
      serviceFeeAmount: input.serviceFeeAmount ?? 0,
      donorCoveredFee: input.donorCoveredFee ?? false,

      organizationId: input.organizationId,
      customerKey: sanitizeCustomerKey(input.customerKey),

      donorFirstName: input.donorFirstName,
      donorLastName: input.donorLastName,
      donorEmail: input.donorEmail,
      donorPhone: normalizePhone(input.donorPhone),

      frequency: input.frequency,

      paymentMethodToken: input.paymentMethodToken,
      storedToken: input.storedToken,
      cardBrand: input.cardBrand,
      cardType: input.cardType,
      last4: input.last4,

      selectedFundIds: input.selectedFundIds ?? [],
      source: input.source ?? 'donation_form',
    }) as Promise<SyntchCreateRecurringResponse>;
  }

  async cancelRecurring(input: {
    recurringScheduleId: string;
    organizationId: string;
  }) {
    return this.post('/public/payments/recurring/cancel', {
      organizationId: input.organizationId,
      recurringScheduleId: input.recurringScheduleId,
    });
  }

  async listRecurringTransactions(input: {
    organizationId: string;
    recurringScheduleId?: string;
    startDate: string;
    endDate: string;
  }) {
    return this.post('/public/payments/recurring/transactions', {
      organizationId: input.organizationId,
      recurringScheduleId: input.recurringScheduleId,
      startDate: input.startDate,
      endDate: input.endDate,
    });
  }
}


CREATE RECURRING ENDPOINT:

app.post('/api/syntch/recurring/create', async (req, res) => {
  try {
    const input = req.body as SyntchCreateRecurringInput;
    const idempotencyKey = requireIdempotencyKey(req);

    const requestHash = JSON.stringify({
      ...input,
      paymentMethodToken: '[REDACTED]',
      storedToken: '[REDACTED]',
    });

    const response = await withIdempotency({
      db,
      organizationId: input.organizationId,
      idempotencyKey,
      requestHash,
      handler: async () => {
        const syntch = new SyntchClient();

        const customerKey = sanitizeCustomerKey(
          `${input.organizationId}_${input.donorEmail}_${input.donorPhone || ''}`
        );

        await syntch.createOrUpdateCustomer({
          customerKey,
          firstName: input.donorFirstName,
          lastName: input.donorLastName,
          email: input.donorEmail,
          phone: input.donorPhone,
        });

        const customer = await db.queryOne(
          `
          INSERT INTO payment_customers (
            organization_id,
            donor_email,
            donor_first_name,
            donor_last_name,
            donor_phone,
            gateway,
            gateway_customer_key
          )
          VALUES ($1, $2, $3, $4, $5, 'syntch', $6)
          ON CONFLICT (organization_id, gateway, gateway_customer_key)
          DO UPDATE SET
            donor_email = EXCLUDED.donor_email,
            donor_first_name = EXCLUDED.donor_first_name,
            donor_last_name = EXCLUDED.donor_last_name,
            donor_phone = EXCLUDED.donor_phone,
            updated_at = NOW()
          RETURNING *
          `,
          [
            input.organizationId,
            input.donorEmail,
            input.donorFirstName,
            input.donorLastName,
            normalizePhone(input.donorPhone),
            customerKey,
          ]
        );

        const paymentMethod = await db.queryOne(
          `
          INSERT INTO payment_methods (
            organization_id,
            customer_id,
            gateway,
            payment_method_token,
            stored_token,
            card_brand,
            last4,
            exp_month,
            exp_year
          )
          VALUES ($1, $2, 'syntch', $3, $4, $5, $6, $7, $8)
          RETURNING *
          `,
          [
            input.organizationId,
            customer.id,
            input.paymentMethodToken,
            input.storedToken,
            input.cardBrand || input.cardType,
            input.last4,
            input.expMonth,
            input.expYear,
          ]
        );

        const gatewayResponse = await syntch.createRecurring({
          ...input,
          customerKey,
        });

        const recurringSchedule = await db.queryOne(
          `
          INSERT INTO recurring_schedules (
            organization_id,
            customer_id,
            payment_method_id,
            gateway,
            gateway_recurring_schedule_id,
            amount_cents,
            frequency,
            status,
            next_billing_date,
            donor_email,
            donor_first_name,
            donor_last_name,
            donor_phone
          )
          VALUES ($1, $2, $3, 'syntch', $4, $5, $6, 'active', $7, $8, $9, $10, $11)
          ON CONFLICT (organization_id, gateway, gateway_recurring_schedule_id)
          DO UPDATE SET
            status = 'active',
            next_billing_date = EXCLUDED.next_billing_date,
            updated_at = NOW()
          RETURNING *
          `,
          [
            input.organizationId,
            customer.id,
            paymentMethod.id,
            gatewayResponse.recurringScheduleId,
            input.amountCents,
            input.frequency,
            gatewayResponse.nextBillingDate
              ? new Date(gatewayResponse.nextBillingDate)
              : null,
            input.donorEmail,
            input.donorFirstName,
            input.donorLastName,
            normalizePhone(input.donorPhone),
          ]
        );

        let payment = null;

        if (gatewayResponse.firstTransactionId) {
          payment = await db.queryOne(
            `
            INSERT INTO payments (
              organization_id,
              customer_id,
              payment_method_id,
              recurring_schedule_id,
              gateway,
              gateway_transaction_id,
              amount_cents,
              status,
              source,
              donor_email,
              donor_first_name,
              donor_last_name,
              donor_phone
            )
            VALUES ($1, $2, $3, $4, 'syntch', $5, $6, 'succeeded', $7, $8, $9, $10, $11)
            ON CONFLICT (gateway_transaction_id)
            DO NOTHING
            RETURNING *
            `,
            [
              input.organizationId,
              customer.id,
              paymentMethod.id,
              recurringSchedule.id,
              gatewayResponse.firstTransactionId,
              input.amountCents,
              input.source || 'recurring_initial_charge',
              input.donorEmail,
              input.donorFirstName,
              input.donorLastName,
              normalizePhone(input.donorPhone),
            ]
          );
        }

        return {
          success: true,
          customerId: customer.id,
          paymentMethodId: paymentMethod.id,
          recurringScheduleId: recurringSchedule.id,
          gatewayRecurringScheduleId: gatewayResponse.recurringScheduleId,
          firstTransactionId: gatewayResponse.firstTransactionId,
          paymentId: payment?.id ?? null,
          nextBillingDate: gatewayResponse.nextBillingDate,
          message: gatewayResponse.message,
        };
      },
    });

    res.json(response);
  } catch (error: any) {
    console.error('Create Syntch recurring failed', error);
    res.status(500).json({
      success: false,
      error: error.message || 'Failed to create recurring payment',
    });
  }
});


CANCEL RECURRING ENDPOINT:

app.post('/api/syntch/recurring/cancel', async (req, res) => {
  try {
    const { organizationId, recurringScheduleId } = req.body;
    const idempotencyKey = requireIdempotencyKey(req);

    const response = await withIdempotency({
      db,
      organizationId,
      idempotencyKey,
      requestHash: JSON.stringify(req.body),
      handler: async () => {
        const schedule = await db.queryOne(
          `
          SELECT *
          FROM recurring_schedules
          WHERE id = $1 AND organization_id = $2
          `,
          [recurringScheduleId, organizationId]
        );

        if (!schedule) {
          throw new Error('Recurring schedule not found');
        }

        const syntch = new SyntchClient();

        await syntch.cancelRecurring({
          organizationId,
          recurringScheduleId: schedule.gateway_recurring_schedule_id,
        });

        await db.query(
          `
          UPDATE recurring_schedules
          SET status = 'cancelled', updated_at = NOW()
          WHERE id = $1
          `,
          [recurringScheduleId]
        );

        return {
          success: true,
          recurringScheduleId,
          gatewayRecurringScheduleId: schedule.gateway_recurring_schedule_id,
          status: 'cancelled',
        };
      },
    });

    res.json(response);
  } catch (error: any) {
    console.error('Cancel Syntch recurring failed', error);
    res.status(500).json({
      success: false,
      error: error.message || 'Failed to cancel recurring payment',
    });
  }
});


WEBHOOK ENDPOINT:

app.post('/api/webhooks/syntch', async (req, res) => {
  try {
    const event = req.body;

    console.log('Syntch webhook received', event);

    const eventType = event.type || event.eventType;
    const data = event.data || event;

    switch (eventType) {
      case 'payment_success':
      case 'recurring_charge_success': {
        const gatewayTransactionId = String(data.transactionId);
        const gatewayRecurringScheduleId = data.recurringScheduleId
          ? String(data.recurringScheduleId)
          : null;

        let schedule = null;

        if (gatewayRecurringScheduleId) {
          schedule = await db.queryOne(
            `
            SELECT *
            FROM recurring_schedules
            WHERE gateway = 'syntch'
              AND gateway_recurring_schedule_id = $1
            `,
            [gatewayRecurringScheduleId]
          );
        }

        await db.query(
          `
          INSERT INTO payments (
            organization_id,
            customer_id,
            payment_method_id,
            recurring_schedule_id,
            gateway,
            gateway_transaction_id,
            amount_cents,
            status,
            source,
            donor_email,
            donor_first_name,
            donor_last_name,
            donor_phone,
            settled_at
          )
          VALUES ($1, $2, $3, $4, 'syntch', $5, $6, 'succeeded', 'syntch_webhook', $7, $8, $9, $10, $11)
          ON CONFLICT (gateway_transaction_id)
          DO UPDATE SET
            status = 'succeeded',
            settled_at = COALESCE(EXCLUDED.settled_at, payments.settled_at)
          `,
          [
            schedule?.organization_id || data.organizationId,
            schedule?.customer_id || null,
            schedule?.payment_method_id || null,
            schedule?.id || null,
            gatewayTransactionId,
            Number(data.amountCents),
            schedule?.donor_email || data.donorEmail,
            schedule?.donor_first_name || data.donorFirstName,
            schedule?.donor_last_name || data.donorLastName,
            schedule?.donor_phone || normalizePhone(data.donorPhone),
            data.settledAt ? new Date(data.settledAt) : new Date(),
          ]
        );

        break;
      }

      case 'payment_failed':
      case 'recurring_charge_failed': {
        console.warn('Syntch recurring payment failed', data);

        if (data.recurringScheduleId) {
          await db.query(
            `
            UPDATE recurring_schedules
            SET updated_at = NOW()
            WHERE gateway = 'syntch'
              AND gateway_recurring_schedule_id = $1
            `,
            [String(data.recurringScheduleId)]
          );
        }

        break;
      }

      case 'recurring_cancelled': {
        await db.query(
          `
          UPDATE recurring_schedules
          SET status = 'cancelled', updated_at = NOW()
          WHERE gateway = 'syntch'
            AND gateway_recurring_schedule_id = $1
          `,
          [String(data.recurringScheduleId)]
        );

        break;
      }

      case 'refund':
      case 'chargeback':
      case 'dispute': {
        await db.query(
          `
          UPDATE payments
          SET status = $1
          WHERE gateway = 'syntch'
            AND gateway_transaction_id = $2
          `,
          [eventType, String(data.transactionId)]
        );

        break;
      }

      default:
        console.log('Unhandled Syntch webhook event', eventType);
    }

    res.json({ received: true });
  } catch (error: any) {
    console.error('Syntch webhook failed', error);
    res.status(500).json({
      received: false,
      error: error.message,
    });
  }
});


RECONCILIATION JOB:

async function reconcileSyntchRecurringPayments({
  organizationId,
  startDate,
  endDate,
}: {
  organizationId: string;
  startDate: string;
  endDate: string;
}) {
  const syntch = new SyntchClient();

  const schedules = await db.query(
    `
    SELECT *
    FROM recurring_schedules
    WHERE organization_id = $1
      AND gateway = 'syntch'
      AND status = 'active'
    `,
    [organizationId]
  );

  const results = {
    checked: 0,
    created: 0,
    skipped: 0,
    errors: [] as any[],
  };

  for (const schedule of schedules) {
    results.checked++;

    try {
      const gatewayTransactions = await syntch.listRecurringTransactions({
        organizationId,
        recurringScheduleId: schedule.gateway_recurring_schedule_id,
        startDate,
        endDate,
      });

      const transactions = gatewayTransactions.transactions || gatewayTransactions.data || [];

      for (const tx of transactions) {
        const gatewayTransactionId = String(tx.transactionId || tx.id);

        if (!gatewayTransactionId) {
          results.skipped++;
          continue;
        }

        const existing = await db.queryOne(
          `
          SELECT id
          FROM payments
          WHERE gateway = 'syntch'
            AND gateway_transaction_id = $1
          `,
          [gatewayTransactionId]
        );

        if (existing) {
          results.skipped++;
          continue;
        }

        const amountCents = Number(tx.amountCents || schedule.amount_cents);

        await db.query(
          `
          INSERT INTO payments (
            organization_id,
            customer_id,
            payment_method_id,
            recurring_schedule_id,
            gateway,
            gateway_transaction_id,
            amount_cents,
            status,
            source,
            donor_email,
            donor_first_name,
            donor_last_name,
            donor_phone,
            settled_at
          )
          VALUES ($1, $2, $3, $4, 'syntch', $5, $6, 'succeeded', 'syntch_reconciliation', $7, $8, $9, $10, $11)
          `,
          [
            organizationId,
            schedule.customer_id,
            schedule.payment_method_id,
            schedule.id,
            gatewayTransactionId,
            amountCents,
            schedule.donor_email,
            schedule.donor_first_name,
            schedule.donor_last_name,
            schedule.donor_phone,
            tx.settledAt ? new Date(tx.settledAt) : new Date(),
          ]
        );

        results.created++;
      }
    } catch (error: any) {
      console.error('Syntch reconciliation schedule failed', {
        scheduleId: schedule.id,
        gatewayRecurringScheduleId: schedule.gateway_recurring_schedule_id,
        error: error.message,
      });

      results.errors.push({
        scheduleId: schedule.id,
        gatewayRecurringScheduleId: schedule.gateway_recurring_schedule_id,
        error: error.message,
      });
    }
  }

  return results;
}


MANUAL RECONCILIATION ENDPOINT:

app.post('/api/syntch/recurring/reconcile', async (req, res) => {
  try {
    const { organizationId, startDate, endDate } = req.body;

    const result = await reconcileSyntchRecurringPayments({
      organizationId,
      startDate,
      endDate,
    });

    res.json({
      success: true,
      result,
    });
  } catch (error: any) {
    console.error('Syntch reconciliation failed', error);
    res.status(500).json({
      success: false,
      error: error.message,
    });
  }
});


CRON JOB:

Run this nightly:

cron.schedule('0 3 * * *', async () => {
  const organizations = await db.query(
    `
    SELECT DISTINCT organization_id
    FROM recurring_schedules
    WHERE gateway = 'syntch'
      AND status = 'active'
    `
  );

  const endDate = new Date();
  const startDate = new Date();
  startDate.setDate(startDate.getDate() - 7);

  for (const org of organizations) {
    await reconcileSyntchRecurringPayments({
      organizationId: String(org.organization_id),
      startDate: startDate.toISOString(),
      endDate: endDate.toISOString(),
    });
  }
});


FRONTEND CALL EXAMPLE:

async function createSyntchRecurringDonation(input) {
  const idempotencyKey = crypto.randomUUID();

  const response = await fetch('/api/syntch/recurring/create', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Idempotency-Key': idempotencyKey,
    },
    body: JSON.stringify({
      organizationId: input.organizationId,
      amountCents: input.amountCents,
      baseAmount: input.baseAmount,
      serviceFeeAmount: input.serviceFeeAmount || 0,
      donorCoveredFee: input.donorCoveredFee || false,

      donorFirstName: input.donorFirstName,
      donorLastName: input.donorLastName,
      donorEmail: input.donorEmail,
      donorPhone: input.donorPhone,

      frequency: input.frequency,

      paymentMethodToken: input.paymentMethodToken,
      storedToken: input.storedToken,
      cardBrand: input.cardBrand,
      cardType: input.cardType,
      last4: input.last4,
      expMonth: input.expMonth,
      expYear: input.expYear,

      selectedFundIds: input.selectedFundIds || [],
      source: input.source || 'donation_form',
    }),
  });

  const json = await response.json();

  if (!response.ok || !json.success) {
    throw new Error(json.error || 'Failed to create recurring donation');
  }

  return json;
}


ACCEPTANCE CRITERIA:

1. Backend never receives raw card numbers, CVV, or full expiration details unless Syntch explicitly requires non-PCI proxy handling. Prefer token-only.
2. Customer is created or reused before recurring schedule creation.
3. customerKey is sanitized before sending to Syntch.
4. paymentMethodToken and storedToken are both stored.
5. recurringScheduleId is saved locally.
6. firstTransactionId creates a separate payment/donation record.
7. Every future recurring charge creates a new payment/donation record.
8. Idempotency prevents duplicate schedules and duplicate charges.
9. Webhooks update payment and recurring statuses.
10. Nightly reconciliation creates any missing recurring payment records.
11. Gateway errors are logged with full gateway response, excluding sensitive token/card values.
12. Reporting should read from the payments table, not only the recurring_schedules table.

Givehub.com

GiveHub.com
Acworth, GA 30101

United States
Email: sales@givehub.com
Phone: 866-933-7048

 

MISSION STATEMENT

 

To offer a robust and a superior product suite to help non-profits and churches increase giving and operate more effectively and efficiently with cutting edge technology. 

 

Yours in Christ, 
GiveHub.com Team

  • Facebook Social Icon
  • LinkedIn Social Icon
  • Instagram Social Icon

© 2026 GiveHub.com - All rights reserved - Support - Book a Demo

bottom of page