Skip to main content

Yuno IPN / Webhooks

Summary

Yuno notifies any state change in payments and subscriptions against a webhook URL configured on the Yuno side. The webhook is the only reliable source of state: the user redirect to the site may not occur (tab closed, network error), so no approval, cancellation, or refund decision depends on the browser flow.

Payload Structure

Yuno sends a JSON object with a top-level type_event field that identifies the event (for example payment.purchase) and a data field that wraps the main object. For payment events, the object lives in data.payment; for subscription events, in data.subscription. Each carries its id (or code for subscriptions), its status, and a metadata field. The full webhook object reference is in the Yuno documentation.

Yuno metadata has a specific shape: instead of a flat object, it is an array of objects with key and value. Farfalla always reads it with a helper that iterates the array looking for the key; the rest of the code does not assume any other format. The keys that Farfalla includes when creating the checkout and depends on the IPN to read are order_uuid, tenant_id, and, for subscriptions, transaction_type.

Event Types

Yuno emits events in several families. Only two are routed to a handler in Farfalla; the rest enter the endpoint, leave a row in ipn_records, and, when the job tries to process them, fail with an exception from the manager indicating that the driver does not exist.

FamilyStatus in Farfalla
payment.*Routed to the payment handler
subscription.*Routed to the subscription handler
enrollment.*, payout.*, onboarding.*No handler — the job fails when building the manager

Payment events

All payment events go to the same handler, which ignores the event name and is guided by the status of the payment object inside the payload. This means that a payment.purchase with a failed status is treated the same as a payment.cancel: what drives the decision is the normalized status, not the event verb.

Events observed in production are payment.purchase (the large majority of volume), payment.refund, payment.chargeback, and payment.fraud_screening. The manager also accepts payment.authorize, payment.capture, payment.cancel, and payment.verify, but those events do not arrive in the current flow.

Subscription events

Subscription events also all go to the same handler. What the handler uses is the status of the subscription object, normalized to the internal vocabulary.

EventStatus carriedAction applied
subscription.createACTIVE or CREATEDIf the order is pending and the status normalizes to approved, approves the order. CREATED is only considered approved for subscriptions with a free trial
subscription.activeACTIVEApproves the order if it is pending; reactivates if it was cancelled by a previous IPN (see re-approval)
subscription.pausePAUSEDMarks the order as paused
subscription.resumeACTIVEApproves the order if it was pending
subscription.cancelCANCELEDCancels the order if it was approved, recording IPN as the reason

The manager also accepts subscription.complete, but that event does not appear in the current flow.

Status Mapping

For payment events, the handler builds the status by prioritizing the sub_status from the payload over the top-level status. The logic is deliberate: the sub_status describes more precisely the concrete phase of the payment within Yuno (for example, WAITING_ADDITIONAL_STEP when 3DS is pending, or PARTIALLY_REFUNDED when a SUCCEEDED hides a partial refund). For subscription events, the handler uses the status directly.

The resulting value is normalized against Farfalla's internal vocabulary.

Yuno status (top-level or sub_status)Internal status
SUCCEEDED, ACTIVE, APPROVED, COMPLETEDapproved
CREATED (only for subscriptions with free trial)approved
PENDING, PROCESSING, IN_PROGRESSpending
PAUSEDpaused
CANCELED, CANCELLEDcancelled
FAILED, REJECTED, ERRORerror
REFUNDED, PARTIALLY_REFUNDEDrefunded
DISPUTE_LOST, CHARGEBACKdispute_lost
Any other statuspending (fallback)

Yuno emits several statuses not explicitly mapped in the internal enum: DECLINED (the bulk of rejected payments), EXPIRED, READY_TO_PAY, IN_DISPUTE, sub-statuses such as WAITING_ADDITIONAL_STEP or ENROLLMENT_ERROR. All fall to the fallback and are persisted as pending. The decision is intentional: pending is the status Farfalla uses for intermediate or uncertain results, where the charge is not yet considered either approved or lost. Keeping them in pending avoids approving access by mistake and avoids premature cancellations until a subsequent event arrives with a definitive status.

The sub-status PENDING_PROVIDER_CONFIRMATION accompanying a payment.refund with status REFUNDED is handled separately: the payment handler explicitly skips processing and leaves it to the pending refund verification job. More detail in Yuno Refunds — Async flow.

Background Processing

Once the row is persisted in ipn_records, the controller dispatches a job to the high-priority queue with a short delay that depends on the event family.

Event typeDelay
payment.purchase45 seconds
Other payment.*55 seconds
subscription.*20 seconds
Others60 seconds

The delay exists to avoid races with the synchronous checkout flow: if the user completes the redirect and the frontend fires the confirmation to the backend, that flow writes the payment before the webhook tries to do so. The job also uses WithoutOverlapping by row ID with a 180-second expiry, so that two dispatches of the same record do not overlap.

