# AI Toolkit ## Start - [Get Your Dedicated Test Account](https://docs.payme.io/agents/start/get-your-test-account): Every integration starts with a dedicated test account. A seller opens one from the dashboard (Switch to test account, then Create test account) or signs up at paid.co.il. A partner writes to partners@payme.io for a sandbox partner key, partner secret and a connected terminal. PayMe publishes no shared test credentials. # Get Your Dedicated Test Account Every integration starts with its own test account, so that what you test is yours and nobody else's sales can affect it. PayMe publishes no shared test credentials. Never copy a seller ID or key from an example, a repository or another integrator. ## If you integrate as a direct seller 1. If you already have a PayMe account, open the dashboard, click your name, then choose **Switch to test account** and **Create test account**. 2. If you have no account, sign up at . 3. Find your keys under **Admin → API & Integration**. The page has a test and production toggle. You need the MPL (`seller_payme_id`) and, for Hosted Fields, the public key from **Hosted Fields JSAPI Credentials**. If that page is missing, [Authentication](https://docs.payme.io/payments/getting-started/authentication#getting-credentials) explains how to add it. ## If you integrate as a partner Write to and ask for: - a sandbox partner key (`payme_client_key`) and partner secret; - a terminal connected to the sandbox account; - the rest of the list on [What to Ask PayMe First](https://docs.payme.io/agents/start/ask-payme-first). ## Rules - Put the values in your environment or secret store. Never paste them into a chat, a ticket, a commit or a log. - Sandbox and production are separate accounts with separate keys. A sandbox key is refused on `live.payme.io`, and a production key is refused on `sandbox.payme.io`. - Examples in these docs use `MPLDEMO-MPLDEMO-MPLDEMO-1234567` as the seller ID. It is an example only. Never integrate against it. Next: [What to Ask PayMe First](https://docs.payme.io/agents/start/ask-payme-first). - [What to Ask PayMe First](https://docs.payme.io/agents/start/ask-payme-first): Before building, ask PayMe (partners@payme.io) whether a terminal is connected to the sandbox account, to set the Default Callback URL per environment, whether Hosted Fields sellers need 3D Secure, whether subscriptions are enabled, what seller_plan values apply, how partial capture behaves, and whether white-label onboarding can be enabled. # What to Ask PayMe First These answers live on PayMe's side, and your code cannot discover them. Send the list as early as you can, and keep the replies with your decisions. A partner writes to . A direct seller uses its PayMe contact if it has one, and otherwise that same address. | Ask | Who needs it | Why it matters | |---|---|---| | Is a terminal connected to our sandbox account? | Both | Without one, every card fails on the payment page with [error 420](https://docs.payme.io/agents/errors/error-420). Nothing in your code fixes it | | Please set our Default Callback URL for sandbox, and later for production | Partner | Seller and withdrawal callbacks go only to that URL. Sale and subscription callbacks fall back to it when a request sends no URL | | Do our sellers need 3D Secure on Hosted Fields? | Both, if using Hosted Fields | The hosted page runs it for you. [3DS with Hosted Fields](https://docs.payme.io/accepting-payments/3d-secure/3ds-with-hosted-fields) is extra work | | Are subscriptions enabled on the account? | Both, if using subscriptions | If not, you get error `357` | | Which `seller_plan` values may we use? | Partner | There is no public list, and an unknown value fails `create-seller`. Leave the field out until you have one | | Can white-label seller onboarding be enabled? | Partner | PayMe hosts the form, so you never hold sellers' ID and bank details | | Does partial capture fit our case, and what happens to the rest of the hold? | Both, if capturing part of an authorization | This is unconfirmed, so don't offer partial capture until PayMe answers | | What are our `sale_price` limits? | Both | The default range is `500` to `4000000`, and PayMe can set different limits per account | | Can you set up a test seller for a successful payout: manual withdrawal plan, approved, with funds available? | Partner, if using payouts | A new sandbox seller came back on automatic plan `2`, so `withdraw-balance` answers `907`. Only PayMe can change that | | Why does `seller_wallets` stay at 0 after completed sandbox sales? | Partner, if using payouts | Seen in the sandbox and not explained. Until PayMe answers, you can't prove a non-zero balance display against the sandbox | Add anything the docs leave open that your scope depends on. If a page here says something is unconfirmed, ask rather than assume. Next: [Rules for Agents](https://docs.payme.io/agents/start/rules-for-agents). - [Rules for Agents](https://docs.payme.io/agents/start/rules-for-agents): Numbered rules with stable IDs for PayMe integrations. SEC secrets stay on the server. MON money is integer minor units. TRN success is status_code 0 in a 2xx response, and money calls with unknown outcomes are not retried. CB callback bodies are never trusted and are confirmed by query. TOK tokens are encrypted bearer credentials. WRK look up fields, sandbox first, claim only what was run. # Rules for Agents Cite these by ID in plans, reviews and reports. The IDs don't change. **P** means partners only, **S** means direct sellers only, and no mark means both. ## Working | ID | Rule | |---|---| | WRK-01 | Look up every field name in these docs before writing it. PayMe's names are not guessable: `amount` or `merchant_id` compile and then fail. | | WRK-02 | Work in the sandbox with your dedicated test account. Never point an integration at production yourself. | | WRK-03 | Never say a flow works unless you ran it against PayMe and checked the final state with a query. | | WRK-04 | If the docs don't answer something, say so and ask. Never invent a field or a behaviour. | | WRK-05 | Keep PayMe's field names as they are in your transport layer. Rename only in your own domain code. | | WRK-06 | Anything you add that moves money or shows payment or seller data needs your app's own login and permission check. If the app has none, stop and ask. If you're working unattended, implement named authentication and explicit roles before exposing these routes, and record your assumptions. | ## Secrets | ID | Rule | |---|---| | SEC-01 | The MPL, the partner key and the partner secret stay on the server. They never reach a browser, a mobile app, a log, a URL, a commit or a chat message. | | SEC-02 | Only the seller's public key goes to the browser. | | SEC-03 | Load credentials from the environment or a secrets manager, and fail at startup when one is missing, without printing it. | | SEC-04 | **P** Never send the partner secret to PayMe on any request. | | SEC-05 | Redact from logs: `payme_client_key`, `seller_payme_id`, `buyer_key`, `sub_payme_id`, `sub_url`, `meta_data_jwt`, card fields and personal data. IDs also appear in URL paths, so log a masked path. | | SEC-06 | Ignore `seller_payme_secret` from `create-seller`. It is a random value that nothing checks. Don't store it. | ## Money | ID | Rule | |---|---| | MON-01 | Amounts are integers in minor units everywhere: `5075` means 50.75. Convert only for display. | | MON-02 | Compute every amount on your server, from your own order. Never take a price from the browser. | | MON-03 | Reject a decimal or out-of-range amount before calling PayMe. `generate-sale` rounds a decimal instead of refusing it. | ## Transport | ID | Rule | |---|---| | TRN-01 | Success is `status_code` 0 in a 2xx response. Compare the numeric value, because it can arrive as a string. Nothing else counts as success. | | TRN-02 | Branch on `status_error_code`, never on the message. | | TRN-03 | Log `status_error_code`, `status_error_details`, `status_additional_info` and `session` on every error, after redacting. | | TRN-04 | Judge a failure by its body, not its HTTP status. `status_code` 1 with a non-zero `status_error_code` means PayMe refused, even at HTTP 500. The outcome is **unknown** after a timeout, a dropped connection, a body that isn't PayMe's JSON, codes `991`, `20002` and `999`, or HTTP 429. Then don't repeat a call that moves money until a query has shown the state. See [Reading a failure](https://docs.payme.io/agents/steps/config-and-transport#reading-a-failure). | | TRN-05 | Always send `seller_payme_id` in the JSON body of a query. Don't rely on a header for it. A direct seller sends a single value. A partner may send an array to read several sellers. | | TRN-06 | Send `language: "en"`, or messages come back in Hebrew. | | TRN-07 | If a money call succeeded and your read-back query then failed, answer success with a "check this" flag, and let reconciliation clear it. Never answer an error: that invites a repeat of a call that worked. | ## Callbacks | ID | Rule | |---|---| | CB-01 | Never apply a callback body. Status, amounts and tokens come from PayMe's answer to your own query. | | CB-02 | A valid sale signature means only "worth checking". It proves neither the status nor the amount. Confirm by query and match it to your own order. | | CB-03 | **P** Check the signature before anything else on a sale callback. A wrong one, or one you could not check, changes nothing. | | CB-04 | Subscription, seller and withdrawal-complete callbacks carry no signature. Don't reject them for that, and don't believe them for that either: re-read from PayMe. | | CB-05 | Find the order by the `payme_sale_id` you stored, and query by your own order ID. A callback naming an unknown ID changes nothing. | | CB-06 | Answer below 400 for every decision you recorded, including rejections. Use 400 or above only when your own system failed before it could record the callback. | | CB-07 | Every value in a callback is a string, and a missing field means "no information", never zero. Update only what is present, and never use a strict schema that rejects unknown fields. | | CB-08 | Callbacks repeat, arrive out of order, and can be skipped. Make side effects happen once, never move a status backwards, and reconcile on a schedule. | | CB-09 | Never fulfil from the return URL. The buyer controls that browser. | | CB-10 | Record every callback with its outcome, including rejected ones, and alert on runs of failures. | ## Tokens | ID | Rule | |---|---| | TOK-01 | A saved-card token is a bearer credential. Encrypt it at rest, never send it to a browser, never log it. | | TOK-02 | **S** Never store a token taken from a callback body. Fetch it with `get-buyer-key` after the sale is confirmed. | | TOK-03 | Never send raw card data unless you are PCI DSS certified and a person has confirmed it. | ## Sales | ID | Rule | |---|---| | SAL-01 | Keep one open sale per order. A hosted sale stays payable while it is `initial` **or `failed`**: after a decline, the same `sale_url` still opens and a good card completes it. Reuse that sale rather than creating another: two payable sales is how a buyer pays twice. | | SAL-02 | Send your order ID as `transaction_id`, and store `payme_sale_id` before anything else can fail. | | SAL-03 | Don't build a scheduler that charges subscriptions. PayMe runs the schedule. | The steps that apply to your integration are in [plan order](https://docs.payme.io/agents) under Steps, and the checks that prove them are under Verify. ## Partner - [Integrate as a Partner](https://docs.payme.io/agents/partner/payme-partner-integration): The procedure for a partner (marketplace or platform) integration: load the rules, inspect the project and keep three record files, interview the person in short rounds, plan and get approval, build in a fixed order, prove it with test cases and the sandbox, and report. Also published as the agent skill payme-partner-integration. # Integrate as a Partner You are adding PayMe to **the user's project**, as a **partner**: a platform that onboards sellers, charges buyers on their behalf and may take a `market_fee`. This page is the procedure. Facts are on the pages it links to, so read them when you reach them, and never rely on memory for a field name (WRK-01). Work through the phases in order. Don't skip phase 2 or 3: an integration built on guessed answers about flows and onboarding is the expensive kind of wrong. ## Phase 1: Load the facts 1. Read [Rules for Agents](https://docs.payme.io/agents/start/rules-for-agents). They bind every later step. 2. Get your step list. With the MCP server, call `plan_integration` with role `partner` and the features chosen in phase 3. Without it, the Steps and Verify sections of the [AI Toolkit](https://docs.payme.io/agents) are in order, and each page says which features it applies to. 3. When PayMe returns an error, use `explain_error` or the page for that code, such as [error 420](https://docs.payme.io/agents/errors/error-420). [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting) covers the rest. ## Phase 2: Understand the project If the working directory has no project, ask where it lives first (Round 0). Inspect, don't ask, for anything the code can tell you: language and framework, how routes and middleware are declared; database, migrations, and existing order, user and seller models; how secrets load; background jobs; test framework; frontend stack and its Content Security Policy; existing payment code. Create the three [record files](#keep-a-record) now, and keep them current to the end. ## Phase 3: Interview **If nobody is available to ask,** take the stated defaults, list each one as unconfirmed in the decisions file, build what doesn't need a person, and report the rest as not proven. Never invent a credential, and don't skip WRK-06 or TOK-03. The same goes for approval in phase 4. Ask with the structured question tool if you have one: at most four questions per round, each self-contained, with a recommended default offered first. Never ask for a secret value. Ask only what the code and docs can't answer, and write each answer to the decisions file as it arrives. ### Round 0: where it lives (only if there's no project) | Question | Why it matters | Default | |---|---|---| | An existing codebase (where), or a new service (which stack)? | There's nothing to inspect otherwise | A new service, in its own repository | ### Round 1: scope | Question | Why it matters | Default | |---|---|---| | Seller onboarding: PayMe's white-label form, or `create-seller` with your own form? | The form needs PayMe to enable it, and you then never hold ID or bank data | `create-seller` | | Checkout: hosted page, Hosted Fields, saved cards? (Several allowed) | Each adds browser code, a payment path and tests | Hosted page | | Capabilities: refunds and voids, authorize and capture, subscriptions, balances and payouts, marketplace-wide saved cards? (Several allowed) | Each is a step page and endpoints to guard | Refunds and voids | | If Hosted Fields: do sellers need 3D Secure? | Without it, part of the payment path is missing | Unknown: build it behind a switch, off, and ask PayMe | ### Round 2: commercials and the account | Question | Why it matters | Default | |---|---|---| | Default `market_fee`, 0 to 60, as a plain number? | It goes on every seller and sale, and `1%` fails | Configurable, 0 | | `seller_plan` values from your account manager? | An unknown value fails `create-seller` | Leave it out | | Do you have a sandbox partner key and partner secret? | Nothing can be proven otherwise. See [Get Your Dedicated Test Account](https://docs.payme.io/agents/start/get-your-test-account) | Build with PayMe mocked, and give the variable names | | Is a terminal connected to your sandbox account? | Without one, every card fails with [error 420](https://docs.payme.io/agents/errors/error-420) | Ask them to confirm with PayMe before payment steps | | How will PayMe reach your callback endpoint? | Seller callbacks arrive only at the Default Callback URL | One endpoint; a tunnel in development | ### Round 3: operations | Question | Why it matters | Default | |---|---|---| | Where will production run? | Decides where the partner secret lives | Secrets manager | | Who calls your money-moving endpoints? | One shared key can't be revoked per person or audited | Named credentials with roles, and an audit log | | One instance or several? | In-process locks prevent double charges in one process only | One instance, enforced at startup | | Who resolves money movements with an unknown outcome? | A person must confirm against PayMe before anything is retried | The finance role | Draft [What to Ask PayMe First](https://docs.payme.io/agents/start/ask-payme-first) for the person as facts come up. Don't wait until the end. ## Phase 4: Plan, then confirm Write a short plan mapping each chosen capability to files in **their** project: modules, routes, data changes and migrations, config keys, frontend changes, scheduled jobs, tests. Follow their conventions. Show it, and wait for approval before writing code. Ask before any destructive migration. ## Phase 5: Build, in this order 1. [Config and Transport](https://docs.payme.io/agents/steps/config-and-transport) 2. [Money](https://docs.payme.io/agents/steps/money) 3. [Seller Onboarding](https://docs.payme.io/agents/steps/seller-onboarding) 4. [Checkout](https://docs.payme.io/agents/steps/checkout) 5. [Callbacks (Partner)](https://docs.payme.io/agents/steps/callbacks-partner), the security-critical step, including scheduled reconciliation 6. Guard every endpoint you added (WRK-06). If the app has no login, stop and ask. If you're working unattended, implement named authentication and explicit roles before exposing these routes, and record your assumptions. 7. As chosen: [Capture, Refund, Void](https://docs.payme.io/agents/steps/capture-refund-void), [Saved Cards](https://docs.payme.io/agents/steps/saved-cards), [Subscriptions](https://docs.payme.io/agents/steps/subscriptions), [Balances and Payouts](https://docs.payme.io/agents/steps/balances-and-payouts) Port from the [reference code](https://docs.payme.io/agents/verify/test-cases-and-reference-code) where it helps. Check your code against [Implementation Traps](https://docs.payme.io/agents/steps/implementation-traps) before writing the connecting code. ## Phase 6: Prove it 1. Port **every** test case for your role, and write the tests for your own code: [Test Cases and Reference Code](https://docs.payme.io/agents/verify/test-cases-and-reference-code). 2. Run the whole test suite and the linter. Fix what you broke. 3. Check [Implementation Traps](https://docs.payme.io/agents/steps/implementation-traps) again, and commission a reviewer who didn't write the code. Fix or record every finding. 4. Run the [Readiness Preflight](https://docs.payme.io/agents/verify/readiness-preflight). 5. Run the [Sandbox Walk-through](https://docs.payme.io/agents/verify/sandbox-walk-through) with the person. Confirm every final state with a query. ## Phase 7: Report Fill in the [Proof-of-Integration Report](https://docs.payme.io/agents/verify/proof-of-integration-report). Tell the person what was built by capability, which files changed, the config keys per environment, how to run the tests, which sandbox steps ran and which remain, and every departure from these docs with its reason. Point them to the [Go-Live Checklist](https://docs.payme.io/agents/verify/go-live-checklist). ## Keep a record Create these in the user's project. They will ask for them. **`INTEGRATION-DECISIONS.md`**, with three tables: - **Questions and answers:** number, question, answer, consequence in the code. - **Decisions taken without asking:** number, decision, why. List every default you chose, so it can be overturned. - **Open issues:** number, issue, status, owner and next step. Close rows rather than deleting them. **`FIXES-MANIFEST.csv`**, one row per mistake (yours, the review's, the docs') and per action someone must take: ``` id,found_in,type,severity,description,what_needs_to_be_done,owner,status ``` `type` is one of: `bug in our code`, `docs gap`, `docs defect`, `spec vs reality`, `finding from sandbox`, `hardening`, `action`, `question`, `verification`. **`SANDBOX-REPORT.md`**: only the steps that actually ran against PayMe, with pass, fail or finding, and the steps not yet run. ## When something disagrees If a page and PayMe's real behaviour disagree, trust what PayMe did, record it as `spec vs reality`, and tell the person. If nothing covers a question, say so and ask (WRK-04). Working style that helps: run independent work in parallel with narrow file ownership, browser-test buyer pages, report in the person's language, and never commit unless asked. ## Seller - [Integrate as a Direct Seller](https://docs.payme.io/agents/seller/payme-seller-integration): The procedure for a direct-seller integration, using one credential, the MPL: load the rules, inspect the project, ask the person a few questions, plan and get approval, build in a fixed order, prove it with test cases and the sandbox, and report. Callbacks are confirmed by query. Also published as the agent skill payme-seller-integration. # Integrate as a Direct Seller You are adding PayMe to **the user's project** for a business that sells on its own account. Its only secret PayMe credential is its **MPL**. It has no partner secret, so it can't check callback signatures, and every callback is confirmed by query. This page is the procedure. Facts are on the pages it links to, so read them when you reach them, and never rely on memory for a field name (WRK-01). ## Phase 1: Load the facts 1. Read [Rules for Agents](https://docs.payme.io/agents/start/rules-for-agents). They bind every later step. 2. Get your step list. With the MCP server, call `plan_integration` with role `seller` and the features chosen in phase 3. Without it, the Steps and Verify sections of the [AI Toolkit](https://docs.payme.io/agents) are in order, and each page says which features it applies to. 3. When PayMe returns an error, use `explain_error` or the page for that code, such as [error 420](https://docs.payme.io/agents/errors/error-420). [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting) covers the rest. ## Phase 2: Understand the project Inspect, don't ask, for anything the code can tell you: language and framework, how routes, middleware and background jobs are declared; database, migrations, and existing order, customer and subscription models; how secrets load; test framework and linter; frontend stack and its Content Security Policy; existing payment code. Keep three record files in the project from now on: `INTEGRATION-DECISIONS.md`, `FIXES-MANIFEST.csv` and `SANDBOX-REPORT.md`. Their format is on the partner page, under [Keep a record](https://docs.payme.io/agents/partner/payme-partner-integration#keep-a-record), and applies to you too. ## Phase 3: Ask the person **If nobody is available to ask,** take the stated defaults, list each one as unconfirmed in the decisions file, build what doesn't need a person, and report the rest as not proven. Never invent a credential, and don't skip WRK-06 or TOK-03. The same goes for approval in phase 4. One message with all the questions. Ask only what the code can't answer, never ask for a secret value, and record each answer. | Question | Why it matters | Default | |---|---|---| | Checkout: hosted page, Hosted Fields, saved cards? | Each adds browser code, a payment path and tests | Hosted page | | If Hosted Fields: does the account need 3D Secure? | Without it, part of the payment path is missing | Unknown: ask PayMe | | Capabilities: refunds and voids, authorize and capture, subscriptions? | Each is a step page and endpoints to guard | Refunds and voids | | Do you have a dedicated sandbox account, with its MPL and, for Hosted Fields, its public key? | Nothing can be proven otherwise. See [Get Your Dedicated Test Account](https://docs.payme.io/agents/start/get-your-test-account) | Build with PayMe mocked, and give the variable names | | Is a terminal connected to it? | Without one, every card fails with [error 420](https://docs.payme.io/agents/errors/error-420) | Ask them to confirm with PayMe | | What public address will PayMe post to, and what address do buyers return to? | Callbacks need one, and return pages need the other | A tunnel in development | ## Phase 4: Plan, then confirm Write a short plan mapping each chosen capability to files in **their** project: modules, routes, data changes and migrations, config keys, frontend changes, scheduled jobs, tests. Follow their conventions. Show it, and wait for approval before writing code. Ask before any destructive migration. ## Phase 5: Build, in this order 1. [Config and Transport](https://docs.payme.io/agents/steps/config-and-transport) 2. [Money](https://docs.payme.io/agents/steps/money) 3. [Checkout](https://docs.payme.io/agents/steps/checkout) 4. [Callbacks (Direct Seller)](https://docs.payme.io/agents/steps/callbacks-seller), the security-critical step, including the scheduled re-check and the status table it links to 5. Guard every endpoint you added (WRK-06). If the app has no login, stop and ask. If you're working unattended, implement named authentication and explicit roles before exposing these routes, and record your assumptions. 6. As chosen: [Capture, Refund, Void](https://docs.payme.io/agents/steps/capture-refund-void), [Saved Cards](https://docs.payme.io/agents/steps/saved-cards), [Subscriptions](https://docs.payme.io/agents/steps/subscriptions) Port from the [reference code](https://docs.payme.io/agents/verify/test-cases-and-reference-code) where it helps. Check your code against [Implementation Traps](https://docs.payme.io/agents/steps/implementation-traps) before writing the connecting code. ## Phase 6: Prove it 1. Port **every** test case for your role, and write the tests for your own code: [Test Cases and Reference Code](https://docs.payme.io/agents/verify/test-cases-and-reference-code). 2. Run the whole test suite and the linter. Fix what you broke. 3. Commission a reviewer who didn't write the code, against [Implementation Traps](https://docs.payme.io/agents/steps/implementation-traps) and these docs. Fix or record every finding. 4. Run the [Readiness Preflight](https://docs.payme.io/agents/verify/readiness-preflight). 5. Run the [Sandbox Walk-through](https://docs.payme.io/agents/verify/sandbox-walk-through) with the person. Confirm every final state with a query. ## Phase 7: Report Fill in the [Proof-of-Integration Report](https://docs.payme.io/agents/verify/proof-of-integration-report). Tell the person what was built by capability, which files changed, the config keys per environment, how to run the tests, which sandbox steps ran and which remain, and every departure from these docs with its reason. Point them to the [Go-Live Checklist](https://docs.payme.io/agents/verify/go-live-checklist). ## When something disagrees If a page and PayMe's real behaviour disagree, trust what PayMe did, record it, and tell the person. If nothing covers a question, say so and ask (WRK-04). ## Steps - [Config and Transport](https://docs.payme.io/agents/steps/config-and-transport): Build configuration and one PayMe client first. Choose the environment from config, keep credentials out of code, treat status_code 0 in a 2xx response as the only success, read a failure by its body (HTTP 500 with status_code 1 and an error code is a refusal), do not retry money calls with an unknown outcome, omit local callback URLs (error 21), keep two public URL settings, and use dd/mm/yyyy on seller and subscription dates but yyyy-mm-dd hh:mm:ss on query date filters. # Config and Transport Reference pages: [Authentication](https://docs.payme.io/payments/getting-started/authentication), [Environments](https://docs.payme.io/payments/getting-started/environments), [Errors](https://docs.payme.io/payments/getting-started/errors), [Idempotency and Retries](https://docs.payme.io/payments/getting-started/idempotency-and-retries). This page holds only the order and the traps. ## Configuration 1. Choose the environment from one setting, such as `PAYME_ENV`. The base URL and the Hosted Fields `testMode` both follow it, because a sandbox token can't pay a live sale. 2. Load credentials by name from the environment or a secrets manager, and refuse to start without them (SEC-03). 3. Keep two public addresses: one PayMe's servers can reach for callbacks, and one the buyer's browser reaches for return pages. In development these are usually different. | Role | Values to configure | |---|---| | Direct seller | MPL, public key if using Hosted Fields, the two public addresses | | Partner | Partner key, partner secret, the two public addresses. Each seller's MPL and public key belong with that seller's record | ## The client Write one client for every call, with these behaviours: - `POST` with a JSON body and `Content-Type: application/json`, except the few endpoints that use another method or headers (their reference pages say so). - Success per TRN-01. A non-2xx is a failure even if its body says `status_code: 0`. - `pay-sale` can answer `status_code: 5` with a `redirect_url`. That is a 3D Secure challenge, not a failure. - One error type that carries the code, details, additional information and `session`. Support asks for `session` first. - A request timeout, and a distinct "outcome unknown" result, decided as in [Reading a failure](#reading-a-failure) (TRN-04). - Redaction per SEC-05. PayMe sometimes echoes a value inside an error message, so scrub what you sent from what it returns before logging. `reference/payme-client.ts` and `reference/payme-error.ts` in the [integration kit](https://docs.payme.io/integration-kit/index.json) implement this. ## Reading a failure The HTTP status doesn't decide this. PayMe sends ordinary refusals as HTTP 500 with a full error body, and a declined card as HTTP 200. Go down the table and stop at the first row that matches. | What came back | What it means | What to do | |---|---|---| | `status_code` 0 in a 2xx | Success (TRN-01) | Carry on | | HTTP 429, usually with code `998` | This call was turned away, but a matching call may still be running: a retried `pay-sale` can be | Wait for `Retry-After` if it was sent, re-read the state, and repeat only if the read shows it didn't happen. See [error 998](https://docs.payme.io/agents/errors/error-998) | | Code `991`, `20002` or `999`, at any HTTP status | **Unknown.** `991` and `20002` mean contact was lost part-way, and the sale can show `failed` even so. `999` gives no information | Re-read, block repeats, and have a person check with PayMe, quoting `session` | | `status_code` 1 and any other non-zero `status_error_code`, at any HTTP status, or any other 4xx | **Refused** | Show it, fix the request or offer another try. Don't freeze the order | | Anything else: a timeout, a dropped connection, a body that isn't JSON, a gateway error page, JSON with no error code, or code `0` with `status_code` 1 | **Unknown.** PayMe may have acted | Re-read, and don't repeat a money call until the read settles it. Only a connection that was refused or never resolved is known to be unsent | Before offering a refused money call again, re-read the sale once. It costs nothing, and PayMe publishes no promise that a code means no money moved: work continues after the card company answers. If the read disagrees with the refusal, trust the read. ## URLs PayMe checks PayMe validates `sale_callback_url` and `sale_return_url` when it receives the request, so a bad value fails the whole `generate-sale` with [error 21](https://docs.payme.io/agents/errors/error-21). In the sandbox this happened for `localhost` in either field and for a private address. - When an address is local, leave the field out and log that you did. PayMe then falls back to a URL held on its side for the account. A partner asks PayMe to set it. A direct seller has no call to read or set it, and whether its account has one is unconfirmed, so expect no callback and rely on the [scheduled re-check](https://docs.payme.io/agents/steps/callbacks-seller#re-check-on-a-schedule). - To receive real callbacks in development, run a [tunnel](#a-tunnel-for-callbacks-in-development) and use its public address. - Put your order ID in the path of a return URL. PayMe appends its own query parameters. ## A tunnel for callbacks in development PayMe hasn't run this recipe end to end. It follows Cloudflare's documentation, so if a step looks different, follow theirs. An agent's permissions may block the download or the tunnel. If so, give a person these commands and ask them to paste back only the public address, which isn't a secret. 1. Download `cloudflared` from [Cloudflare's downloads page](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/). On 64-bit Windows, the file is `cloudflared-windows-amd64.exe`. 2. Start your server. Then, in a terminal of its own, start the tunnel with your server's port: ```powershell # Windows, in the folder holding the download .\cloudflared-windows-amd64.exe tunnel --url http://localhost: ``` ```bash # macOS or Linux, once installed cloudflared tunnel --url http://localhost: ``` 3. Read the public address it prints, an `https://` address ending in `trycloudflare.com`, and leave the process running. 4. Send that address plus your callback path as `sale_callback_url`, and restart your server if it loads settings at startup. 5. Call your health route through the public address, so the request comes in from outside. On Windows, type `curl.exe`: ```bash curl -i /health ``` | Result | Meaning | |---|---| | 200 | Good | | 502 | The tunnel is up and your server is down | | Cloudflare error 1033 | The tunnel process has gone | A quick tunnel's address stops working when the process stops, and the next run gets a new one, so update `sale_callback_url` each time. Anyone with the address can reach your server, so use it for development only. A per-sale `sale_callback_url` needs nothing from PayMe. The Default Callback URL, where seller callbacks arrive, does: only PayMe can set it. ## Dates | Where | Format | |---|---| | `seller_birthdate`, `seller_social_id_issued`, `sub_start_date` | `dd/mm/yyyy` | | Query date filters | `yyyy-mm-dd hh:mm:ss`, Asia/Jerusalem local time, not UTC | | Dates in responses | `yyyy-mm-dd hh:mm:ss`, with a space and no `T` | - [Money](https://docs.payme.io/agents/steps/money): PayMe amounts are integers in minor units. generate-sale rounds a decimal sale_price rather than refusing it, so 600.75 becomes 601 and charges 6.01, and 50.75 becomes 51 and fails with error 352. The default range is 500 to 4000000 per account. Convert only for display, and reject bad amounts before calling PayMe. # Money `sale_price: 5075` means 50.75, in agorot for ILS and cents for USD or EUR (MON-01). ## What goes wrong `generate-sale` does not refuse a decimal. It rounds it and then checks the range. These are from the sandbox: | You send | PayMe does | |---|---| | `600.75` | Creates a sale for `601`, which charges 6.01 | | `50.75` | Rounds to `51`, then refuses it with [error 352](https://docs.payme.io/agents/errors/error-352) | An amount in whole currency units is also accepted when it's in range, and charges a hundredth of what you meant: `600` charges 6.00, not 600.00. These mistakes produce a working integration that charges the wrong amount, so check them yourself before calling PayMe (MON-03). ## Limits - The default `sale_price` range is `500` to `4000000`, the same in every currency. PayMe can set different limits per account. - `sale_refund_amount` must be within the same range. - A token sale has its price set by PayMe. `sale_type: seller-token` answered `price: 100` in the sandbox whatever was sent. - On `capture-sale`, a decimal `sale_price` can fail with [error 42](https://docs.payme.io/agents/errors/error-42). ## The one display value Hosted Fields `total.amount.value` is a decimal string such as `"50.75"`. It only sets what the buyer sees. The amount charged comes from the sale on your server, so keep the two equal. ## In your code - Keep integer minor units everywhere, and convert only at the display edge. - Use an integer type for `sale_price`. A client generated from OpenAPI may type it as a number or float. - Search your code for `/ 100` and `* 100` outside the display layer. - Accept digits only when parsing minor units. A general number parser also accepts `5e2`, `0x1f4` and values with spaces. `reference/money.ts` and `test-vectors/money.json` in the [integration kit](https://docs.payme.io/integration-kit/index.json) implement and pin these rules. - [Seller Onboarding](https://docs.payme.io/agents/steps/seller-onboarding): Partners onboard sellers through PayMe's white-label form or create-seller. In the sandbox seller_phone 0501234567 failed with error 25 and +972501234567 passed. Dates are dd/mm/yyyy. Store seller_payme_id and the public key, ignore seller_payme_secret, read state from get-sellers, and never block checkout on approval, which gates payouts only. # Seller Onboarding Reference pages: [Create Seller](https://docs.payme.io/payments/merchants-management/post-create-seller) for the fields, [How to Create New Sellers](https://docs.payme.io/marketplaces-and-platforms/seller-onboarding/create-seller) for the white-label form, [Test Cards and Payment Methods](https://docs.payme.io/payments/getting-started/test-cards-and-payment-methods#creating-test-sellers-partners) for sandbox values. ## Choose the path first | | PayMe's white-label form | `create-seller` API | |---|---|---| | Who builds the form | PayMe | You | | ID and bank details pass through your servers | No | Yes | | Availability | PayMe enables it on request | Every partner | Ask the person which path they want. Prefer the white-label form when PayMe has enabled it. Everything after the MPL exists applies to both. ## What create-seller rejects These are from the sandbox: | Sent | Result | |---|---| | `seller_phone: "0501234567"` | Refused: [error 25](https://docs.payme.io/agents/errors/error-25) | | `seller_phone: "+972501234567"` | Accepted | | `seller_birthdate: "1990-01-01"` | Refused: [error 116](https://docs.payme.io/agents/errors/error-116). Dates are `dd/mm/yyyy` | | `seller_inc: 2` with `seller_inc_code` different from `seller_social_id` | Refused: [error 114](https://docs.payme.io/agents/errors/error-114) | | A `seller_inc_code` that isn't numeric | Refused: [error 121](https://docs.payme.io/agents/errors/error-121) | Validate these before calling PayMe. Other rules: - For individuals, sole proprietors and exempt dealers, `seller_inc_code` must equal `seller_social_id`. - A plan can require or refuse a field, and [error 790](https://docs.payme.io/agents/errors/error-790) names it. `seller_id` is the usual one. Correlate on `seller_payme_id`, which always works. - Leave out `seller_plan` until your account manager gives you a value. - `market_fee` is a plain number of percent, such as `2.5`: see [Platform Fees](https://docs.payme.io/marketplaces-and-platforms/fees-and-payouts/platform-fees). ## What to store Save the response before anything else can fail. | Field | What to do | |---|---| | `seller_payme_id` | Store it. You can read it again from `get-sellers` | | `seller_public_key.uuid` | Store it. You can read it again with [Get Seller Public Key](https://docs.payme.io/payments/merchants-management/get-sellers-mpl-public-keys) | | `seller_dashboard_signup_link` | Store it exactly as returned and show it to the seller, who finishes onboarding there. Its host depends on your partner account and may differ from the API host, so don't build it yourself | | `seller_payme_secret` | Ignore it (SEC-06) | ## Approval gates payouts, not payments A new partner-created seller is active and not approved. In the sandbox a sale on an unapproved seller completed. - Never block checkout on approval. - Show the seller its approval state and its signup link, because a seller who hasn't finished can't be paid out. ## Reading state back [`get-sellers`](https://docs.payme.io/payments/query-apis/post-get-sellers) is the source of truth for approval, fees, currencies and balances. They change without notice, so keep at most a display copy. - A JSON array of `seller_payme_id` fetched several sellers in one call in the sandbox. Use it rather than one call per seller. - `seller_fees.fee_market_fee` shows the fee PayMe holds for that seller. - Seller callbacks are unsigned: see [Callbacks (Partner)](https://docs.payme.io/agents/steps/callbacks-partner). - [Checkout](https://docs.payme.io/agents/steps/checkout): Every PayMe flow starts with generate-sale. Default to the hosted page, add Hosted Fields when the checkout must look native, use a saved token only when no buyer is present. Keep one open sale per order: a failed hosted sale stays payable, so reuse it, send your order ID as transaction_id, store payme_sale_id, fulfil only after a query confirms payment, and use sale_expiry_seconds to limit how long a payment page stays open. # Checkout Reference pages: [Payment Options](https://docs.payme.io/payments/getting-started/integration-options), [Generate Payment](https://docs.payme.io/payments/hosted-payment-page/post-generate-sale), [Hosted Payment Page options](https://docs.payme.io/accepting-payments/checkout-and-in-person/hosted-payment-page-options), [Hosted Fields JSAPI Guide](https://docs.payme.io/accepting-payments/checkout-and-in-person/hosted-fields-jsapi-guide), [Pay Sale](https://docs.payme.io/payments/direct-api/post-pay-sale). ## Choose | | Hosted page | Hosted Fields | Saved token | |---|---|---|---| | Card typed in | PayMe's page, in an iframe or a redirect | PayMe's iframes, in your form | Nowhere | | Buyer present | Yes | Yes | No | | Calls | `generate-sale` | `generate-sale`, browser `tokenize`, `pay-sale` | `generate-sale` with the token | Default to the hosted page. Add Hosted Fields only when the checkout must look native. Use a token only when no buyer is present (TOK-03 covers raw cards). ## Every flow 1. Create the sale on your server. A partner uses the **seller's** MPL: the partner's own MPL fails with [error 174](https://docs.payme.io/agents/errors/error-174). Amounts follow [Money](https://docs.payme.io/agents/steps/money). 2. Send your order ID as `transaction_id`, and store `payme_sale_id` against the order (SAL-02). 3. Keep one open sale per order (SAL-01). **`failed` isn't final.** After a decline the sale is `failed`, but the same `sale_url` still opens, and a good card then completes it. Send the buyer back to that page, don't open a second one, and keep re-reading the sale. Create a new sale only once the previous one is `voided` or `canceled`, or its `sale_expiry_seconds` has passed. 4. Take a callback or a return page only as a signal to check. Fulfil after a query confirms it (CB-01, CB-09). ## Limiting a payment page - `sale_expiry_seconds` closes the page. In the sandbox, a page with `sale_expiry_seconds: 5` answered HTTP 403 after 7 seconds, and a `failed` sale's page closed the same way once its time passed. Paying an expired sale fails with error `367`. Set it when you create the sale: it's the only way to close an ordinary open page. - An ordinary open sale has no cancel call. `cancel-template` is for template sales only, and a void needs an authorized sale. - `sale_should_be_opened_once` applies to pages that capture a card without charging it. An ordinary sale page still opened twice in the sandbox, so don't rely on it there. ## Hosted page - Show `sale_url` in an iframe, or redirect to it. If your site sets a Content Security Policy, its `frame-src` must allow that address. - Pre-filling fields and iframe redirects are on [Hosted Payment Page options](https://docs.payme.io/accepting-payments/checkout-and-in-person/hosted-payment-page-options). - On an Israeli card, a Social ID field replaces Zip Code as soon as the card number is typed. When automating a test, fill whichever of the two is showing. - `browser/hosted-page.html` in the [integration kit](https://docs.payme.io/integration-kit/index.json) is a complete example. ## Hosted Fields - `PayMe.create` takes the seller's public key, never an MPL or a partner key. - One instance gets one `tokenize()`. After any failure, clear the containers and start again with a new sale and a new instance. A new sale is safe here because your server makes the payment call and the buyer never saw the first sale's `sale_url`. The [Hosted Fields JSAPI Guide](https://docs.payme.io/accepting-payments/checkout-and-in-person/hosted-fields-jsapi-guide#rules-that-cost-people-time) has the full list. - Handle `pay-sale` by `status_code`: `0` means processed, so record `sale_status` and confirm it; `5` means send the buyer to `redirect_url`; anything else means start again with a new sale. - Ask PayMe whether you need [3DS with Hosted Fields](https://docs.payme.io/accepting-payments/3d-secure/3ds-with-hosted-fields). - `browser/hosted-fields.js` in the [integration kit](https://docs.payme.io/integration-kit/index.json) is a working module. ## Saved token See [Saved Cards](https://docs.payme.io/agents/steps/saved-cards). Never repeat a token charge whose outcome is unknown before checking the sale (TRN-04). - [Callbacks (Partner)](https://docs.payme.io/agents/steps/callbacks-partner): For partners, a sale callback's valid signature means only worth checking, because it covers credentials and two IDs, not status or amount. Confirm with get-transactions using the seller's MPL and match your own order. Subscription, seller and withdrawal-complete callbacks are unsigned, so re-read them. Delivered means below 400, 4xx is retried, and a failing domain can be skipped, so reconcile, including finished orders, and a failed sale can still complete. # Callbacks (Partner) Reference page: [Callbacks](https://docs.payme.io/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](https://docs.payme.io/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](https://docs.payme.io/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`](https://docs.payme.io/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](https://docs.payme.io/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](https://docs.payme.io/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](https://docs.payme.io/agents/steps/capture-refund-void#reading-the-rows)). [Reporting Across Your Sellers](https://docs.payme.io/marketplaces-and-platforms/reporting/reporting-across-your-sellers) and [Reconcile Sales and Withdrawals](https://docs.payme.io/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](https://docs.payme.io/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. - [Callbacks (Direct Seller)](https://docs.payme.io/agents/steps/callbacks-seller): A direct seller cannot check payme_signature, because it needs partner credentials. Treat every callback as a trigger: find your order by the payme_sale_id you stored, call get-transactions with your MPL and your order ID, check the rows, then apply PayMe's answer. Reply below 400 for recorded decisions, rate-limit lookups, and re-check on a schedule because callbacks can be skipped. # Callbacks (Direct Seller) Reference page: [Callbacks](https://docs.payme.io/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`](https://docs.payme.io/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](https://docs.payme.io/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](https://docs.payme.io/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](https://docs.payme.io/agents/steps/capture-refund-void#reading-the-rows)). [Reconcile Sales and Withdrawals](https://docs.payme.io/accepting-payments/reporting/reconcile-sales-and-withdrawals) and [Find the Right Data](https://docs.payme.io/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](https://docs.payme.io/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. - [Capture, Refund, Void](https://docs.payme.io/agents/steps/capture-refund-void): An authorization lasts 7 days and captures once; a second capture fails with error 305 and a late one with 516. A refund with no amount after a partial refund returns exactly the rest, and the sale stays partial-refund, so track the total. refund-sale on an uncaptured authorization voids it. Refund rows in get-transactions have the action Refund or Partial Refund; a void adds none. Partial capture's remainder is unconfirmed, so do not offer it yet. # Capture, Refund, Void Reference pages: [Capture Sale](https://docs.payme.io/payments/post-sale-actions/post-capture-sale), [Refund Sale](https://docs.payme.io/payments/post-sale-actions/post-refund-sale), [Void Authorization](https://docs.payme.io/payments/post-sale-actions/void-sale), the [capture guide](https://docs.payme.io/accepting-payments/capture-and-saved-cards/capture-sale), [Idempotency and Retries](https://docs.payme.io/payments/getting-started/idempotency-and-retries). Send `seller_payme_id` on all three. A partner also sends `payme_client_key`, and that seller must belong to it: otherwise `refund-sale` fails with error `303`. ## Authorize now, capture later `generate-sale` with `sale_type: "authorize"`, the buyer pays, the sale is `authorized`, then `capture-sale` moves it to `completed`. | Rule | What happens otherwise | |---|---| | Capture within 7 days of the authorization | [Error 516](https://docs.payme.io/agents/errors/error-516) | | Capture once | A second capture fails with [error 305](https://docs.payme.io/agents/errors/error-305) in the sandbox, with `completed` as the additional information | | Capture only after the buyer paid | A sale still `initial` fails with [error 305](https://docs.payme.io/agents/errors/error-305) in the sandbox | - Alert before the limit, for example at 144 hours, counted from the authorization. Your own business decides what happens then. - Send `language: "en"`, or errors arrive in Hebrew. - A partner's `market_fee` is set when the sale is created, not at capture: see [Platform Fees](https://docs.payme.io/marketplaces-and-platforms/fees-and-payouts/platform-fees). ### Partial capture PayMe accepts `sale_price` on `capture-sale`, within the account's capture limit. What happens to the rest of the hold is unconfirmed, and the query check fails by design because no row matches the full order amount. Don't offer partial capture until PayMe answers (see [What to Ask PayMe First](https://docs.payme.io/agents/start/ask-payme-first)). ## Refund - Leave out `sale_refund_amount` for the full amount. To refund part, send it in minor units, within the [allowed range](https://docs.payme.io/agents/steps/money). - Partial refunds can repeat while the total stays within the sale. Refunding when nothing is left failed with [error 352](https://docs.payme.io/agents/errors/error-352) in the sandbox, and a refund above the sale's price failed with error `517`. - After a partial refund, a refund with no amount returned exactly the rest in the sandbox: a `2000` sale refunded `1000`, then `1000`. - **The sale then stays `partial-refund`.** It never moves to `refunded`. Add each response's `payme_transaction_total` to your own refunded total, and block further refunds when it equals the price. - If a refund's outcome is unknown, re-read the [rows](#reading-the-rows). A refund row settles it: count it once. No row doesn't prove there was no refund, so keep further refunds blocked until a person records the true total (TRN-04). ## Void There's no separate call: `refund-sale` with no amount on an uncaptured authorization releases the hold, and the status becomes `voided`. This worked for a partner and for a direct seller in the sandbox. - Show it as "Void" for authorized sales and "Refund" for completed ones. - A void's `refund` callback isn't refunded money. Skip it when the status is `voided`. - A void adds no refund row. To confirm one, read `get-sales`, or `get-transactions` with `transaction_type: 40`: its row shows `sale_status` `voided`. A default query returned no rows for a voided sale in the sandbox. Add `transaction_status: ["validated", "voided"]` to see the `Void` row too. ## Reading the rows Each `get-transactions` row has a `transaction_action`, and PayMe's values are: | `transaction_action` | What the row is | |---|---| | `Debit` | A charge. Seen in the sandbox | | `Authorize`, `Capture` | A hold, and taking it | | `Refund`, `Partial Refund` | Money back to the buyer. Seen in the sandbox | | `Void`, `Partial Void` | A hold released, not refunded money. `Void` seen in the sandbox | | `Chargeback`, `Partial Chargeback` | The card company took the money back | | `Chargeback Refund`, `Partial Chargeback Refund` | A chargeback reversed. Not a refund to the buyer | | `Tokenize`, `Detokenize` | Token work, with no money moved | To add up refunds: - Count a row only if `transaction_action` is exactly `Refund` or `Partial Refund`, and `transaction_status` is `refunded` or `p-refund`. Don't match the word "refund", which also finds `Chargeback Refund`, and don't count a row whose status is `failed`. - Its `transaction_price` is the positive amount refunded. The charge row keeps the sale's full price. - Count each row once, by `payme_transaction_id`. A `refund-sale` response gives that same ID, so a response and a row can't add the amount twice. - A signed callback is only a reason to re-read. Never add from its body (CB-01). - [Saved Cards](https://docs.payme.io/agents/steps/saved-cards): Saved cards use a token. capture_buyer, Hosted Fields and capture-buyer-token give a buyer_key; POST /sellers/{mpl}/tokens gives a uuid sent as token, and it worked across a partner's sellers in the sandbox. Without buyer_is_permanent, a second charge failed with error 604. Encrypt tokens, never send them to a browser. A direct seller sends buyer_is_private: true and fetches the token with get-buyer-key after confirming the sale. # Saved Cards Reference pages: [Save a card and charge it later](https://docs.payme.io/accepting-payments/capture-and-saved-cards/save-a-card-and-charge-it-later), [Capture Buyer Token](https://docs.payme.io/payments/tokenization/post-capture-buyer-token), [Generate Sale with Token](https://docs.payme.io/payments/tokenization/post-generate-sale-tokenization), [Get Buyer Key](https://docs.payme.io/payments/tokenization/post-get-buyer-key), [Create Marketplace Token](https://docs.payme.io/payments/tokenization/post-sellers-mpl-tokens). A token stands in for a card (TOK-01). Before you save one, you need a first [Checkout](https://docs.payme.io/agents/steps/checkout) to capture the card. ## Getting one | Way | Who | You get | Notes | |---|---|---|---| | `capture_buyer: "1"` on `generate-sale` | Both | `buyer_key` | Captured as part of the payment. Can't be combined with `buyer_key` on one request. A direct seller also sends `buyer_is_private: true`: see below | | Hosted Fields `tokenize()` | Both | `token`, used as `buyer_key` | `tokenIsPermanent: false` makes it single-use | | `sale_type: "token"` on `generate-sale` | Both | `buyer_key` | The buyer enters a card and nothing is charged | | [`capture-buyer-token`](https://docs.payme.io/payments/tokenization/post-capture-buyer-token) | PCI DSS certified only | `buyer_key` | Takes a raw card (TOK-03) | | `sale_type: "seller-token"` on `generate-sale` | Partner | `uuid` | A hosted page that captures a marketplace token | | [`POST /sellers/{mpl}/tokens`](https://docs.payme.io/payments/tokenization/post-sellers-mpl-tokens) | Partner, PCI DSS certified | `uuid` | Takes a raw card | ### Getting it safely after a sale - A direct seller can't verify a callback, so it never stores a token from one (TOK-02). Once the sale is confirmed, it calls `get-buyer-key` with its MPL. - A partner treats a callback's `buyer_key` as unconfirmed too, until the sale is confirmed. Which credential `get-buyer-key` takes depends on how the token was captured: | Token | `get-buyer-key` with | Seen in the sandbox | |---|---|---| | Sale sent `buyer_is_private: true` | The seller's MPL | `status_code: 0` | | Sale sent no `buyer_is_private`, or a marketplace token | `payme_client_key` and no `seller_payme_id` | `status_code: 0`. With the MPL, alone or with the partner key, it fails with [error 603](https://docs.payme.io/payments/getting-started/errors) | So a direct seller, which has no partner key, must send `buyer_is_private: true` on `generate-sale`, or it can't read the token back. ## Scope and lifetime - **Reuse:** without `buyer_is_permanent: true`, a `capture-buyer-token` token was charged once and the next charge failed with [error 604](https://docs.payme.io/agents/errors/error-604) in the sandbox. With it, the same token paid repeatedly. Subscriptions need a permanent token too. - **A `capture_buyer` token:** in the sandbox one was charged three times and then started a subscription, with `buyer_is_private: true` on the sale. `buyer_is_permanent` isn't a `generate-sale` field, and PayMe publishes no lifetime for these tokens, so handle error 604 on any token anyway. - **Seller or account level:** `buyer_is_private: true` ties a token to one seller. Without it, the token is account-level, and a charge sending `seller_payme_id` can fail: see [Errors](https://docs.payme.io/payments/getting-started/errors). - **Marketplace-wide:** a token from `POST /sellers/{mpl}/tokens` was charged on another seller of the same partner in the sandbox. Against a seller of a different partner, it failed with [error 605](https://docs.payme.io/agents/errors/error-605). ## Charging one | You hold | Send on `generate-sale` | |---|---| | A `buyer_key` | `buyer_key` | | A marketplace `uuid` | `token`. Sent as `buyer_key`, it failed with error `600` in the sandbox | - A token charge returned the final `sale_status` in one call in the sandbox, with no `sale_url`. Confirm it as usual before fulfilling. - `pay-sale` with `buyer_key` is the other route. It is how a Hosted Fields sale finishes. - Charge only for the buyer the card belongs to: tie every saved token to your own buyer record and check it. ## Removing one These docs list no call to revoke a token. Delete it from your store, and that is what "remove this card" means. Let the buyer see the saved card (`buyer_card_mask`) and delete it. ## Test the failure To test "the saved card stopped working", use `4000000000000036` ([Test Cards and Payment Methods](https://docs.payme.io/payments/getting-started/test-cards-and-payment-methods)). It only reaches the charge if the token comes from Hosted Fields: | Way in | What happened in the sandbox | |---|---| | Hosted Fields `tokenize()`, then `pay-sale` with the token | Token created, then the charge failed with `20004` and the sale became `failed`. Use this one | | Hosted page, with `capture_buyer` or `sale_type: "token"` | Declined at once with `20004`, and no token exists | If you didn't build Hosted Fields, there's no way to run this without raw card data (TOK-03). Test your handling of a refused charge with a fake PayMe, and report the real decline as not proven. - [Subscriptions](https://docs.payme.io/agents/steps/subscriptions): PayMe runs the subscription schedule, so never build a charging scheduler. Subscription callbacks are unsigned: re-read with get-subscriptions using the seller's MPL in the JSON body. Grant service for each increase in sub_iterations_completed, not per notification. Status 7 is failed-retry, so don't dun the buyer. Monthly start days must be 1 to 28, and a token used here must be permanent. # Subscriptions Reference pages: [Subscriptions](https://docs.payme.io/payments/subscriptions), [Generate](https://docs.payme.io/payments/subscriptions/post-generate-subscription), [List Subscriptions](https://docs.payme.io/payments/query-apis/post-get-subscriptions), [Subscription Statuses](https://docs.payme.io/payments/codes-statuses/subscription-statuses), [Recurring Payments](https://docs.payme.io/payments/saved-cards-recurring). Both roles follow the same rules. A partner works with the seller's MPL. A direct seller uses its own. ## Creating - `sub_price` is one iteration, in minor units. `sub_start_date` is `dd/mm/yyyy`. A monthly subscription must start on day 1 to 28, so when defaulting to today, move the 29th to 31st to the 1st of the next month. - Without a token, the response has `sub_url`, a hosted page where the buyer enters a card. The status stays initial until they pay. - With a `buyer_key`, no page is needed. The token must be permanent: see [Saved Cards](https://docs.payme.io/agents/steps/saved-cards). - PayMe runs the schedule. There's no "charge the next iteration" call, so don't build one (SAL-03). - Never use `pay-subscription` with raw card fields unless you are PCI DSS certified (TOK-03), and treat `sub_payme_id` as sensitive (SEC-05). ## Callbacks are a trigger Subscription callbacks carry no signature (CB-04). A signature appears only on the subscription return URL. 1. Find the subscription by the `sub_payme_id` you stored. No match: record it and change nothing. 2. Call `get-subscriptions` with `sub_payme_id` and the seller's MPL as `seller_payme_id`, in the JSON body (TRN-05). A direct seller's MPL alone returned that seller's subscriptions in the sandbox. 3. Discard the answer and alert if any row names another seller. Keep the row with your ID, and check its currency. 4. Normalise `sub_status`: it is a number from the query and a string on a callback. Record an unknown value, and act on nothing. 5. Answer below 400 (CB-06). ## Grant service from PayMe's numbers Grant one period for each increase in `sub_iterations_completed` you haven't already granted, not per notification. That stays right however many callbacks arrive, in whatever order, and when one is lost. An absent value is unknown, never zero. Re-check active subscriptions on a schedule, for example daily and the day after each expected charge. ## Statuses Use [Subscription Statuses](https://docs.payme.io/payments/codes-statuses/subscription-statuses). The one to get right is **7, failed and pending automatic retry**: don't chase the buyer about it, because PayMe will try again. Retries are described on [Recurring Payments](https://docs.payme.io/payments/saved-cards-recurring). Pause is reversible, and cancel is final, so confirm with the person before cancelling. The actions are [Cancel](https://docs.payme.io/payments/subscriptions/postcancel-subscription), [Pause](https://docs.payme.io/payments/subscriptions/post-pause-subscription), [Resume](https://docs.payme.io/payments/subscriptions/patch-subscriptions-sub-payme-id-resume) and [Update Price](https://docs.payme.io/payments/subscriptions/patch-subscriptions-sub-id-set-price). Resume and Update Price use a different request shape from the rest: follow their reference pages. `reference/subscription-verification.ts` and `test-vectors/subscription-verification.json` in the [integration kit](https://docs.payme.io/integration-kit/index.json) pin these rules. - [Balances and Payouts](https://docs.payme.io/agents/steps/balances-and-payouts): For partners, balances are in seller_wallets on get-sellers. wallet_releasable comes from release dates. A withdrawal can be refused because the seller is not validated (109) or is on an automatic plan (907). withdrawal-complete is unsigned and fires on commit, before the bank transfer, so re-read with get-withdrawals. market_fee reaches the platform separately, by default monthly. # Balances and Payouts Reference pages: [Withdrawals](https://docs.payme.io/marketplaces-and-platforms/fees-and-payouts/withdrawals), [Platform Fees](https://docs.payme.io/marketplaces-and-platforms/fees-and-payouts/platform-fees), [List Sellers](https://docs.payme.io/payments/query-apis/post-get-sellers), [Withdraw Balance](https://docs.payme.io/payments/merchants-management/post-withdraw-balance), [List Past Withdrawals](https://docs.payme.io/payments/query-apis/post-get-withdrawals), [List Future Withdrawals](https://docs.payme.io/payments/query-apis/get-future-withdrawals), [Reporting Across Your Sellers](https://docs.payme.io/marketplaces-and-platforms/reporting/reporting-across-your-sellers). ## Reading a balance There's no balance endpoint. `get-sellers` returns `seller_wallets`, by currency, in minor units. | Field | Meaning | |---|---| | `wallet_total` | Everything PayMe holds for the seller | | `wallet_releasable` | The part past its release date | | The difference | Recent sales still before their release date | Show the seller all three. "Why can I only withdraw part of it?" is the first question they ask. In the sandbox, `seller_wallets` stayed at 0 after several completed sales. Why is unconfirmed, so [ask PayMe](https://docs.payme.io/agents/start/ask-payme-first). Until then, test the display with a fake answer and report the real figures as not proven. ## Withdrawing | Refusal | Cause | What to do | |---|---|---| | [Error 109](https://docs.payme.io/agents/errors/error-109) | The seller isn't validated | Show approval state and signup link, and disable the action with an explanation | | Error `907` | The seller is on an automatic withdrawal plan, so manual withdrawals aren't allowed. Seen in the sandbox | Nothing to do. PayMe pays out on the plan's schedule | PayMe checks the plan first, so an unapproved seller on an automatic plan gets `907`, not `109`. Don't rely on seeing `109`: check `seller_approved` yourself. A new seller takes the partner's default plan, and every one created in the sandbox came back with `seller_withdrawal_plan` `2`, which is automatic. No API field changes it, so PayMe must set up a test seller before a payout can succeed: [ask them](https://docs.payme.io/agents/start/ask-payme-first). If a withdrawal's outcome is unknown, don't start another in that currency until a person has checked `get-withdrawals` (TRN-04). ## The completion callback `withdrawal-complete` has no signature, and it fires when the withdrawal is committed, **before the bank transfer happens**. So it doesn't prove the money arrived, and its body proves nothing at all (CB-04). Re-read with `get-withdrawals`, and tell sellers the money is on its way rather than received. ## Your own fee `market_fee` isn't part of a seller's wallet, and it isn't withdrawn with `withdraw-balance`. By default it's transferred to you monthly. See [Platform Fees](https://docs.payme.io/marketplaces-and-platforms/fees-and-payouts/platform-fees). - [Implementation Traps](https://docs.payme.io/agents/steps/implementation-traps): A checklist of traps found by independent review of a real PayMe integration: two payable sales for one order, concurrent capture or refund, unknown refund outcomes, reconcile clearing flags on empty answers, retried payouts or subscriptions, counting a void as a refund, and lenient number parsing. Check your code against each row before review. # Implementation Traps An independent review of a first implementation found every one of these. Check your code against each row before writing the connecting code, and again before asking for a review. ## Orders and concurrency | Trap | Correct behaviour | |---|---| | Two payable sales for one order, from two tabs, switching flows or concurrent requests | Take a per-order lock around every call that opens or pays a sale. Once a hosted-page sale is open, refuse other flows for that order, because the buyer can still pay it | | Concurrent capture, refund or void on one sale | The same order lock. PayMe also answers [error 998](https://docs.payme.io/agents/errors/error-998) to a duplicate still running | | A rejected duplicate `pay-sale` marks the sale failed from a stale row | Re-read the row inside the lock before writing | | A lock that exists only in one process | In-process locks protect one instance only. Enforce a single instance, or use a shared lock | ## Unknown outcomes | Trap | Correct behaviour | |---|---| | A refund with an unknown outcome (see [Reading a failure](https://docs.payme.io/agents/steps/config-and-transport#reading-a-failure)) | Re-read the rows. A refund row settles it, but no row doesn't prove there was none, so keep refunds blocked until a person records the total. Never clear this automatically | | A plain refusal arriving as HTTP 500 treated as unknown | It freezes the order for nothing. `status_code` 1 with an error code is a refusal, apart from the listed exceptions | | A money call worked, then the read-back query failed | Answer success with a "check this" flag, and let reconciliation clear it (TRN-07). An error invites a repeat | | Reconciliation clears the "check this" flag when the query returned nothing | Clear it only when PayMe returned the sale, under the order lock | | A payout with an unknown outcome is retried | Refuse a new payout in that currency until a person resolves the old one against `get-withdrawals` | | `generate-subscription` with a saved card is retried after a timeout | Insert a guard row, and refuse a new subscription for that seller and buyer until it's resolved | | The pending check after a successful `pay-sale` never clears | Clear it when a query confirms the sale | ## Callbacks | Trap | Correct behaviour | |---|---| | Unsigned lifecycle callbacks deduplicated by a key with no transaction ID | Pause, resume, pause sends the same body twice. Don't deduplicate re-reads | | A void's `refund` callback counted as refunded money | A void releases a hold. Skip it when the status is `voided` | | A refund allowed because the status is `partial-refund` | Use your own refunded total, because a sale refunded in parts never moves to `refunded` | | Refund totals taken from callbacks | Add from each `refund-sale` response and each [refund row](https://docs.payme.io/agents/steps/capture-refund-void#reading-the-rows), one per `payme_transaction_id`. A callback only triggers the re-read (CB-01). Once a person has recorded a total, alert on later rows rather than adding them | | Any row with "refund" in its action counted as a refund | `Chargeback Refund` isn't one, and a failed attempt isn't money back. Match the exact values and statuses | | A paid order stuck because `failed` was treated as final | A declined hosted sale can still complete. Keep re-reading, and allow `failed` to `completed` or `authorized` | | An error thrown in the reconcile timer stops the process | Catch and log it | ## Values | Trap | Correct behaviour | |---|---| | A saved card charged without a buyer reference | Require your buyer reference, and it must match the card's | | A number parser accepting `5e2`, `0x1f4` or ` 600 ` as an amount | Digits only for minor units | | `market_fee: null` becoming `0` | Treat null as "not given" | | A `market_fee` configured as `1%` | A plain number, such as `1` | ## Before calling it reviewed Commission a reviewer that didn't write the code, against these docs. Fix or record every finding. Then go to [Readiness Preflight](https://docs.payme.io/agents/verify/readiness-preflight). ## Verify - [Test Cases and Reference Code](https://docs.payme.io/agents/verify/test-cases-and-reference-code): PayMe publishes language-neutral JSON test cases and TypeScript and browser reference code at /integration-kit/, listed in index.json, under the MIT licence. Port every case for your role into your own test suite and make it pass, whatever your language. Add tests for your own connecting code. The files ship with each docs release. # Test Cases and Reference Code The [integration kit](https://docs.payme.io/integration-kit/index.json) holds code and test cases as files, under the [MIT licence](https://docs.payme.io/integration-kit/LICENSE.txt). `index.json` lists every file. Download what you need. Don't import it blind: port it to your own language and framework, and keep the behaviour identical. ## Test cases: port every case | File | Partner | Direct seller | Covers | |---|---|---|---| | [`test-vectors/money.json`](https://docs.payme.io/integration-kit/test-vectors/money.json) | ✓ | ✓ | Amount conversion and limits | | [`test-vectors/sale-query-verification.json`](https://docs.payme.io/integration-kit/test-vectors/sale-query-verification.json) | ✓ | ✓ | Confirming a sale from query rows | | [`test-vectors/subscription-verification.json`](https://docs.payme.io/integration-kit/test-vectors/subscription-verification.json) | ✓ | ✓ | Confirming a subscription from query rows | | [`test-vectors/partner-signatures.json`](https://docs.payme.io/integration-kit/test-vectors/partner-signatures.json) | ✓ | | Signature digests and outcomes | | [`test-vectors/partner-callbacks.json`](https://docs.payme.io/integration-kit/test-vectors/partner-callbacks.json) | ✓ | | What a partner does with each callback | | [`test-vectors/seller-callbacks.json`](https://docs.payme.io/integration-kit/test-vectors/seller-callbacks.json) | | ✓ | What a seller does with each callback | Each file states its `case_count`. Fail your test run if a file has fewer cases than that. ## Reference code | File | Use for | |---|---| | [`reference/payme-client.ts`](https://docs.payme.io/integration-kit/reference/payme-client.ts), [`reference/payme-error.ts`](https://docs.payme.io/integration-kit/reference/payme-error.ts) | Transport, the success test, error handling, redaction | | [`reference/money.ts`](https://docs.payme.io/integration-kit/reference/money.ts) | Amounts | | [`reference/payme-types.ts`](https://docs.payme.io/integration-kit/reference/payme-types.ts) | Request, response and callback shapes | | [`reference/callback-fields.ts`](https://docs.payme.io/integration-kit/reference/callback-fields.ts) | Classifying and reading a callback body | | [`reference/sale-query-verification.ts`](https://docs.payme.io/integration-kit/reference/sale-query-verification.ts), [`reference/subscription-verification.ts`](https://docs.payme.io/integration-kit/reference/subscription-verification.ts) | Confirming by query | | [`reference/partner-signature.ts`](https://docs.payme.io/integration-kit/reference/partner-signature.ts), [`reference/partner-callbacks.ts`](https://docs.payme.io/integration-kit/reference/partner-callbacks.ts) | Partners: signature checks and callback routing | | [`reference/seller-callbacks.ts`](https://docs.payme.io/integration-kit/reference/seller-callbacks.ts) | Direct sellers: callback routing | | [`browser/hosted-page.html`](https://docs.payme.io/integration-kit/browser/hosted-page.html), [`browser/hosted-fields.js`](https://docs.payme.io/integration-kit/browser/hosted-fields.js) | The browser side | The reference covers the pieces with one right answer. Request builders, order matching, locks and state changes are yours to write, in your project's style. ## Tests for your own code Passing the published test cases proves the pieces you ported, not the service around them. Bugs in your connecting code still need these service tests and an independent review by someone who didn't write the code. Test that: - a forged callback gets a status below 400 and changes nothing; - a confirmed sale is fulfilled once when its callback arrives twice; - a callback can't move a sale backwards; - a partial refund sequence ends with the right total and the status `partial-refund`; - amounts below the minimum or with decimals never reach PayMe; - credentials and tokens never appear in logs, responses or a browser bundle; - money-moving endpoints refuse unauthenticated callers; - a charge that timed out is checked, not retried; - each row of [Reading a failure](https://docs.payme.io/agents/steps/config-and-transport#reading-a-failure) gets its outcome: HTTP 500 with code `305` is refused, code `20002` at HTTP 200 is unknown, HTTP 429 means re-read first, and a non-JSON 500 is unknown; - a `failed` sale can still become paid, and is fulfilled once; - every row of [Implementation Traps](https://docs.payme.io/agents/steps/implementation-traps) that applies to you. - [Readiness Preflight](https://docs.payme.io/agents/verify/readiness-preflight): Run these before any sandbox step. Confirm credentials are set without printing them and are not placeholders, the config loads, a read-only query is accepted, the running server has the current settings, the public address answers from the internet, and PayMe has connected a terminal and, for partners, set the Default Callback URL. # Readiness Preflight Run these in order. Never print a secret: report lengths and yes or no only. 1. **Credentials are set.** The secret file exists and is excluded from git. Report `set (N characters)` or `empty` for each credential, and flag a key equal to the secret, whitespace in a value, placeholder text, or a percent sign in a fee setting. 2. **Config loads** with the project's own loader. A validation message is safe to show. 3. **Credentials work,** with one read-only call: | Role | Call | Expect | |---|---|---| | Partner | `get-sellers` with `payme_client_key` and `page_size: 5` | `status_code` 0 | | Direct seller | `get-transactions` with `seller_payme_id` and `page_size: 1` | `status_code` 0 | [Error 200](https://docs.payme.io/agents/errors/error-200) or [error 251](https://docs.payme.io/agents/errors/error-251) here usually means a value from the wrong environment. 4. **The server is running the current settings.** Compare the process start time with the settings file's modified time. The startup log should show the callback address it will send, and not `localhost` once a tunnel exists. 5. **The public address answers from the internet.** If you have no public address yet, start a tunnel with `cloudflared tunnel --url http://localhost:`, following the [recipe, with Windows steps](https://docs.payme.io/agents/steps/config-and-transport#a-tunnel-for-callbacks-in-development). PayMe hasn't run that recipe end to end. Call your health route through the address, such as `/health`: | Result | Meaning | |---|---| | 200 | Good | | 502 | The tunnel is up and your server is down | | Cloudflare error 1033 | The tunnel process has gone. A quick tunnel's address goes with it | 6. **You can call your own endpoints.** The person creates an admin credential for your app, so you never see it. For your own runs, keep a temporary one in a shell variable, never echo it, and revoke it when done. 7. **PayMe's side is confirmed:** a terminal is connected, and for partners the Default Callback URL is set. See [What to Ask PayMe First](https://docs.payme.io/agents/start/ask-payme-first). Downloading a binary or opening a public tunnel is often blocked for an agent. Give the person the recipe's exact commands, ask them to paste back only the public address, which isn't a secret, and update the settings for them. Next: [Sandbox Walk-through](https://docs.payme.io/agents/verify/sandbox-walk-through). - [Sandbox Walk-through](https://docs.payme.io/agents/verify/sandbox-walk-through): Run each chosen flow against the PayMe sandbox and confirm final states with a query. Order: setup and forged callback, sellers (partner), open sales, pay them with test cards, confirm, then capture, void, partial refund and remainder, saved card reuse, subscription actions, payouts. Tick only what was run. Partners also need one real signed sale callback verified. # Sandbox Walk-through Tick only what actually ran (WRK-03). Test cards are on [Test Cards and Payment Methods](https://docs.payme.io/payments/getting-started/test-cards-and-payment-methods). Run only the flows in your scope, and record the rest as not run. ## 1. Setup - [ ] [Readiness Preflight](https://docs.payme.io/agents/verify/readiness-preflight) passes. - [ ] Every test case is ported and passing ([Test Cases and Reference Code](https://docs.payme.io/agents/verify/test-cases-and-reference-code)). - [ ] A forged callback through the public address gets a status below 400 and changes nothing. The command is on [Callbacks (Partner)](https://docs.payme.io/agents/steps/callbacks-partner#test-it) or [Callbacks (Direct Seller)](https://docs.payme.io/agents/steps/callbacks-seller#test-it). ## 2. Sellers (partner) - [ ] Onboard a seller with the [sandbox values](https://docs.payme.io/payments/getting-started/test-cards-and-payment-methods#creating-test-sellers-partners) and `seller_phone` `+972501234567`. MPL and public key stored. - [ ] `get-sellers` shows it not approved, and your app shows the signup link. - [ ] A `seller-*` callback arrives at the Default Callback URL and causes a re-read. Its body isn't trusted. ## 3. Checkout - [ ] Hosted page: pay with `5326105300985846`, `12/30`, CVV `658`, Social ID `008336174`. The order becomes paid only after the query confirms it. - [ ] Declined card `4000000000000002`: nothing fulfilled, status `failed` recorded. - [ ] A good card on the same `sale_url` after that decline: the sale ends `completed`, and the order is fulfilled once. - [ ] Hosted Fields, if built: tokenize, `pay-sale`, and a retry after a failure uses a new sale and a new instance. - [ ] **Partner:** at least one real sale callback verified as signed correctly. This proves the partner secret and the formula. With no public address, a real return URL's signature proves the same two things, but not delivery: tick this as secret and formula only, and report delivery as not proven ([details](https://docs.payme.io/agents/steps/callbacks-partner#with-no-public-address)). - [ ] The same callback delivered twice is fulfilled once. - [ ] **Direct seller:** stop the tunnel, pay, restart it: the scheduled re-check marks the order paid with no callback. ## 4. After the sale - [ ] Authorize, then confirm with `transaction_type: 40`, then capture, ending `completed`. - [ ] Capture on a sale still `initial` is refused, by your code or with [error 305](https://docs.payme.io/agents/errors/error-305). - [ ] Partial refund, then the rest with no amount. The sale stays `partial-refund`, and your total equals the price. - [ ] A second authorization voided, ending `voided`. ## 5. Saved cards - [ ] Token captured, stored encrypted, and charged again, on the sandbox. - [ ] If you built Hosted Fields: `4000000000000036` tokenizes, then the charge declines with `20004`. Otherwise record this as not run: both hosted-page forms decline that card at capture ([why](https://docs.payme.io/agents/steps/saved-cards#test-the-failure)). - [ ] **Direct seller:** the token came from `get-buyer-key` after confirmation, not from a callback. ## 6. Subscriptions - [ ] Created without a token: `sub_url`, pay, callback, `get-subscriptions` confirms, service granted once. - [ ] Pause, resume, change price and cancel, with your state following what PayMe reports. - [ ] An unsigned callback alone changes nothing. ## 7. Payouts (partner) - [ ] `seller_wallets` shown as total, releasable and the difference. - [ ] Withdrawal refused or disabled for an unapproved seller. A new sandbox seller is on automatic plan `2`, so PayMe answers `907` before it looks at approval. - [ ] A payout as far as the account allows, re-read with `get-withdrawals`. A successful payout needs PayMe to set up a seller first ([what to ask](https://docs.payme.io/agents/start/ask-payme-first)). Without that, record the refusal, and report a successful payout as not proven. ## 8. Final states - [ ] Every sale and subscription you created re-read with a query. Any disagreement with your records is recorded as a finding. - [ ] No partner key, secret, MPL or token in logs, responses or the browser bundle. Then fill in the [Proof-of-Integration Report](https://docs.payme.io/agents/verify/proof-of-integration-report). - [Go-Live Checklist](https://docs.payme.io/agents/verify/go-live-checklist): Before going live with PayMe, switch base URL, credentials and Hosted Fields testMode together, keep secrets in a secrets manager, use a real HTTPS callback address, confirm a forged callback is rejected, remove simulators, reconcile on a schedule, watch authorizations near 7 days, and log session IDs. Partners also confirm the production Default Callback URL. # Go-Live Checklist ## What changes | | Sandbox | Production | |---|---|---| | API base | `https://sandbox.payme.io/api/` | `https://live.payme.io/api/` | | Credentials | Sandbox set | A separate set, on the production side of **Admin → API & Integration**, or from your account manager for partner keys | | Hosted Fields | `testMode: true` | `testMode: false` | | Test cards | Work | Don't work | | Sellers | Sandbox values pass | Real documents, real review | ## Credentials and secrets - [ ] Production credentials obtained, and kept in a secrets manager, not a database column or a committed file. - [ ] Tokens encrypted at rest. - [ ] Log pipeline checked: no partner key, MPL, token or personal data in logs or error reports. ## Callbacks - [ ] Callback address is a real HTTPS origin, not a tunnel. - [ ] A forged callback was seen doing nothing on a test server of your own. - [ ] Handlers are safe to run twice. - [ ] Alerts on runs of rejected or unconfirmed callbacks, and on handler errors. - [ ] Any local callback simulator or debug route is out of the build. - [ ] Scheduled reconciliation is running in production, for sales and for active subscriptions. - [ ] Callback lookups are rate-limited. - [ ] **Partner:** production Default Callback URL set by PayMe, and partner secret configured. ## Code - [ ] The environment switch reads production, and `testMode` follows it. - [ ] For Hosted Fields, the site's Content Security Policy allows PayMe: see the [Hosted Fields JSAPI Guide](https://docs.payme.io/accepting-payments/checkout-and-in-person/hosted-fields-jsapi-guide#rules-that-cost-people-time). - [ ] No amount conversion outside the display layer. - [ ] Bad amounts are rejected before reaching PayMe. ## Commercials and operations - [ ] **Partner:** `market_fee` matches what sellers were told, and currencies are limited to what each seller has enabled (`seller_currencies`). - [ ] **Partner:** sellers can see their approval state and signup link. - [ ] Someone watches authorizations approaching 7 days. - [ ] PayMe `session` values are logged with every error. - [ ] Reconciliation runs against PayMe's queries, not only your database: [Reconcile Sales and Withdrawals](https://docs.payme.io/accepting-payments/reporting/reconcile-sales-and-withdrawals). - [Proof-of-Integration Report](https://docs.payme.io/agents/verify/proof-of-integration-report): A template for proving a PayMe integration works. Fill in evidence per area, including test cases ported, independent review, credentials accepted, forged callback unchanged and real signed callback for partners. List each flow actually run with its final state confirmed by query, the flows not run, findings, open questions and how many times a person stepped in. # Proof-of-Integration Report An integration isn't done until every row in scope has its evidence (WRK-03). Copy this template into your project, and fill it in from what actually ran. Leave out credentials, tokens and real personal data. ```markdown # PayMe integration: proof report Role: partner / direct seller Stack: Date and docs address used: Flows in scope: ## Evidence | Area | Evidence | Pass? | |---|---|---| | Test cases | Files ported, cases passing | | | Own tests | Connecting code and traps covered | | | Independent review | Who reviewed, findings fixed or recorded | | | Credentials | Read-only call accepted | | | Reachability | Public health route answered from the internet | | | Forged callback | Status below 400, recorded, order unchanged | | | Real sale callback (partner) | Signature verified as valid at least once | | | PayMe's side | Terminal connected, Default Callback URL set (partner) | | ## Flows run against the sandbox | Flow | Sale or subscription (last 4 characters) | Final state per PayMe's query | Result | |---|---|---|---| ## Not run, and why ## Findings | ID | Found in | Type | Severity | Description | What needs doing | Owner | Status | |---|---|---|---|---|---|---|---| ## Questions still open with PayMe ## How many times a person stepped in Total: | # | What was needed | Could the docs have prevented it? | |---|---|---| ``` Use the Findings types from the record files of [Integrate as a Partner](https://docs.payme.io/agents/partner/payme-partner-integration#keep-a-record). ## Errors - [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting): Symptom-first troubleshooting for PayMe integrations. Every card failing on the hosted page points to no terminal (420). A paid subscription stuck at initial means the handler expects signatures. A real sale callback rejected means a wrong partner secret. No callbacks means an unreachable address, a local URL or a skipped domain. Tunnel 1033 and 502 mean tunnel or server down. # Troubleshooting by Symptom For a specific code, go to `/agents/errors/error-`, for example [error 420](https://docs.payme.io/agents/errors/error-420). The [Errors](https://docs.payme.io/payments/getting-started/errors) page has the full list. ## Payments | Symptom | Cause | Fix | |---|---|---| | Every card fails on the hosted page | No terminal connected to the account | [Error 420](https://docs.payme.io/agents/errors/error-420): PayMe connects one | | `generate-sale` fails only in development | A local callback or return URL | [Error 21](https://docs.payme.io/agents/errors/error-21): leave the field out | | A payment charged a hundredth of the expected amount | Whole currency units sent instead of minor units | [Money](https://docs.payme.io/agents/steps/money) | | A saved card worked once and then stopped | The token wasn't permanent | [Error 604](https://docs.payme.io/agents/errors/error-604) | | A marketplace token says "Buyer not found" | The `uuid` was sent as `buyer_key` | Send it as `token`: [Saved Cards](https://docs.payme.io/agents/steps/saved-cards#charging-one) | | Sale shows `partial-refund` after a full refund in parts | Expected | Track the total yourself: [Capture, Refund, Void](https://docs.payme.io/agents/steps/capture-refund-void#refund) | | An authorized sale isn't in `get-transactions` | The default query skips uncaptured authorizations | Send `transaction_type: 40` | | Error messages in Hebrew | No `language` field | Send `language: "en"` | | A plain refusal arrives as HTTP 500 | Normal for PayMe | It's a refusal, not an unknown outcome: [Reading a failure](https://docs.payme.io/agents/steps/config-and-transport#reading-a-failure) | | A sale is `failed`, then later `completed` | The buyer retried on the same page | Expected: [Checkout](https://docs.payme.io/agents/steps/checkout) | | `get-buyer-key` answers 603 with the MPL | The sale didn't send `buyer_is_private: true` | [Saved Cards](https://docs.payme.io/agents/steps/saved-cards#getting-it-safely-after-a-sale) | | A voided sale has no rows in `get-transactions` | The default query leaves it out | [Void](https://docs.payme.io/agents/steps/capture-refund-void#void) | ## Callbacks | Symptom | Cause | Fix | |---|---|---| | A real sale callback is rejected as wrongly signed | Wrong partner secret, partner key and secret from different accounts, or the wrong `transaction_id` field hashed | Fix the settings. Never loosen the check. See [Callbacks (Partner)](https://docs.payme.io/agents/steps/callbacks-partner#the-transaction_id-trap) | | A forged test callback is rejected | Expected | Nothing | | A paid subscription stays initial, with callbacks recorded as unsigned or rejected | The handler expects signed subscription callbacks | Re-read with `get-subscriptions`: [Subscriptions](https://docs.payme.io/agents/steps/subscriptions) | | No seller callbacks arrive | They go only to the Default Callback URL | Ask PayMe to set it | | No callbacks arrive at all | The address is unreachable, or PayMe is skipping a failing domain | Check it from outside your network, answer below 400, and reconcile meanwhile | | The same callback arrives repeatedly | Your endpoint answers 400 or above, or too slowly | Answer below 400 quickly, and work afterwards | ## Your own setup | Symptom | Cause | Fix | |---|---|---| | Startup log shows a `localhost` callback address after a tunnel was configured | The server started before the settings changed | Restart it | | The configured public address is an example value | The placeholder was copied | Use the real address the tunnel printed | | Public address returns Cloudflare 1033 | The tunnel process exited, and a quick tunnel's address has gone | Restart it, update the settings, restart the server, and tell PayMe the new address if it's the Default Callback URL. A named tunnel on your own domain avoids this | | Public address returns 502 | The tunnel is up and your server isn't listening | Start the server | | Server refuses to start on `market_fee` | A value like `1%` | A plain number | | Pasted error lines don't match what you see now | They may predate the fix | Compare their timestamps with the server's start time | If none of this helps, send the `session` value with the request and response, after removing credentials. - [Error 420: No Terminal Available](https://docs.payme.io/agents/errors/error-420): PayMe error 420 (Please try a different card or contact customer support). PayMe found no terminal that can process this card for this seller. # Error 420: No Terminal Available PayMe's message with `language: "en"`: **Please try a different card or contact customer support** PayMe found no terminal that can process this card for this seller. The buyer usually sees it on the payment page, and the API answers HTTP 500. ## Usual causes - No terminal (provider number) is connected to the account. If **every** card fails, this is the cause. - The connected terminals can't process this card, if only some cards fail. ## What to do - Nothing in your code fixes a missing terminal. Ask PayMe to connect one: [What to Ask PayMe First](https://docs.payme.io/agents/start/ask-payme-first). - Check that the order stays unpaid, and let the buyer try again with a new sale. The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 25: Invalid Phone Number](https://docs.payme.io/agents/errors/error-25): PayMe error 25 (Please verify phone number). A phone number was refused. # Error 25: Invalid Phone Number PayMe's message with `language: "en"`: **Please verify phone number** A phone number was refused. `status_additional_info` holds the value. ## Usual causes - On `create-seller` in the sandbox, `seller_phone` `0501234567` was refused with HTTP 500, and `+972501234567` was accepted. ## What to do - Send `seller_phone` in international format, starting with `+`, and validate it before calling PayMe. See [Seller Onboarding](https://docs.payme.io/agents/steps/seller-onboarding). The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 21: Invalid URL](https://docs.payme.io/agents/errors/error-21): PayMe error 21 (Please verify URL validity). PayMe refused a URL in the request, and the whole call fails. # Error 21: Invalid URL PayMe's message with `language: "en"`: **Please verify URL validity** PayMe refused a URL in the request, and the whole call fails. `generate-sale` creates no sale. `status_additional_info` holds the URL. ## Usual causes - A local address in `sale_callback_url` or `sale_return_url`. In the sandbox, `localhost` in either field and a private address both failed with HTTP 500. - A malformed address, or one with two schemes. ## What to do - When an address is local, leave the field out and log that you did. PayMe then uses the account's configured URL. - To receive callbacks in development, use a tunnel. See [Config and Transport](https://docs.payme.io/agents/steps/config-and-transport). The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 790: Parameter Not Allowed on This Plan](https://docs.payme.io/agents/errors/error-790): PayMe error 790 (A parameter is missing or cannot be set on this plan). The seller's plan requires a field you left out, or refuses a field you sent. # Error 790: Parameter Not Allowed on This Plan PayMe's message with `language: "en"`: **A parameter is missing or cannot be set on this plan** The seller's plan requires a field you left out, or refuses a field you sent. `status_additional_info` names it. ## Usual causes - `seller_id` on `create-seller`, on plans that refuse it. ## What to do - Add or remove the named field. For `seller_id`, correlate on `seller_payme_id` instead. See [Seller Onboarding](https://docs.payme.io/agents/steps/seller-onboarding). - If you think the plan is wrong, ask your account manager. The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 174: Not Supported for This Seller Type](https://docs.payme.io/agents/errors/error-174): PayMe error 174 (Feature is not supported for this seller type). The account behind this seller ID can't perform the call. # Error 174: Not Supported for This Seller Type PayMe's message with `language: "en"`: **Feature is not supported for this seller type** The account behind this seller ID can't perform the call. On `generate-sale`, only merchant accounts can create sales. `status_additional_info` holds the type. ## Usual causes - The partner's own MPL was sent where a seller's belongs. ## What to do - Create the sale with the **seller's** MPL. See [Checkout](https://docs.payme.io/agents/steps/checkout). The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 114: Business Type Not Valid](https://docs.payme.io/agents/errors/error-114): PayMe error 114 (The selected business type is not valid for this business). The seller's business type doesn't fit its other details. # Error 114: Business Type Not Valid PayMe's message with `language: "en"`: **The selected business type is not valid for this business** The seller's business type doesn't fit its other details. PayMe's message often says exactly which rule failed. ## Usual causes - For an individual, sole proprietor or exempt dealer, `seller_inc_code` differs from `seller_social_id`. The sandbox answered HTTP 400 with `status_additional_info` `seller_inc`. - For a company, `seller_inc_code` equals the owner's social ID, or doesn't start with the right two digits for that type. - An unknown `seller_inc`, or one that doesn't match `seller_address_country`. ## What to do - Read the message, correct the field and validate these rules before calling. The values are on [Seller Incorporation Types (IL)](https://docs.payme.io/marketplaces-and-platforms/seller-reference/seller-incorporation-types-il). The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 121: Invalid Business Code](https://docs.payme.io/agents/errors/error-121): PayMe error 121 (The business code is invalid, Please enter only numbers). seller_inc_code isn't a positive number. # Error 121: Invalid Business Code PayMe's message with `language: "en"`: **The business code is invalid, Please enter only numbers** `seller_inc_code` isn't a positive number. `status_additional_info` is `seller_inc_code`. ## Usual causes - Letters or punctuation in the business number. The sandbox answered HTTP 500 for `ABC`. ## What to do - Send digits only. See [Seller Onboarding](https://docs.payme.io/agents/steps/seller-onboarding). The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 352: Invalid Price](https://docs.payme.io/agents/errors/error-352): PayMe error 352 (Invalid price, out of min-max bounds). An amount is outside the allowed range. # Error 352: Invalid Price PayMe's message with `language: "en"`: **Invalid price, out of min-max bounds** An amount is outside the allowed range. The default is `500` to `4000000` minor units, and PayMe can set different limits per account. `status_additional_info` holds the value or the field. ## Usual causes - An amount sent in whole currency units or with decimals. `50.75` was rounded to `51` and refused in the sandbox. - A `sale_refund_amount` out of range, or a refund with nothing left: a third refund on a fully refunded `2000` sale gave this error with `0`. ## What to do - Send integer minor units: [Money](https://docs.payme.io/agents/steps/money). - Track your own refunded total, and stop when it equals the price: [Capture, Refund, Void](https://docs.payme.io/agents/steps/capture-refund-void). The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 305: Action Not Allowed in This Status](https://docs.payme.io/agents/errors/error-305): PayMe error 305 (Cannot perform action due to an incorrect status). The sale isn't in a status that allows this call. # Error 305: Action Not Allowed in This Status PayMe's message with `language: "en"`: **Cannot perform action due to an incorrect status** The sale isn't in a status that allows this call. `status_additional_info` holds its current status. ## Usual causes - `capture-sale` on a sale the buyer hasn't paid: `initial` in the sandbox. - A second `capture-sale`: `completed` in the sandbox. - Paying a sale that's already paid, or refunding one that's fully refunded. ## What to do - Don't retry. Read the sale with [List Sales](https://docs.payme.io/payments/query-apis/post-get-sales), and update your own record from it. See [Capture, Refund, Void](https://docs.payme.io/agents/steps/capture-refund-void). The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 163: Seller Inactive](https://docs.payme.io/agents/errors/error-163): PayMe error 163 (Account settings problem - please contact Customer Support). The seller exists but isn't active. # Error 163: Seller Inactive PayMe's message with `language: "en"`: **Account settings problem - please contact Customer Support** The seller exists but isn't active. ## Usual causes - The account has been made inactive on PayMe's side. ## What to do - Write to with the `session` value. Your code can't fix it. The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 130: Seller Over Sale Limit](https://docs.payme.io/agents/errors/error-130): PayMe error 130 (Seller is over sale limit, unable to create new sales). The sale would take the seller over its processing limit. Complete any pending verification, then ask partners@payme.io to review the limit. # Error 130: Seller Over Sale Limit PayMe's message with `language: "en"`: **Seller is over sale limit, unable to create new sales** PayMe refused the sale because it would take the seller over its processing limit. `status_additional_info` is `processing limit`. ## Usual causes - The seller reached its processing limit. ## What to do - Complete any pending verification in the dashboard, then ask to review the seller's processing limit. Send `seller_payme_id` with the request and response. The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 998: Too Many Requests or Duplicate Action](https://docs.payme.io/agents/errors/error-998): PayMe error 998 (Operation is not permitted). HTTP 429. # Error 998: Too Many Requests or Duplicate Action PayMe's message with `language: "en"`: **Operation is not permitted** HTTP 429. `status_additional_info` tells the two cases apart: `Too Many Attempts.` is the rate limit, and `Duplicated action detected, please retry later` means the same action is still running. ## Usual causes - Too many `generate-sale`, `pay-sale` or `capture-sale` calls for one seller. - A repeat of a refund, capture or withdrawal while the first is running. ## What to do - Wait for the `Retry-After` time. - Check the state with a query before repeating a call that moves money. See [Rate limits](https://docs.payme.io/payments/getting-started/idempotency-and-retries#rate-limits). The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 513: Duplicate Purchase](https://docs.payme.io/agents/errors/error-513): PayMe error 513 (Duplicate purchase attempt detected. Please wait one minute before retrying). A saved-card charge repeated the same seller, amount and currency within 60 seconds. The check is off unless PayMe turns it on, so most integrations never see this code. # Error 513: Duplicate Purchase PayMe's message with `language: "en"`: **Duplicate purchase attempt detected. Please wait one minute before retrying** PayMe refused a saved-card charge (`generate-sale` with `buyer_key`) because the same card was charged for the same seller, amount and currency within the last 60 seconds. This check is off unless PayMe turns it on, so most integrations never see this code. ## Usual causes - A retried or double-submitted saved-card charge. ## What to do - Check the first charge with a query before you retry. It may have succeeded. See [Saved Cards](https://docs.payme.io/agents/steps/saved-cards). The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 516: Authorization Expired](https://docs.payme.io/agents/errors/error-516): PayMe error 516 (Sale authorization has been expired). The capture came more than 7 days after the authorization, so it can no longer be captured. # Error 516: Authorization Expired PayMe's message with `language: "en"`: **Sale authorization has been expired** The capture came more than 7 days after the authorization, so it can no longer be captured. This error doesn't void it. ## Usual causes - A late `capture-sale`. ## What to do - Release the hold with [Refund Sale](https://docs.payme.io/payments/post-sale-actions/post-refund-sale), otherwise the card issuer releases it on its own schedule. - Alert before the limit: [Capture, Refund, Void](https://docs.payme.io/agents/steps/capture-refund-void). The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 42: Number Must Be an Integer](https://docs.payme.io/agents/errors/error-42): PayMe error 42 (Number must be an integer). A value that must be whole had decimals. # Error 42: Number Must Be an Integer PayMe's message with `language: "en"`: **Number must be an integer** A value that must be whole had decimals. `status_additional_info` names the field. ## Usual causes - A decimal `sale_price` on `capture-sale`. Note that `generate-sale` rounds a decimal instead of giving this error. - A decimal `withdrawal_amount`. ## What to do - Send integer minor units, and reject decimals before calling PayMe. See [Money](https://docs.payme.io/agents/steps/money). The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 109: Seller Not Validated](https://docs.payme.io/agents/errors/error-109): PayMe error 109 (Seller not validated). The seller hasn't been validated, so a withdrawal is refused. # Error 109: Seller Not Validated PayMe's message with `language: "en"`: **Seller not validated** The seller hasn't been validated, so a withdrawal is refused. Sales still work. ## Usual causes - A withdrawal for a seller that hasn't finished onboarding. ## What to do - Show the seller its approval state and signup link, and disable the action until `get-sellers` shows it approved. See [Balances and Payouts](https://docs.payme.io/agents/steps/balances-and-payouts). The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 10: Field Must Be Numeric](https://docs.payme.io/agents/errors/error-10): PayMe error 10 (Field must be numeric). A field that must be a number isn't. # Error 10: Field Must Be Numeric PayMe's message with `language: "en"`: **Field must be numeric** A field that must be a number isn't. `status_additional_info` holds the value, not the field name. ## Usual causes - `installments` that isn't a number: the sandbox answered HTTP 500 for `x`. ## What to do - Send a number, or a string of digits such as `"1"`. Check the field on its reference page. The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 304: Market Fee Out of Range](https://docs.payme.io/agents/errors/error-304): PayMe error 304 (Market fee is not in the allowed range). market_fee or market_fee_fixed isn't a number, or is negative. # Error 304: Market Fee Out of Range PayMe's message with `language: "en"`: **Market fee is not in the allowed range** `market_fee` or `market_fee_fixed` isn't a number, or is negative. `status_additional_info` holds the value. ## Usual causes - A negative value: the sandbox answered HTTP 400 for `-1`. - A value like `1%`. ## What to do - Send a plain number. See [Platform Fees](https://docs.payme.io/marketplaces-and-platforms/fees-and-payouts/platform-fees). The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 308: Market Fee Above Maximum](https://docs.payme.io/agents/errors/error-308): PayMe error 308 (Market fee exceed allowed maximum of 60%). The fee, including any market_fee_fixed as a percent of the price, is above 60%. # Error 308: Market Fee Above Maximum PayMe's message with `language: "en"`: **Market fee exceed allowed maximum of 60%** The fee, including any `market_fee_fixed` as a percent of the price, is above 60%. ## Usual causes - A `market_fee` of `61` gave HTTP 400 in the sandbox. - A fixed fee that's large for a small sale. ## What to do - Keep the combined fee at 60% or below. See [Platform Fees](https://docs.payme.io/marketplaces-and-platforms/fees-and-payouts/platform-fees). The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 116: Invalid Birth Date](https://docs.payme.io/agents/errors/error-116): PayMe error 116 (Invalid birthdate). seller_birthdate is unreadable, or the person is under 16. # Error 116: Invalid Birth Date PayMe's message with `language: "en"`: **Invalid birthdate** `seller_birthdate` is unreadable, or the person is under 16. `status_additional_info` is `seller_birthdate`. ## Usual causes - An ISO date: `1990-01-01` gave HTTP 400 in the sandbox. ## What to do - Send `dd/mm/yyyy`, and validate it before calling. See [Seller Onboarding](https://docs.payme.io/agents/steps/seller-onboarding). The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 118: Invalid ID Issue Date](https://docs.payme.io/agents/errors/error-118): PayMe error 118 (Invalid social ID issue date). seller_social_id_issued is unreadable or in the future. # Error 118: Invalid ID Issue Date PayMe's message with `language: "en"`: **Invalid social ID issue date** `seller_social_id_issued` is unreadable or in the future. `status_additional_info` is `seller_social_id_issued`. ## Usual causes - An ISO date instead of `dd/mm/yyyy`. ## What to do - Send `dd/mm/yyyy`, and validate it before calling. See [Seller Onboarding](https://docs.payme.io/agents/steps/seller-onboarding). The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 200: Partner Key Not Found](https://docs.payme.io/agents/errors/error-200): PayMe error 200 (Merchant was not found). The payme_client_key or PayMe-Partner-Key matches no partner. # Error 200: Partner Key Not Found PayMe's message with `language: "en"`: **Merchant was not found** The `payme_client_key` or `PayMe-Partner-Key` matches no partner. HTTP 500, with `status_additional_info` `payme_client_key`. ## Usual causes - A seller sent its own MPL in `payme_client_key`. - A production key sent to the sandbox, or the reverse. ## What to do - A direct seller removes the field and sends `seller_payme_id` alone. - A partner checks the key and the environment. See [Authentication](https://docs.payme.io/payments/getting-started/authentication). The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 201: Partner Inactive](https://docs.payme.io/agents/errors/error-201): PayMe error 201 (Merchant is blocked). The partner account isn't active. # Error 201: Partner Inactive PayMe's message with `language: "en"`: **Merchant is blocked** The partner account isn't active. `status_additional_info` is `payme_client_key`. ## Usual causes - The account has been made inactive on PayMe's side. ## What to do - Write to with the `session` value. Your code can't fix it. The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 251: Seller Not Found](https://docs.payme.io/agents/errors/error-251): PayMe error 251 (Seller was not found). The seller_payme_id or seller_id matches no seller. # Error 251: Seller Not Found PayMe's message with `language: "en"`: **Seller was not found** The `seller_payme_id` or `seller_id` matches no seller. Also returned for an unknown public key during tokenization. Support may call this `ERR-020`. ## Usual causes - A wrong MPL. - A sandbox value sent to production, or the reverse. ## What to do - Copy the MPL from **Admin → API & Integration**, and check that the key and the base URL belong to the same environment. See [Authentication](https://docs.payme.io/payments/getting-started/authentication). The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 252: Currency Not Allowed](https://docs.payme.io/agents/errors/error-252): PayMe error 252 (Currency is not allowed for this seller). This seller can't use that currency. # Error 252: Currency Not Allowed PayMe's message with `language: "en"`: **Currency is not allowed for this seller** This seller can't use that currency. `status_additional_info` holds it. ## Usual causes - A currency not enabled on the account: `JPY` gave HTTP 400 in the sandbox. ## What to do - Use a currency in `seller_currencies` from [List Sellers](https://docs.payme.io/payments/query-apis/post-get-sellers), or ask PayMe to enable it. The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 604: Token Inactive](https://docs.payme.io/agents/errors/error-604): PayMe error 604 (Buyer inactive). The saved-card token can't be charged any more. # Error 604: Token Inactive PayMe's message with `language: "en"`: **Buyer inactive** The saved-card token can't be charged any more. `status_additional_info` is `buyer_key`. ## Usual causes - A single-use token charged a second time. In the sandbox, a `capture-buyer-token` token without `buyer_is_permanent: true` was charged once and then gave this error. ## What to do - Ask the buyer for the card again. For tokens you mean to reuse, ask for a permanent one. See [Saved Cards](https://docs.payme.io/agents/steps/saved-cards). The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting). - [Error 605: Token From Another Seller](https://docs.payme.io/agents/errors/error-605): PayMe error 605 (The buyer was not captured using the provided seller). The token doesn't belong to the seller in the request. # Error 605: Token From Another Seller PayMe's message with `language: "en"`: **The buyer was not captured using the provided seller** The token doesn't belong to the seller in the request. ## Usual causes - A seller-level token charged on a different seller. - A marketplace token charged on a seller of a different partner. ## What to do - Charge the token on a seller that can use it, or capture the card again. See [Saved Cards](https://docs.payme.io/agents/steps/saved-cards). The full list of codes is on [Errors](https://docs.payme.io/payments/getting-started/errors). If something else is going on, see [Troubleshooting by Symptom](https://docs.payme.io/agents/errors/troubleshooting).