# Callbacks (Direct Seller) Reference page: [Callbacks](/payments/getting-started/callbacks) has the payload fields. A direct seller has no partner secret, so it can't check `payme_signature`, and no callback body is ever trusted (CB-01). ## A sale callback, in order 1. Parse the form-encoded body. Record the event. 2. Read `payme_sale_id`, and find **your** order by the ID you stored at `generate-sale`. No match: record it and change nothing, with no query. 3. Query [`get-transactions`](/payments/query-apis/post-get-transactions) with your MPL in the JSON body and your order ID as `transaction_id`. To confirm `sale-authorized`, add `transaction_type: 40`: a default query skipped an uncaptured authorization in the sandbox and returned no rows. 4. If any row names another seller, discard the answer and alert. 5. Keep the rows for your sale ID. Each must agree with your order on order ID, seller and currency where it carries them. For `completed` or `authorized`, at least one row must have `transaction_price` equal to the order amount. 6. The status is the `sale_status` of the newest row by `transaction_created_at`. Apply that and nothing else. 7. Answer below 400 (CB-06). PayMe waits 5 seconds, so if the query can take longer, record the event, answer first and confirm in the background. `get-sales` also reports a sale's status and price, and the reference pages use it for a quick check. These steps use `get-transactions` because its rows carry each amount and action, and `get-sales` leaves out `initial` sales by default. ### Why the query uses your order ID Asking only "is this sale ID completed?" can be beaten. An attacker buys something cheap, keeps the real sale ID, and sends it in a forged callback for an expensive order. That sale really is completed. Finding the order by the ID you stored, querying by your order ID and matching the amount closes that. ## What each callback gets | `notify_type` | What to do | |---|---| | `sale-complete`, `sale-authorized`, `refund`, `sale-failure`, `sale-chargeback`, `sale-chargeback-refund` | Confirm with the steps for a sale callback | | `sub-*` | Confirm with `get-subscriptions`: see [Subscriptions](/agents/steps/subscriptions) | | `seller-*`, `withdrawal-*` | Partner notifications. Record and ignore | ## Handler rules - Answer below 400 for every decision you recorded (CB-06). Any status below 400 counts as delivered, and a 4xx is retried. - Keep side effects (shipping, emails, access) to once per order. Applying PayMe's current status twice is harmless. - Never move a status backwards. The [status table on the partner page](/agents/steps/callbacks-partner#keep-your-status-moving-forward) applies to you too. **`failed` isn't final:** after a decline, a good card on the same page moves the sale to `completed`, so a `failed` order must still be able to become paid. A sale fully refunded in parts stays `partial-refund`, so track the total yourself. - Rate-limit the lookups, because each callback costs a query. Ignore unknown sale IDs without querying, keep one query in flight per order, reuse a fresh result for about 30 seconds, and cap queries overall. A rate-limited genuine callback is caught by the re-check. - Record every callback with its outcome, and alert on runs PayMe doesn't confirm. ## Re-check on a schedule Callbacks can be lost, and repeated failures can make PayMe skip your domain. Run the same function on a timer for: - orders still `initial` or `failed`, or `authorized` for capture sales: every few minutes for the first hour, then hourly for a day; - any order a callback mentioned that you couldn't confirm, including refunds and chargebacks; - authorizations near their limit. Finished orders need it too, because a chargeback or a dashboard refund may only show up on a re-read. PayMe publishes no window for these, so this schedule is a suggestion: once a day, query `get-transactions` with your MPL, `transaction_status: ["refunded", "p-refund", "chargeback", "partial-chargeback", "chargeback-refund", "partial-chargeback-refund"]` and `transaction_created_min` set seven days back. Count each row once by `payme_transaction_id` ([reading the rows](/agents/steps/capture-refund-void#reading-the-rows)). [Reconcile Sales and Withdrawals](/accepting-payments/reporting/reconcile-sales-and-withdrawals) and [Find the Right Data](/accepting-payments/reporting/find-the-right-data) cover the queries. ## Test it `reference/seller-callbacks.ts`, `reference/sale-query-verification.ts` and their test cases in the [integration kit](/integration-kit/index.json) pin this page. Send a forged callback for a real, unpaid order at least once: ```bash curl -X POST https://your-host.example/payme/callbacks \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data 'notify_type=sale-complete&status_code=0&payme_sale_id=YOUR-UNPAID-SALE-ID&transaction_id=YOUR-ORDER-ID&price=5075¤cy=ILS' ``` Expect a status below 400, the event recorded as not confirmed, and the order unchanged.