The job has tries = 1. Any exception during processing leaves the row with processed = false and logs the error in activity_log. There is no automatic retry: correction requires manual intervention or a command that iterates unprocessed records and requeues them.

Re-approval of an Already Cancelled Order

There is a specific case in subscription events worth keeping in mind. If a previous IPN cancels the order (for example subscription.cancel or three consecutive failed payments) and then a subscription.active or equivalent arrives with an approved status, the subscription handler does not simply ignore the event: it re-approves the order and clears the cancelled_by, cancelled_by_id, and valid_to fields so that the UserPlan becomes active again.

The condition is strict: it only applies if the previous cancellation was marked with cancelled_by = 'ipn' and the UserPlan has no refund_status recorded. A cancellation originated by the user or an admin is not reactivated by a gateway event, and an order with a refund is not reactivated either even if the subscription becomes active again in Yuno. This covers the scenario of a transient failure in a recurring charge that pauses the subscription and recovers when the next attempt succeeds.

Local Cancellation Due to Failed Attempts

The payment handler also triggers a cancellation when it detects that a recurring charge has accumulated three consecutive failures. At that point, in addition to marking the payment as error, it calls the gateway to pause the subscription in Yuno and cancels the order locally with the IPN reason. The local cancellation is what allows a subsequent subscription.active to trigger the re-approval described above: it leaves the order cancelled but with the correct flag to reactivate.

Activity Logs

Webhooks leave a trace in activity_log under several channels. HTTP-level events (reception, deduplication, payload without ID, order not found) go to yuno_webhooks. Changes to payments and orders applied by the job go to payment_event. If the job makes a call to the Yuno API to resolve the event (for example, querying the original payment during a refund) and that call fails or records an observation, that trace goes to yuno_api. Cancellations due to failed attempts are logged with the payment detail and the subscription status at the time of the decision.

SituationChannel and message
Payload without extractable IDyuno_webhooks — webhook missing event ID
Duplicate webhookyuno_webhooks — repeated IPN from Yuno arrives
Order not found and no tenantyuno_webhooks — Order not found and tenant_id not available
Failed jobIPN activity log — IPN processing failed
Refund pending from providerIPN activity log — skipped, handled by VerifyPendingRefunds
Cancellation due to 3 failurespayment_event — Subscription cancelled because of 3 failed payment tries

Edge Cases

Events from unrouted families. Yuno may send enrollment, payout, or onboarding events to the same URL. The controller accepts them and persists the row in ipn_records, but the manager does not know them. When the job tries to build the driver to process them, it throws an exception and the row is left with processed = false. No harm is done: it simply accumulates rows that require cleanup if Yuno starts emitting those events in production.

Events without event_id. If Yuno sends an event without the identification field corresponding to its family (for example a payment.* with an empty data.payment.id), the ipn_id cannot be constructed and the webhook is discarded with a log entry before being persisted. Yuno receives a 200 and does not retry.

Order not found. The handler logs the order_uuid and payment_id attempted, the row is left with order_id = null and processed = false. There is no automatic retry or proactive alert: it depends on manual log review or general health checks.

Webhook arriving before the synchronous response. If Farfalla is waiting for the HTTP response from a Yuno endpoint and the webhook for the same event arrives first, the job processes the webhook and writes the payment. When the synchronous response arrives, the payment existence checks prevent duplicates. The trace is left in the log for reconciliation.

Job without retry. Any uncontrolled exception during processing leaves the row marked as unprocessed. If the problem was transient (timeout against Yuno, network error during an additional fetch), the row is left behind and requires manual redispatching. A periodic pass that iterates processed = false rows with a certain age and requeues them would be the natural solution, but it does not exist today.

Code References

FileResponsibility
app/Http/Controllers/Commerce/PaymentsIpnController.phpWebhook reception, initial persistence, metadata reading, job dispatch
app/Domains/Commerce/Jobs/ProcessIpn.phpBackground processing, queue, overlap control
app/Managers/YunoEventManager.phpMapping of type_event to the corresponding handler
app/Domains/Commerce/Gateways/Yuno/Events/Payment.phpActions for payment.* events, payment-level deduplication, skip of pending refund
app/Domains/Commerce/Gateways/Yuno/Events/Subscription.phpActions for subscription.* events, normalization with free trial
app/Domains/Commerce/Gateways/Yuno/YunoBaseService.phpConstruction of ipn_id, reading of event_id by family
app/Domains/Commerce/Gateways/Yuno/YunoPaymentStatus.phpStatus enum, grouping helpers
app/Domains/Commerce/Services/PaymentService.phpPayment persistence, approval/cancellation decision, cancellation due to 3 failures
app/Domains/Commerce/Services/UserProductService.phpOrder re-approval after an approved IPN following an IPN-triggered cancellation
app/Domains/Commerce/Models/IPNRecord.phpModel for the persisted row
X

Graph View