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.
Flow at a glance
Section titled “Flow at a glance”- The online store calls
GET /accountto validate that the club has activated donations on that platform - The fundraising cause is displayed as a donation upsell at checkout
- Your online store emits an
orders/paidwebhook to your app after an order is captured. - Your app checks to see if the order contained a donation and if so, calls
POST /donationswith the order details. - When payment of donated funds is made into ASF’s bank account, your app
POST /remittancesto 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.
Source metadata fields
Section titled “Source metadata fields”An Online Store integration may populate these fields on every donation:
| Field | Example | Required |
|---|---|---|
orderNumber | #00010 | No |
orderTotal | 250.00 | No |
customerId | CUS1234567 | No |
orderDate | 2026-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.
Validating Club Activation
Section titled “Validating Club Activation”/accounts/ASF123456?channel=XYZ%20Online%20StorePosting a donation
Section titled “Posting a donation”A complete Online Store donation looks like this:
{ "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" }}Implementation
Section titled “Implementation”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.
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 });};<?php$body = json_decode(file_get_contents('php://input'), true);
$fundraiserId = collect($body['note_attributes'] ?? []) ->firstWhere('name', 'asf_fundraiser_id')['value'] ?? null;
if (!$fundraiserId) { http_response_code(204); exit;}
$correlationId = (string) Str::uuid();$base = getenv('ASF_BASE_URL');$headers = [ 'client_id: ' . getenv('ASF_CLIENT_ID'), 'client_secret: ' . getenv('ASF_CLIENT_SECRET'), 'X-Correlation-ID: ' . $correlationId, 'Content-Type: application/json',];
// 1. Validate$validate = curl_init("$base/fundraisers?channel=XYZ%20Online%20Store&id=$fundraiserId");curl_setopt_array($validate, [CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true]);$validateResponse = curl_exec($validate);
if (curl_getinfo($validate, CURLINFO_HTTP_CODE) !== 200) { error_log("ASF validate failed [correlationId=$correlationId]: $validateResponse"); http_response_code(422); exit;}
// 2. POST /donations — see Node.js example above for the full body shape.Reconciling remittances
Section titled “Reconciling remittances”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:
{ "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.
Things that have bitten partners before
Section titled “Things that have bitten partners before”country_codevs.country. ASF accepts bothAUandAustralia. 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.