Callbacks (Partner)

Reference page: 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
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

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 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, 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

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.

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) and keep re-reading it. A failed capture-sale also leaves the sale failed, and a later capture can still complete it.

Delivery

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). Reporting Across Your Sellers and 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 pin this page. Send a forged callback through your public address at least once:

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&currency=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.