PFlux DOCS
GitHub Integration enquiries Create a payment link
PAYMENTS/WEBHOOKS

Webhooks

Optional. Give P2Flux an https address and it tells your server when a payment is verified on chain — a hint your server confirms with one call.

OPT-INnotify_url
EVENTSpayment.paid · subscription.*
RETRIES~18 hours
SECRETNone — confirm with the API
A HINT, CONFIRMED BY YOUR SERVER

Give P2Flux an https address when you create a payment or a payment link. When the payment is verified on chain, P2Flux POSTs a short message there. Your server asks the API whether it is true before it ships anything — the SDKs do that in one call.

Overview

Webhooks are optional. Without them nothing changes: your server verifies the transaction the checkout reports, and a scheduled recoverPayment finds payments whose buyer closed the page. With them, P2Flux does that watching for you and tells your server when there is something to confirm — useful when your platform has no reliable scheduled jobs, or for payment links you never see the buyer of.

P2Flux still stores no orders and holds no money. A notification only says “look at this payment”; what decides is the same verification as always.

Ask for notifications

Add notify_url (SDK: notifyUrl) to POST /v1/payments or POST /v1/links. The response repeats it, so you know it was accepted.

const payment = await p2flux.createPayment({
  recipient: merchantWallet,
  amount: '12.50',
  notifyUrl: 'https://shop.example.com/p2flux/notify?order=1042',
})
// store payment.intent and payment.reference with order 1042, as always
$payment = $p2flux->createPayment([
    'recipient'  => $merchantWallet,
    'amount'     => '12.50',
    'notify_url' => 'https://shop.example.com/p2flux/notify?order=1042',
]);
// store $payment['intent'] and $payment['reference'] with order 1042, as always
curl -X POST "$P2FLUX_API_URL/v1/payments" \
  -H "content-type: application/json" \
  -d '{
    "recipient": "0x8fA4bE1a0F2d3C4b5A6978Ee0d1C2b3A4F5e6C21",
    "amount": "12.50",
    "notify_url": "https://shop.example.com/p2flux/notify?order=1042"
  }'
  • A public https URL on the default port: no IP addresses, no user name or password, at most 512 characters. The path and query are yours — an order id in the query is the usual shape.
  • On a payment link it covers every payment through the link. P2Flux keeps the address against the link’s id, not inside the link, so the link and its QR code stay the same length.
  • A deployment that does not offer webhooks refuses the field with INVALID_REQUEST rather than accepting it and staying silent.
  • Watching is capped per caller and in total. Past the cap the payment or link is still created — only without notifications — and the response carries no notify_url. Check for it: when it is missing, fall back to polling for that payment.

Events

TYPESENT WHENCONFIRM WITH
payment.paidA one-time payment (or a payment through a once / reusable link) is verified on chain.POST /v1/payments/verify with the intent and tx_hash from the event
subscription.chargedA subscription link collected a period (the first one included).POST /v1/links/status with the link’s manage token
subscription.updatedA subscriber stopped, was paused, or ended — reason says why (for example PERMISSION_REVOKED).The same
SUBSCRIPTIONS

Grant or remove access from the subscriber the manage view returns — its state and lastPeriod — never “one more period per event”: an old event replayed at your URL is still a real past charge.

event
{
  "id": "0x3c1f…a7d2",
  "type": "payment.paid",
  "created_at": 1791100420,
  "chain_id": 8453,
  "data": {
    "intent": "p2i1.…",
    "reference": "0x9d2c…41be",
    "tx_hash": "0x5e7a…0c19",
    "amount": "12.500000",
    "block_number": "36120445",
    "gas_payment_mode": "native",
    "screening": "clear"
  }
}

Link payments add link_id. Subscription events carry link_id, subscription_id, payer, state, paid_count, and for subscription.charged the period_index and tx_hash. Headers p2flux-event-id and p2flux-event-type repeat the id and type.

Handle a notification

handleNotification takes the raw body and what you know about the order — the reference you stored, or your link’s linkId (plus its manage token for subscription events). It asks the API, and answers confirmed only when the payment is real and yours.

// POST /p2flux/notify?order=1042
app.post('/p2flux/notify', async (req, res) => {
  const order = await orders.get(req.query.order)
  const result = await p2flux.handleNotification(req.rawBody, { reference: order.p2fluxReference })
  if (result.confirmed) {
    await orders.markPaidOnce(order, result.dedupeKey)   // idempotent on dedupeKey, under a lock
  }
  res.sendStatus(200)   // handled; an exception above answers 5xx and it is sent again
})
// notify.php?order=1042
$order  = $orders->find($_GET['order']);
$result = $p2flux->handleNotification(file_get_contents('php://input'), ['reference' => $order->p2flux_reference]);
if ($result['confirmed']) {
    $orders->markPaidOnce($order, $result['dedupe_key']);   // idempotent on dedupe_key, under a lock
}
http_response_code(200);   // handled; an exception above answers 5xx and it is sent again
RESULTMEANING
confirmed: truePaid, verified, and for this order or link. Fulfil idempotently on dedupeKey (reference and transaction; for a subscription, subscription and its latest paid period) — never on the event id, which anyone could make up.
NOT_YOUR_ORDERA real payment, but not this order’s (someone replayed another notification at your URL). Ignore it.
an exceptionNot final yet — the payment is still confirming, or the API could not answer. Answer with a 5xx and the notification comes again.
NOT_CURRENTA subscription charge for an older period than the subscriber’s latest (a replay, or a period that was skipped and never paid). Nothing to grant.
any other reasonNot a payment of yours. Ignore it.

Delivery and retries

  • Answer any 2xx to acknowledge. Anything else — an error, a redirect, or no answer within 10 seconds — is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours, then dropped.
  • The same event can arrive more than once, and anyone can POST to your URL. Fulfil once per dedupeKey, which the SDK builds from the verified payment.
  • Order is not guaranteed between events.
  • Payments are noticed from the chain within a few minutes of settling (once the block is safe). A one-time payment is watched until its intent expires, plus an hour; a link until it expires. A payment settled later is still found by recoverPayment, just not announced.
  • Notifications go only to addresses that resolve to public internet hosts; redirects are not followed.

Why there is no secret

Many payment providers sign their webhooks with a shared secret you have to store and rotate. P2Flux has no merchant accounts and no API keys, so there is nothing to share — and nothing to leak. Instead the message is never trusted on its own: handleNotification confirms it with the API, and the reference or link id it checks is the one P2Flux generated and signed for your order.

IF YOU HANDLE IT YOURSELF

Never mark an order paid from the body alone. Call POST /v1/payments/verify with the event’s intent and tx_hash, require valid: true, require the verdict’s reference to be the one you stored (or to start with your link’s id), and fulfil once per reference and transaction.

Without webhooks

Everything that worked before still works the same way: verify what the checkout reports, and recover anything the browser lost from a scheduled job. Webhooks only take the scheduled job off your hands. See lost callbacks.