# Callbacks (Partner) Reference page: [Callbacks](/payments/getting-started/callbacks) has the signature formula, the payload fields and the worked example. This page holds what you must do with them. ## What each callback gets | `notify_type` | Signed? | What to do | |---|---|---| | `sale-complete`, `sale-authorized`, `refund`, `sale-failure`, `sale-chargeback`, `sale-chargeback-refund` | Yes, or null when no transaction exists | Check the signature, then confirm with `get-transactions` | | `sub-*` | Never | Re-read with `get-subscriptions`: see [Subscriptions](/agents/steps/subscriptions) | | `seller-create`, `seller-update`, `seller-approve` | Never | Ignore the body, re-read with `get-sellers` | | `withdrawal-complete` | Never | Ignore the body, re-read with `get-withdrawals`: see [Balances and Payouts](/agents/steps/balances-and-payouts) | ## A sale callback, in order 1. Parse the form-encoded body. Record the event. 2. Check `payme_signature` with a constant-time comparison. | Result | Action | |---|---| | Valid | Worth checking: go on to step 3 | | Wrong, or no secret configured | Record it, change nothing, alert on runs | | Null or empty | No transaction behind it. Record it, and move no money | 3. Find the order by the `payme_sale_id` you stored (CB-05). An unknown ID stops here, with no query, so nobody can make you spend API calls. 4. Query [`get-transactions`](/payments/query-apis/post-get-transactions) with the seller's 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. 5. Apply the [row checks](#checking-the-rows), then apply PayMe's status and nothing from the callback. 6. 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. A signature covers the credentials and two IDs, not the status, the amount or the notification type. A replayed callback with its type or price changed still passes, which is why step 4 is not optional. ### The transaction_id trap On a sale callback, `transaction_id` is **your** order ID and PayMe's ID is in `payme_transaction_id`. On a subscription **return URL**, `transaction_id` is PayMe's. Hashing the wrong one looks exactly like a wrong secret. Keep two separate functions, as `reference/partner-signature.ts` does. ## Checking the rows - If any row names another seller, discard the whole answer and alert. - Keep the rows for your stored 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. - The sale's status is the `sale_status` of the newest row by `transaction_created_at`. Partial capture fails this check, because no row matches the full order amount. That failure is safe. Don't work around it: see [Capture, Refund, Void](/agents/steps/capture-refund-void). ## Keep your status moving forward PayMe publishes no transition table, so use your own conservative one: | From | May move to | |---|---| | `initial` | `authorized`, `completed`, `failed`, `canceled` | | `failed` | `failed`, `authorized`, `completed` | | `authorized` | `completed`, `voided`, `partial-void`, `failed` | | `completed` | `partial-refund`, `refunded`, `chargeback`, `partial-chargeback` | | `partial-refund` | `partial-refund`, `refunded`, `chargeback`, `partial-chargeback` | For anything else, record it and don't guess. A sale fully refunded in parts stays `partial-refund`, so track the total yourself. **`failed` isn't final.** A buyer can retry on the same page after a decline, and the sale then moves to `completed`, or `authorized` for a capture sale. If your table has no way out of `failed`, a paid order is never fulfilled. Reuse the same hosted sale ([Checkout](/agents/steps/checkout)) and keep re-reading it. A failed `capture-sale` also leaves the sale `failed`, and a later capture can still complete it. ## Delivery - Any status below 400 counts as delivered. A 4xx is retried, as is a 5xx or a timeout, up to three attempts in all. - A retry can't fix a bad signature, which is why you answer below 400 when rejecting. - Repeated failures can make PayMe skip callbacks to your domain for a while. Those are not delivered, so reconciling is required. - Seller and withdrawal callbacks go only to your Default Callback URL. Keep one handler there that routes by `notify_type`. - Keep at most one seller re-read in flight per seller, so a flood of unsigned callbacks can't become a flood of API calls. ## Reconcile On a schedule, re-read sales still `initial`, `failed` or `authorized`, any sale a callback mentioned that you couldn't record, and authorizations near their limit. A reasonable schedule is every few minutes for the first hour, then hourly for a day. Finished orders need it too. Callbacks can be skipped, so a chargeback or a refund made in the dashboard may only show up on a re-read. PayMe publishes no window for these, so this schedule is a suggestion: once a day, for each seller, query `get-transactions` with `transaction_status: ["refunded", "p-refund", "chargeback", "partial-chargeback", "chargeback-refund", "partial-chargeback-refund"]` and `transaction_created_min` set seven days back, so a missed run is caught by the next. Match rows to your orders by sale ID, and count each row once by `payme_transaction_id` ([reading the rows](/agents/steps/capture-refund-void#reading-the-rows)). [Reporting Across Your Sellers](/marketplaces-and-platforms/reporting/reporting-across-your-sellers) and [Reconcile Sales and Withdrawals](/accepting-payments/reporting/reconcile-sales-and-withdrawals) cover the queries. ## Test it `reference/partner-callbacks.ts`, `reference/partner-signature.ts` and their test cases in the [integration kit](/integration-kit/index.json) pin this page. Send a forged callback through your public address 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=SALE-X&payme_transaction_id=TRAN-X&transaction_id=order-1&price=5075¤cy=ILS&payme_signature=00000000000000000000000000000000' ``` Expect a status below 400, the event recorded as rejected, and the order unchanged. ### With no public address The return-URL redirect uses the same signature, so checking a real one proves your partner secret and your formula. Posting that payload to your own handler proves the handler. Neither proves delivery: PayMe posting a real callback to you still needs a public address. Report that as not proven.