Skip to content

Online Store

An online store integration captures donations at checkout on a partner storefront and forwards them to ASF. Online order-specific order metadata in sourceMetadata.

  1. The online store calls GET /account to validate that the club has activated donations on that platform
  2. The fundraising cause is displayed as a donation upsell at checkout
  3. Your online store emits an orders/paid webhook to your app after an order is captured.
  4. Your app checks to see if the order contained a donation and if so, calls POST /donations with the order details.
  5. When payment of donated funds is made into ASF’s bank account, your app POST /remittances to reconcile the bank transaction against the donation references.

The first step is unchanged from Getting started — what makes this channel specific is the shape of sourceMetadata and the trigger that fires the call.

An Online Store integration may populate these fields on every donation:

FieldExampleRequired
orderNumber#00010No
orderTotal250.00No
customerIdCUS1234567No
orderDate2026-05-11T10:09:54+10:00 (ISO 8601)No

Add any further Online Shop identifiers that would aid in the triage of issues or that you want ASF to be able to surface in reporting (lineItemId, etc.) — sourceMetadata accepts arbitrary properties.

/accounts/ASF123456?channel=XYZ%20Online%20Store

A complete Online Store donation looks like this:

POST /donations body
{
"source": {
"channel": "XYZ Online Store",
"fundraiserId": "ASF123456"
},
"donor": {
"email": "john@example.com",
"firstName": "John",
"lastName": "Smith",
"billingAddress": {
"postalCode": "2000",
"country": "AU"
},
"verifiedEmail": "john@example.com"
},
"transaction": {
"id": "504789900",
"date": "2026-05-11T10:09:54+10:00",
"amount": "50.00"
},
"sourceMetadata": {
"orderNumber": "#00010",
"orderTotal": "250.00",
"customerId": "1234567890",
"orderDate": "2026-05-11T10:09:54+10:00"
}
}

Below is the canonical webhook handler. Trim or expand to match your platform; the network call and the field mapping are the load-bearing parts.

app/routes/webhooks.orders-paid.js
import { randomUUID } from 'node:crypto';
import { json } from '@remix-run/node';
import { authenticate } from '~/store.server';
const BASE = process.env.ASF_BASE_URL; // e.g. https://api.asf.org.au/fundraisers/api/v1
function asfHeaders(correlationId) {
return {
client_id: process.env.ASF_CLIENT_ID,
client_secret: process.env.ASF_CLIENT_SECRET,
'X-Correlation-ID': correlationId,
'Content-Type': 'application/json',
};
}
export const action = async ({ request }) => {
const { payload, topic } = await authenticate.webhook(request);
if (topic !== 'ORDERS_PAID') return json({}, { status: 204 });
const fundraiserId = payload.note_attributes
?.find((a) => a.name === 'asf_fundraiser_id')?.value;
if (!fundraiserId) return json({}, { status: 204 });
const correlationId = randomUUID();
// 1. Validate.
const v = await fetch(
`${BASE}/fundraisers?channel=XYZ%20Online%20Store&id=${fundraiserId}`,
{ headers: asfHeaders(correlationId) },
);
if (!v.ok) {
console.error({ correlationId, status: v.status, body: await v.text() });
return json({ error: 'fundraiser-validation-failed' }, { status: 422 });
}
// 2. Post the donation.
const body = {
source: { channel: 'XYZ Online Store', fundraiserId },
donor: {
email: payload.email,
firstName: payload.customer.first_name,
lastName: payload.customer.last_name,
billingAddress: {
postalCode: payload.billing_address.zip,
country: payload.billing_address.country_code,
},
verifiedEmail: payload.customer.verified_email ? payload.email : undefined,
},
transaction: {
id: String(payload.id),
date: payload.processed_at,
amount: payload.total_price,
},
sourceMetadata: {
orderNumber: payload.name,
orderTotal: payload.total_price,
customerId: String(payload.customer.id),
orderDate: payload.created_at,
},
};
const d = await fetch(`${BASE}/donations`, {
method: 'POST',
headers: asfHeaders(correlationId),
body: JSON.stringify(body),
});
if (!d.ok) {
console.error({ correlationId, status: d.status, body: await d.text() });
return json({ error: 'donation-post-failed' }, { status: 502 });
}
const { data } = await d.json();
// Persist data.items.donationReferenceId against the order for later remittance.
return json({ ok: true, donationReferenceId: data.items.donationReferenceId });
};

Online store payments typically settle in batches (usually daily). When ASF receives a batched payout, your reconciliation job should match the payout’s bank reference against the donation references you persisted, and POST /remittances:

POST /remittances body
{
"source": {
"channel": "XYZ Online Store",
"fundraiserId": "ASF123456"
},
"remittances": [
{
"bankReferenceId": "store_payout_2026_05_11",
"amount": 750.00,
"donationIds": ["0000468-000788", "0000468-000789", "0000468-000790"]
}
]
}

A single bankReferenceId can cover many donation references — group by payout, not by donation.

  • country_code vs. country. ASF accepts both AU and Australia. Pick one and stick with it across donations — reporting joins on this field.
  • Donations are non-refundable. As per ASF policy. If registration fees are refunded, the refund amount should exclude any donation amounts.