diff --git a/README.md b/README.md index 6185392..570fbf1 100644 --- a/README.md +++ b/README.md @@ -111,10 +111,14 @@ sumup readers checkout \ --currency EUR \ --tip-rate 0.10 \ --tip-rate 0.15 \ + --tip-rate 0.20 \ --description "In-person order #123" ``` -When using affiliate attribution, pass all affiliate flags: `--affiliate-app-id`, `--affiliate-key`, and `--affiliate-foreign-transaction-id`. +For Pix payments on Brazilian merchants, add `--payment-type pix`. + +When using affiliate attribution, pass both `--affiliate-app-id` and `--affiliate-key`. +Use `--affiliate-foreign-transaction-id` to include an optional transaction reference. Trigger a checkout on a SumUp Go reader. `--client-transaction-id` is required and used as the idempotency key: @@ -135,6 +139,27 @@ sumup readers status \ reader_42 ``` +Read or update Solo reader menu lock settings. Updates require the reader to be online; +offline reads return the last saved settings when available. + +```bash +sumup readers settings get --merchant-code M123 reader_42 +sumup readers settings update --merchant-code M123 --menu-lock-enabled=false reader_42 +``` + +Set `SUMUP_READER_MENU_LOCK_PASSCODE` to a four-digit PIN to include it in an update. +When enabling menu lock on a reader without a PIN, provide the PIN in the same update. + +## Create a Merchant Token + +Create a short-lived merchant token for a SumUp SDK. The command requires +`--json` and returns the access token and its remaining lifetime. Without +`--json`, it exits before creating a token: + +```bash +sumup --json tokens create --merchant-code M123 --action checkout +``` + ## OpenAPI Command Coverage The CLI keeps its user-facing command implementations handwritten, but derives diff --git a/go.mod b/go.mod index f275348..3ad7f63 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/mergestat/timediff v0.0.4 github.com/shopspring/decimal v1.5.0 github.com/stretchr/testify v1.12.1 - github.com/sumup/sumup-go v0.19.0 + github.com/sumup/sumup-go v0.21.0 github.com/urfave/cli-docs/v3 v3.1.0 github.com/urfave/cli/v3 v3.14.0 golang.org/x/term v0.46.0 @@ -33,7 +33,7 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect - golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/oauth2 v0.37.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.48.0 // indirect ) diff --git a/go.sum b/go.sum index 5ec5748..c46b01a 100644 --- a/go.sum +++ b/go.sum @@ -44,8 +44,8 @@ github.com/shopspring/decimal v1.5.0 h1:HxSWk31RmJv0EyY4oLiGQB9u7WPludUwPBdvQZnY github.com/shopspring/decimal v1.5.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= -github.com/sumup/sumup-go v0.19.0 h1:/AT4t6R4geRDdAtXiEI/JgOK/PyfPd7va77KEjzYoqU= -github.com/sumup/sumup-go v0.19.0/go.mod h1:D++G2u9zvobimuUrgvSbah/tS3ON2Dmi6yb6ScA2Jpg= +github.com/sumup/sumup-go v0.21.0 h1:SSNgl80kST14mj/YsF5EdIa3O0lAUC/VPgzPMjZG1vE= +github.com/sumup/sumup-go v0.21.0/go.mod h1:LRmcEBSpL1rMvjam6OtD0YBK9cyONiMjNp0DnvSLW1o= github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw= github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to= github.com/urfave/cli/v3 v3.14.0 h1:a8414NQlHJs0c/iBsulKLzlES0n/lEAskbL2LKpU4/s= @@ -56,8 +56,8 @@ go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= -golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98= +golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= diff --git a/internal/apicommands/catalog.gen.go b/internal/apicommands/catalog.gen.go index 6df7867..df2b335 100644 --- a/internal/apicommands/catalog.gen.go +++ b/internal/apicommands/catalog.gen.go @@ -1,13 +1,13 @@ -// Code generated by generate-operations from github.com/sumup/sumup-go v0.19.0. DO NOT EDIT. -// OpenAPI SHA-256: b050b86c35d18907a2a6b9f633bd1460568f37e450465047947c2aefad98d72e +// Code generated by generate-operations from github.com/sumup/sumup-go v0.21.0. DO NOT EDIT. +// OpenAPI SHA-256: 05ccb4935d5687339dfc111cf2294a8ae36762393cde5d5ee50361a284eb5949 package apicommands const ( CatalogSDKModule = "github.com/sumup/sumup-go" - CatalogSDKVersion = "v0.19.0" + CatalogSDKVersion = "v0.21.0" CatalogOpenAPIVersion = "1.0.0" - CatalogSpecSHA256 = "b050b86c35d18907a2a6b9f633bd1460568f37e450465047947c2aefad98d72e" + CatalogSpecSHA256 = "05ccb4935d5687339dfc111cf2294a8ae36762393cde5d5ee50361a284eb5949" ) var Operations = []Operation{ @@ -18,7 +18,7 @@ var Operations = []Operation{ HTTPMethod: "POST", Path: "/v0.1/checkouts", Summary: "Create a checkout", - Description: "Creates a new payment checkout resource. The unique `checkout_reference` created by this request, is used for further manipulation of the checkout.\n\nFor 3DS checkouts, add the `redirect_url` parameter to your request body schema.\nTo use the [Hosted Checkout](https://developer.sumup.com/online-payments/checkouts/hosted-checkout/) page, set the `hosted_checkout.enabled` to `true`.\n\nFollow by processing a checkout to charge the provided payment instrument.", + Description: "Creates a payment checkout for the specified merchant, amount, and currency. Supply a `checkout_reference` to identify the payment attempt in your own systems. Creating a checkout does not charge a payment instrument.\n\nSet `hosted_checkout.enabled` to `true` to receive a [Hosted Checkout](https://developer.sumup.com/online-payments/checkouts/hosted-checkout/) URL where the customer can complete the payment.\nUse `redirect_url` for redirect-based payment and 3DS flows. If `return_url` is provided, SumUp sends processing updates to that backend callback URL.\n\nComplete the payment through [Hosted Checkout](https://developer.sumup.com/online-payments/checkouts/hosted-checkout/) or the [Payment Widget](https://developer.sumup.com/online-payments/checkouts/card-widget).", RequestBody: &RequestBody{Schema: "CheckoutCreateRequest", Required: true}, }, { @@ -30,7 +30,7 @@ var Operations = []Operation{ Summary: "Create an Apple Pay session", Description: "Creates an Apple Pay merchant session for the specified checkout.\n\nUse this endpoint after the customer selects Apple Pay and before calling\n`ApplePaySession.completeMerchantValidation(...)` in the browser.\nSumUp validates the merchant session request and returns the Apple Pay\nsession object that your frontend should pass to Apple's JavaScript API.", Parameters: []Parameter{ - {Name: "checkout_id", Location: "path", Description: "Unique identifier of the checkout resource.", Type: "string", Format: "", Required: true}, + {Name: "checkout_id", Location: "path", Description: "SumUp-generated `id` returned when creating a checkout. Use this value to retrieve, update, process, or deactivate the checkout.", Type: "string", Format: "", Required: true}, }, RequestBody: &RequestBody{Schema: "object", Required: false}, }, @@ -43,7 +43,7 @@ var Operations = []Operation{ Summary: "Deactivate a checkout", Description: "Deactivates an identified checkout resource. If the checkout has already been processed it can not be deactivated.", Parameters: []Parameter{ - {Name: "checkout_id", Location: "path", Description: "Unique identifier of the checkout resource.", Type: "string", Format: "", Required: true}, + {Name: "checkout_id", Location: "path", Description: "SumUp-generated `id` returned when creating a checkout. Use this value to retrieve, update, process, or deactivate the checkout.", Type: "string", Format: "", Required: true}, }, }, { @@ -53,9 +53,9 @@ var Operations = []Operation{ HTTPMethod: "GET", Path: "/v0.1/checkouts/{checkout_id}", Summary: "Retrieve a checkout", - Description: "Retrieves an identified checkout resource. Use this request after processing a checkout to confirm its status and inform the end user respectively.", + Description: "Retrieves a checkout by its SumUp `checkout_id`. After processing a payment, returning from a redirect, or receiving a checkout notification, retrieve the checkout to confirm its current `status` before updating your order or displaying the payment outcome to the payer.", Parameters: []Parameter{ - {Name: "checkout_id", Location: "path", Description: "Unique identifier of the checkout resource.", Type: "string", Format: "", Required: true}, + {Name: "checkout_id", Location: "path", Description: "SumUp-generated `id` returned when creating a checkout. Use this value to retrieve, update, process, or deactivate the checkout.", Type: "string", Format: "", Required: true}, }, }, { @@ -67,7 +67,7 @@ var Operations = []Operation{ Summary: "List checkouts", Description: "Lists created checkout resources according to the applied `checkout_reference`.", Parameters: []Parameter{ - {Name: "checkout_reference", Location: "query", Description: "Filters the list of checkout resources by the unique reference of the checkout.", Type: "string", Format: "", Required: false}, + {Name: "checkout_reference", Location: "query", Description: "Filters checkouts by the merchant-defined `checkout_reference` supplied when creating the checkout. This is separate from the SumUp-generated checkout `id`.", Type: "string", Format: "", Required: false}, }, }, { @@ -77,13 +77,27 @@ var Operations = []Operation{ HTTPMethod: "GET", Path: "/v0.1/merchants/{merchant_code}/payment-methods", Summary: "Get available payment methods", - Description: "Get payment methods available for the given merchant to use with a checkout.", + Description: "Lists the payment methods available to the merchant for checkout payments. Use the optional amount and currency filters to check eligibility for a particular payment before presenting payment options to the payer.", Parameters: []Parameter{ {Name: "merchant_code", Location: "path", Description: "Short unique identifier for the merchant.", Type: "string", Format: "", Required: true}, - {Name: "amount", Location: "query", Description: "The amount for which the payment methods should be eligible, in major units.", Type: "number", Format: "", Required: false}, - {Name: "currency", Location: "query", Description: "The currency for which the payment methods should be eligible.", Type: "string", Format: "", Required: false}, + {Name: "amount", Location: "query", Description: "Payment amount in major units, for example `9.99` for EUR 9.99. When filtering by `amount`, also provide `currency`.", Type: "number", Format: "", Required: false}, + {Name: "currency", Location: "query", Description: "Three-letter ISO 4217 currency code for which the payment methods should be eligible, for example `EUR`.", Type: "string", Format: "", Required: false}, }, }, + { + ID: "ProcessCheckout", + Client: "Checkouts", + SDKMethod: "Process", + HTTPMethod: "PUT", + Path: "/v0.1/checkouts/{checkout_id}", + Summary: "Process a checkout", + Description: ":::caution[PCI DSS compliance required]\nWhen you submit raw card details directly to the Checkout API, your systems store, process, or transmit cardholder data and are therefore subject to applicable [PCI DSS requirements](https://www.pcisecuritystandards.org/document_library/). You should only use this integration if your environment is appropriately PCI DSS compliant.\n:::\n\nProcessing a checkout will attempt to charge the provided payment instrument for the amount of the specified checkout resource initiated in the `Create a checkout` endpoint.\n\nA processing response can require an additional payer action, such as a 3DS challenge or a payment-provider redirect. If `next_step` is returned, follow its instructions to continue the payment flow.\n\nRetrieve the checkout afterwards to confirm its payment status. Acceptance of the processing request does not by itself mean the checkout is paid.", + Unsupported: true, + Parameters: []Parameter{ + {Name: "checkout_id", Location: "path", Description: "SumUp-generated `id` returned when creating a checkout. Use this value to retrieve, update, process, or deactivate the checkout.", Type: "string", Format: "", Required: true}, + }, + RequestBody: &RequestBody{Schema: "ProcessCheckout", Required: true}, + }, { ID: "UpdateCheckout", Client: "Checkouts", @@ -91,9 +105,9 @@ var Operations = []Operation{ HTTPMethod: "PATCH", Path: "/v0.1/checkouts/{checkout_id}", Summary: "Update a checkout", - Description: "Updates an identified checkout resource.", + Description: "Updates the amount, currency, description, reference, expiration, or customer associated with an existing checkout. Only the supplied fields are updated.\n\nThis request changes the checkout details; it does not charge a payment instrument. Process the checkout separately to attempt a payment.", Parameters: []Parameter{ - {Name: "checkout_id", Location: "path", Description: "Unique identifier of the checkout resource.", Type: "string", Format: "", Required: true}, + {Name: "checkout_id", Location: "path", Description: "SumUp-generated `id` returned when creating a checkout. Use this value to retrieve, update, process, or deactivate the checkout.", Type: "string", Format: "", Required: true}, }, RequestBody: &RequestBody{Schema: "CheckoutUpdateRequest", Required: true}, }, @@ -104,7 +118,7 @@ var Operations = []Operation{ HTTPMethod: "POST", Path: "/v0.1/customers", Summary: "Create a customer", - Description: "Creates a new saved customer resource which you can later manipulate and save payment instruments to.", + Description: "Creates a customer using the `customer_id` you supply. Choose an identifier that maps to the payer in your own system and reuse it when retrieving the customer or associating checkouts and saved payment instruments with them.", RequestBody: &RequestBody{Schema: "Customer", Required: true}, }, { @@ -116,7 +130,7 @@ var Operations = []Operation{ Summary: "Deactivate a payment instrument", Description: "Deactivates an identified card payment instrument resource for a customer.", Parameters: []Parameter{ - {Name: "customer_id", Location: "path", Description: "Unique identifier of the saved customer resource.", Type: "string", Format: "", Required: true}, + {Name: "customer_id", Location: "path", Description: "The `customer_id` you supplied when creating the customer.", Type: "string", Format: "", Required: true}, {Name: "token", Location: "path", Description: "Unique token identifying the card saved as a payment instrument resource.", Type: "string", Format: "", Required: true}, }, }, @@ -127,9 +141,9 @@ var Operations = []Operation{ HTTPMethod: "GET", Path: "/v0.1/customers/{customer_id}", Summary: "Retrieve a customer", - Description: "Retrieves an identified saved customer resource through the unique `customer_id` parameter, generated upon customer creation.", + Description: "Retrieves a saved customer using the `customer_id` you supplied when creating the customer.", Parameters: []Parameter{ - {Name: "customer_id", Location: "path", Description: "Unique identifier of the saved customer resource.", Type: "string", Format: "", Required: true}, + {Name: "customer_id", Location: "path", Description: "The `customer_id` you supplied when creating the customer.", Type: "string", Format: "", Required: true}, }, }, { @@ -141,7 +155,7 @@ var Operations = []Operation{ Summary: "List payment instruments", Description: "Lists all payment instrument resources that are saved for an identified customer.", Parameters: []Parameter{ - {Name: "customer_id", Location: "path", Description: "Unique identifier of the saved customer resource.", Type: "string", Format: "", Required: true}, + {Name: "customer_id", Location: "path", Description: "The `customer_id` you supplied when creating the customer.", Type: "string", Format: "", Required: true}, }, }, { @@ -153,7 +167,7 @@ var Operations = []Operation{ Summary: "Update a customer", Description: "Updates an identified saved customer resource's personal details.\n\nThe request only overwrites the parameters included in the request, all other parameters will remain with their initially assigned values.", Parameters: []Parameter{ - {Name: "customer_id", Location: "path", Description: "Unique identifier of the saved customer resource.", Type: "string", Format: "", Required: true}, + {Name: "customer_id", Location: "path", Description: "The `customer_id` you supplied when creating the customer.", Type: "string", Format: "", Required: true}, }, RequestBody: &RequestBody{Schema: "object", Required: true}, }, @@ -164,7 +178,7 @@ var Operations = []Operation{ HTTPMethod: "POST", Path: "/v0.1/merchants/{merchant_code}/members", Summary: "Create a member", - Description: "Create a merchant member.", + Description: "Adds a member to the merchant account with the specified roles.\n\nBy default, sends an invitation email to the provided address. The recipient must accept the invitation to join the account.\nWhen `is_managed_user` is `true`, creates a managed user with the provided password and optional nickname and assigns the roles directly, without sending an invitation.", Parameters: []Parameter{ {Name: "merchant_code", Location: "path", Description: "Short unique identifier for the merchant.", Type: "string", Format: "", Required: true}, }, @@ -222,7 +236,7 @@ var Operations = []Operation{ HTTPMethod: "PUT", Path: "/v0.1/merchants/{merchant_code}/members/{member_id}", Summary: "Update a member", - Description: "Update the merchant member.", + Description: "Updates a merchant member and returns the updated member.\n\nProviding `roles` replaces the member's assigned roles and can grant or revoke access. Providing `metadata` replaces the entire metadata object.\nFor managed users, `user.nickname` changes the display name and `user.password` replaces the password. Updating the password also enables the managed user account.", Parameters: []Parameter{ {Name: "merchant_code", Location: "path", Description: "Short unique identifier for the merchant.", Type: "string", Format: "", Required: true}, {Name: "member_id", Location: "path", Description: "The ID of the member to retrieve.", Type: "string", Format: "", Required: true}, @@ -245,8 +259,9 @@ var Operations = []Operation{ {Name: "resource.type", Location: "query", Description: "Filter memberships by resource kind.", Type: "ResourceType", Format: "", Required: false}, {Name: "resource.attributes.sandbox", Location: "query", Description: "Filter memberships by the sandbox status of the resource the membership is in.", Type: "boolean", Format: "", Required: false}, {Name: "resource.name", Location: "query", Description: "Filter memberships by the name of the resource the membership is in.", Type: "string", Format: "", Required: false}, - {Name: "resource.parent.id", Location: "query", Description: "Filter memberships by the parent of the resource the membership is in.\nWhen filtering by parent both `resource.parent.id` and `resource.parent.type` must be present. Pass explicit null to filter for resources without a parent.", Type: "string", Format: "", Required: false}, - {Name: "resource.parent.type", Location: "query", Description: "Filter memberships by the parent of the resource the membership is in.\nWhen filtering by parent both `resource.parent.id` and `resource.parent.type` must be present. Pass explicit null to filter for resources without a parent.", Type: "", Format: "", Required: false}, + {Name: "resource.id", Location: "query", Description: "Filter memberships by the ID of the resource the membership is in.", Type: "string", Format: "", Required: false}, + {Name: "resource.parent.id", Location: "query", Description: "Filter memberships by the parent of the resource the membership is in.\nOmit both `resource.parent.id` and `resource.parent.type` to skip parent filtering. When filtering by parent, both parameters must be present. To select resources without a parent, set each parameter to an empty value. Otherwise, both parameters must identify a parent.", Type: "string", Format: "", Required: false}, + {Name: "resource.parent.type", Location: "query", Description: "Filter memberships by the parent of the resource the membership is in.\nOmit both `resource.parent.id` and `resource.parent.type` to skip parent filtering. When filtering by parent, both parameters must be present. To select resources without a parent, set each parameter to an empty value. Otherwise, both parameters must identify a parent.", Type: "ResourceType", Format: "", Required: false}, {Name: "roles", Location: "query", Description: "Filter the returned memberships by role.", Type: "array", Format: "", Required: false}, }, }, @@ -259,7 +274,6 @@ var Operations = []Operation{ Summary: "Get Merchant", Description: "Returns a Merchant for a valid Merchant code.", Parameters: []Parameter{ - {Name: "version", Location: "query", Description: "The version of the resource. At the moment, the only supported value is `latest`. When provided and the requested resource's `change_status` is pending, the resource will be returned with all pending changes applied. When no changes are pending the resource is returned as is. The `change_status` in the response body will reflect the current state of the resource.", Type: "string", Format: "", Required: false}, {Name: "merchant_code", Location: "path", Description: "Short unique identifier for the merchant.", Type: "string", Format: "", Required: true}, }, }, @@ -272,7 +286,6 @@ var Operations = []Operation{ Summary: "Get Person", Description: "Returns a single Person related to a Merchant.", Parameters: []Parameter{ - {Name: "version", Location: "query", Description: "The version of the resource. At the moment, the only supported value is `latest`. When provided and the requested resource's `change_status` is pending, the resource will be returned with all pending changes applied. When no changes are pending the resource is returned as is. The `change_status` in the response body will reflect the current state of the resource.", Type: "string", Format: "", Required: false}, {Name: "merchant_code", Location: "path", Description: "Short unique identifier for the merchant.", Type: "string", Format: "", Required: true}, {Name: "person_id", Location: "path", Description: "Person ID", Type: "string", Format: "", Required: true}, }, @@ -286,7 +299,6 @@ var Operations = []Operation{ Summary: "List Persons", Description: "Returns the Persons related to a Merchant.", Parameters: []Parameter{ - {Name: "version", Location: "query", Description: "The version of the resource. At the moment, the only supported value is `latest`. When provided and the requested resource's `change_status` is pending, the resource will be returned with all pending changes applied. When no changes are pending the resource is returned as is. The `change_status` in the response body will reflect the current state of the resource.", Type: "string", Format: "", Required: false}, {Name: "merchant_code", Location: "path", Description: "Short unique identifier for the merchant.", Type: "string", Format: "", Required: true}, }, }, @@ -327,7 +339,7 @@ var Operations = []Operation{ HTTPMethod: "POST", Path: "/v0.1/merchants/{merchant_code}/readers/{reader_id}/checkout", Summary: "Create a Reader Checkout", - Description: "Creates a Checkout for a Reader.\n\nThis process is asynchronous and the actual transaction may take some time to be started on the device.\n\n\nThere are some caveats when using this endpoint:\n* The target device must be online, otherwise checkout won't be accepted\n* After the checkout is accepted, the system has 60 seconds to start the payment on the target device. During this time, any other checkout for the same device will be rejected.\n\n\n**Note**: If the target device is a Solo, it must be in version 3.3.24.3 or higher.", + Description: "Creates a Checkout for a Reader.\n\nThis process is asynchronous and the actual transaction may take some time to be started on the device.\n\n\nThere are some caveats when using this endpoint:\n* The target device must be online, otherwise checkout won't be accepted\n* After the checkout is accepted, the system has 60 seconds to start the payment on the target device. During this time, any other checkout for the same device will be rejected.\n\n\n**Note**: If the target device is a Solo, it must be in version 3.3.24.3 or higher.\n\n\nOptionally include an `affiliate` object to attach integrator metadata to the payment.\nProvide both `app_id` and `key` when including this object.", Parameters: []Parameter{ {Name: "merchant_code", Location: "path", Description: "Merchant Code", Type: "string", Format: "", Required: true}, {Name: "reader_id", Location: "path", Description: "The unique identifier of the Reader", Type: "string", Format: "", Required: true}, @@ -389,6 +401,19 @@ var Operations = []Operation{ {Name: "checkout_id", Location: "path", Description: "The unique identifier of the Checkout", Type: "string", Format: "", Required: true}, }, }, + { + ID: "GetReaderSettings", + Client: "Readers", + SDKMethod: "GetReaderSettings", + HTTPMethod: "GET", + Path: "/v0.1/merchants/{merchant_code}/readers/{reader_id}/settings", + Summary: "Get Reader Settings", + Description: "Returns the menu lock settings for a Solo reader.\n\nFor an online reader, returns the current settings. For an offline reader,\nreturns the last saved settings, which may differ from its current configuration.\nReturns `404` if the reader is offline and no saved settings are available.\n\nThe response may include the menu lock PIN.", + Parameters: []Parameter{ + {Name: "merchant_code", Location: "path", Description: "The merchant code of the account that owns the reader.", Type: "string", Format: "", Required: true}, + {Name: "reader_id", Location: "path", Description: "The unique identifier of the reader.", Type: "string", Format: "", Required: true}, + }, + }, { ID: "GetReaderStatus", Client: "Readers", @@ -435,13 +460,27 @@ var Operations = []Operation{ HTTPMethod: "PATCH", Path: "/v0.1/merchants/{merchant_code}/readers/{reader_id}", Summary: "Update a Reader", - Description: "Update a Reader.", + Description: "Updates a reader's name or metadata and returns the updated reader.\n\nProviding `metadata` replaces the entire metadata object; include all entries that should be retained. Omitted fields remain unchanged.", Parameters: []Parameter{ {Name: "merchant_code", Location: "path", Description: "Short unique identifier for the merchant.", Type: "string", Format: "", Required: true}, {Name: "reader_id", Location: "path", Description: "The unique identifier of the reader.", Type: "ReaderID", Format: "", Required: true}, }, RequestBody: &RequestBody{Schema: "object", Required: true}, }, + { + ID: "UpdateReaderSettings", + Client: "Readers", + SDKMethod: "UpdateReaderSettings", + HTTPMethod: "PATCH", + Path: "/v0.1/merchants/{merchant_code}/readers/{reader_id}/settings", + Summary: "Update Reader Settings", + Description: "Updates the menu lock settings for a Solo reader. The reader must be online.\nA successful response confirms that the reader applied the update.\n\nInclude at least one of `menu_lock.enabled` or `menu_lock.passcode`.\nOmitted settings keep their current values. The response contains only the\nsettings included in the request.\n\nWhen enabling menu lock for a reader without a PIN, include a four-digit PIN\nin `menu_lock.passcode` in the same request.", + Parameters: []Parameter{ + {Name: "merchant_code", Location: "path", Description: "The merchant code of the account that owns the reader.", Type: "string", Format: "", Required: true}, + {Name: "reader_id", Location: "path", Description: "The unique identifier of the reader.", Type: "string", Format: "", Required: true}, + }, + RequestBody: &RequestBody{Schema: "UpdateReaderSettingsRequest", Required: true}, + }, { ID: "GetReceipt", Client: "Receipts", @@ -449,7 +488,7 @@ var Operations = []Operation{ HTTPMethod: "GET", Path: "/v1.1/receipts/{transaction_id}", Summary: "Retrieve receipt details", - Description: "Retrieves receipt specific data for a transaction.", + Description: "Retrieves structured receipt data for a transaction belonging to the merchant specified by `mid`. The path accepts either the SumUp transaction ID or transaction code. Provide `tx_event_id` to include a specific transaction event, such as a refund, on the receipt.", Parameters: []Parameter{ {Name: "transaction_id", Location: "path", Description: "SumUp unique transaction ID or transaction code, e.g. TS7HDYLSKD.", Type: "string", Format: "", Required: true}, {Name: "mid", Location: "query", Description: "Short unique identifier for the merchant.", Type: "string", Format: "", Required: true}, @@ -514,13 +553,26 @@ var Operations = []Operation{ HTTPMethod: "PATCH", Path: "/v0.1/merchants/{merchant_code}/roles/{role_id}", Summary: "Update a role", - Description: "Update a custom role.", + Description: "Updates a custom role's name, description, or permissions and returns the updated role.\n\nProviding `permissions` replaces the role's permission list and changes the access granted to members assigned to that role. Omitted fields remain unchanged.", Parameters: []Parameter{ {Name: "merchant_code", Location: "path", Description: "Short unique identifier for the merchant.", Type: "string", Format: "", Required: true}, {Name: "role_id", Location: "path", Description: "The ID of the role to retrieve.", Type: "string", Format: "", Required: true}, }, RequestBody: &RequestBody{Schema: "object", Required: true}, }, + { + ID: "CreateMerchantToken", + Client: "Tokens", + SDKMethod: "Create", + HTTPMethod: "POST", + Path: "/v0.1/merchants/{merchant_code}/tokens", + Summary: "Create a merchant token", + Description: "Creates a short-lived token for the merchant account identified by `merchant_code` and the requested actions.\n\nCall this endpoint from your backend using a [supported authentication method](https://developer.sumup.com/api/authentication). You must have access to the merchant account. Before returning the token, check that the user signed in to your application is allowed to accept payments for that account.\n\nPass the returned `access_token` to the SDK. The token expires 15 minutes after creation; use `expires_in` for its remaining lifetime. To continue after it expires, create a new token through this endpoint.", + Parameters: []Parameter{ + {Name: "merchant_code", Location: "path", Description: "Unique code of the merchant account that will receive the payments.", Type: "string", Format: "", Required: true}, + }, + RequestBody: &RequestBody{Schema: "object", Required: true}, + }, { ID: "GetTransactionV2.1", Client: "Transactions", @@ -544,22 +596,22 @@ var Operations = []Operation{ HTTPMethod: "GET", Path: "/v2.1/merchants/{merchant_code}/transactions/history", Summary: "List transactions", - Description: "Lists detailed history of all transactions associated with the merchant profile.", + Description: "Lists transaction history for the merchant, with optional filters for payment type, status, and date range. The response contains the current page in `items` and pagination query strings in `links`.\n\nTo request another page, use the query string from the relevant link's `href` with this history endpoint. Use `changes_since` when retrieving transactions modified since a previous synchronization, including transactions created earlier whose status has changed.", Parameters: []Parameter{ {Name: "merchant_code", Location: "path", Description: "Short unique identifier for the merchant.", Type: "string", Format: "", Required: true}, {Name: "transaction_code", Location: "query", Description: "Retrieves the transaction resource with the specified transaction code.", Type: "string", Format: "", Required: false}, - {Name: "order", Location: "query", Description: "Specifies the order in which the returned results are displayed.", Type: "string", Format: "", Required: false}, - {Name: "limit", Location: "query", Description: "Specifies the maximum number of results per page. Value must be a positive integer and if not specified, will return 10 results.", Type: "integer", Format: "", Required: false}, - {Name: "users[]", Location: "query", Description: "Filters the returned results by user email.", Type: "array", Format: "", Required: false}, - {Name: "statuses[]", Location: "query", Description: "Filters the returned results by the specified list of final statuses of the transactions.", Type: "array", Format: "", Required: false}, + {Name: "order", Location: "query", Description: "Sort direction for the transaction history. Use `ascending` or `descending`; the default is `ascending`.", Type: "string", Format: "", Required: false}, + {Name: "limit", Location: "query", Description: "Maximum number of transactions per page. Must be a positive integer. Defaults to `10` when omitted; a page can contain fewer results.", Type: "integer", Format: "", Required: false}, + {Name: "users[]", Location: "query", Description: "Filters transactions by user email. For multiple values, repeat the query parameter, for example `users[]=first@example.com&users[]=second@example.com`.", Type: "array", Format: "", Required: false}, + {Name: "statuses[]", Location: "query", Description: "Filters transactions by the listed final statuses. For multiple values, repeat the query parameter, for example `statuses[]=SUCCESSFUL&statuses[]=REFUNDED`.", Type: "array", Format: "", Required: false}, {Name: "payment_types[]", Location: "query", Description: "Filters the returned results by the specified list of payment types used for the transactions.", Type: "array", Format: "", Required: false}, {Name: "entry_modes[]", Location: "query", Description: "Filters the returned results by the specified list of entry modes.", Type: "array", Format: "", Required: false}, {Name: "types[]", Location: "query", Description: "Filters the returned results by the specified list of transaction types.", Type: "array", Format: "", Required: false}, {Name: "changes_since", Location: "query", Description: "Filters the results by the latest modification time of resources and returns only transactions that are modified *at or after* the specified timestamp (in [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) format).", Type: "string", Format: "date-time", Required: false}, {Name: "newest_time", Location: "query", Description: "Filters the results by the creation time of resources and returns only transactions that are created *before* the specified timestamp (in [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) format).", Type: "string", Format: "date-time", Required: false}, - {Name: "newest_ref", Location: "query", Description: "Filters the results by the reference ID of transaction events and returns only transactions with events whose IDs are *smaller* than the specified value. This parameters supersedes the `newest_time` parameter (if both are provided in the request).", Type: "string", Format: "", Required: false}, + {Name: "newest_ref", Location: "query", Description: "Pagination reference that returns results before the specified reference. Use the value from a returned pagination link rather than constructing it yourself. This parameter takes precedence over `newest_time` when both are provided.", Type: "string", Format: "", Required: false}, {Name: "oldest_time", Location: "query", Description: "Filters the results by the creation time of resources and returns only transactions that are created *at or after* the specified timestamp (in [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) format).", Type: "string", Format: "date-time", Required: false}, - {Name: "oldest_ref", Location: "query", Description: "Filters the results by the reference ID of transaction events and returns only transactions with events whose IDs are *greater* than the specified value. This parameters supersedes the `oldest_time` parameter (if both are provided in the request).", Type: "string", Format: "", Required: false}, + {Name: "oldest_ref", Location: "query", Description: "Pagination reference that returns results after the specified reference. Use the value from a returned pagination link rather than constructing it yourself. This parameter takes precedence over `oldest_time` when both are provided.", Type: "string", Format: "", Required: false}, }, }, { @@ -569,25 +621,11 @@ var Operations = []Operation{ HTTPMethod: "POST", Path: "/v1.0/merchants/{merchant_code}/payments/{transaction_id}/refunds", Summary: "Refund a transaction", - Description: "Refunds an identified transaction either in full or partially.", + Description: "Refunds a transaction identified by its SumUp transaction ID. Omit the request body to request a full refund, or provide `amount` for a partial refund in the transaction's currency.\n\nRetrieve the transaction afterwards to inspect its refunded amount and refund events. The transaction must be eligible for a refund; see the error responses for invalid amounts, permissions, and processing failures.", Parameters: []Parameter{ {Name: "merchant_code", Location: "path", Description: "Short unique identifier for the merchant.", Type: "string", Format: "", Required: true}, {Name: "transaction_id", Location: "path", Description: "Unique identifier of the transaction.", Type: "string", Format: "", Required: true}, }, RequestBody: &RequestBody{Schema: "object", Required: false}, }, - { - ID: "ProcessCheckout", - Client: "Checkouts", - SDKMethod: "Process", - HTTPMethod: "PUT", - Path: "/v0.1/checkouts/{checkout_id}", - Summary: "Process a checkout", - Description: ":::caution[PCI DSS compliance required]\nWhen you submit raw card details directly to the Checkout API, your systems store, process, or transmit cardholder data and are therefore subject to applicable [PCI DSS requirements](https://www.pcisecuritystandards.org/document_library/). You should only use this integration if your environment is appropriately PCI DSS compliant.\n:::\n\nProcessing a checkout will attempt to charge the provided payment instrument for the amount of the specified checkout resource initiated in the `Create a checkout` endpoint.\n\nFollow this request with `Retrieve a checkout` to confirm its status.", - Unsupported: true, - Parameters: []Parameter{ - {Name: "checkout_id", Location: "path", Description: "Unique identifier of the checkout resource.", Type: "string", Format: "", Required: true}, - }, - RequestBody: &RequestBody{Schema: "ProcessCheckout", Required: true}, - }, } diff --git a/internal/codesamples/codesamples.go b/internal/codesamples/codesamples.go index 57db5cb..ec074fe 100644 --- a/internal/codesamples/codesamples.go +++ b/internal/codesamples/codesamples.go @@ -185,6 +185,9 @@ func renderCommand(document *openAPIDocument, bound boundCommand, example operat func buildInvocation(document *openAPIDocument, bound boundCommand, example operationExample) (commandInvocation, error) { invocation := commandInvocation{path: strings.Fields(bound.path)} + if operationID, _ := apicommands.OperationID(bound.command); operationID == "CreateMerchantToken" { + invocation.flags = append(invocation.flags, sampleFlag{name: "json", boolean: true}) + } for _, match := range argumentPattern.FindAllStringSubmatch(bound.command.ArgsUsage, -1) { value, ok := argumentValue(document, example, match[1]) if !ok { @@ -216,7 +219,7 @@ func buildInvocation(document *openAPIDocument, bound boundCommand, example oper if len(values) == 0 { continue } - invocation.flags = append(invocation.flags, sampleFlag{name: name, boolean: true}) + invocation.flags = append(invocation.flags, sampleFlag{name: name, value: values[0], boolean: true}) continue } for _, value := range values { @@ -325,6 +328,9 @@ func flagRequired(flag cli.Flag) bool { } func flagBoolean(flag cli.Flag) bool { + if _, ok := flag.(*cli.BoolFlag); ok { + return true + } boolean, ok := flag.(interface{ IsBoolFlag() bool }) return ok && boolean.IsBoolFlag() } @@ -340,7 +346,9 @@ func (invocation commandInvocation) source() string { source.WriteString(" \\\n ") source.WriteString("--") source.WriteString(flag.name) - if !flag.boolean { + if flag.boolean && flag.value == "false" { + source.WriteString("=false") + } else if !flag.boolean { source.WriteByte(' ') source.WriteString(shellQuote(flag.value)) } @@ -353,7 +361,11 @@ func (invocation commandInvocation) args() []string { args := append([]string{"sumup"}, invocation.path...) args = append(args, invocation.arguments...) for _, flag := range invocation.flags { - args = append(args, "--"+flag.name) + name := "--" + flag.name + if flag.boolean && flag.value == "false" { + name += "=false" + } + args = append(args, name) if !flag.boolean { args = append(args, flag.value) } diff --git a/internal/codesamples/codesamples_test.go b/internal/codesamples/codesamples_test.go index b5f5cd8..5e2abfa 100644 --- a/internal/codesamples/codesamples_test.go +++ b/internal/codesamples/codesamples_test.go @@ -64,7 +64,7 @@ func TestGenerate(t *testing.T) { --merchant-code "MH4H92C7" \ --description "Purchase" \ --redirect-url "https://sumup.com" \ - --valid-until "2020-02-29T10:56:56+00:00" + --valid-until "2030-12-31T23:59:59Z" `, createCheckout.Source) assert.Equal(t, `sumup merchants persons get "pers_5AKFHN2KSK8D3TS79DJE3P3A2Z" \ --merchant-code "MK10CL2A" @@ -74,6 +74,9 @@ func TestGenerate(t *testing.T) { assert.Contains(t, sampleByID(t, catalog.Samples, "CreateMerchantMember").Source, "sumup members create") assert.NotContains(t, sampleByID(t, catalog.Samples, "CreateMerchantMember").Source, "members invite") assert.Equal(t, "# not supported", sampleByID(t, catalog.Samples, "ProcessCheckout").Source) + assert.Contains(t, sampleByID(t, catalog.Samples, "CreateMerchantToken").Source, `--action "checkout"`) + assert.Contains(t, sampleByID(t, catalog.Samples, "CreateMerchantToken").Source, "--json \\\n") + assert.Contains(t, sampleByID(t, catalog.Samples, "UpdateReaderSettings").Source, "--menu-lock-enabled \\\n") encodedSample, err := json.Marshal(sampleByID(t, catalog.Samples, "CreateCheckout")) require.NoError(t, err) @@ -104,6 +107,39 @@ func TestGenerateRequiresVersion(t *testing.T) { require.EqualError(t, err, "cli version is required") } +func TestBooleanInvocations(t *testing.T) { + t.Parallel() + + for _, enabled := range []bool{true, false} { + name := "enabled" + wantFlag := "--menu-lock-enabled" + if !enabled { + name = "disabled" + wantFlag += "=false" + } + t.Run(name, func(t *testing.T) { + t.Parallel() + + bound := boundCommand{ + path: "readers settings update", + command: &cli.Command{Flags: []cli.Flag{ + &cli.BoolFlag{Name: "menu-lock-enabled"}, + }}, + } + example := operationExample{ + body: map[string]any{"menu_lock": map[string]any{"enabled": enabled}}, + bodyProvided: true, + } + + invocation, err := buildInvocation(&openAPIDocument{}, bound, example) + + require.NoError(t, err) + assert.Equal(t, []string{"sumup", "readers", "settings", "update", wantFlag}, invocation.args()) + assert.Equal(t, "sumup readers settings update \\\n "+wantFlag+"\n", invocation.source()) + }) + } +} + func TestGeneratedInvocationsReachAPITransport(t *testing.T) { for _, operation := range apicommands.Operations { t.Run(operation.ID, func(t *testing.T) { @@ -125,16 +161,20 @@ func TestGeneratedInvocationsReachAPITransport(t *testing.T) { called = true return nil, transportError })} + appCtx := &app.Context{ + Client: sumup.NewClient(sumupclient.WithClient(httpClient), sumupclient.WithBaseURL("https://api.test")), + MerchantCode: "M123", + Output: io.Discard, + StatusOutput: io.Discard, + } root := &cli.Command{ Name: "sumup", Commands: resourceCommands, + Flags: []cli.Flag{ + &cli.BoolFlag{Name: "json", Destination: &appCtx.JSONOutput}, + }, Metadata: map[string]any{ - app.ContextKey: &app.Context{ - Client: sumup.NewClient(sumupclient.WithClient(httpClient), sumupclient.WithBaseURL("https://api.test")), - MerchantCode: "M123", - Output: io.Discard, - StatusOutput: io.Discard, - }, + app.ContextKey: appCtx, }, } diff --git a/internal/codesamples/values.go b/internal/codesamples/values.go index caeb715..cf26395 100644 --- a/internal/codesamples/values.go +++ b/internal/codesamples/values.go @@ -183,13 +183,10 @@ func formatExampleValue(value any) []string { case nil: return nil case bool: - if !typed { - return nil - } - return []string{""} + return []string{strconv.FormatBool(typed)} case map[string]any: - if enabled, ok := typed["enabled"].(bool); ok && enabled { - return []string{""} + if enabled, ok := typed["enabled"].(bool); ok { + return []string{strconv.FormatBool(enabled)} } return nil case []any: diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 1c3afd2..b34d58b 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -14,6 +14,7 @@ import ( "github.com/sumup/sumup-cli/internal/commands/readers" "github.com/sumup/sumup-cli/internal/commands/receipts" "github.com/sumup/sumup-cli/internal/commands/roles" + "github.com/sumup/sumup-cli/internal/commands/tokens" "github.com/sumup/sumup-cli/internal/commands/transactions" "github.com/sumup/sumup-cli/internal/commands/version" ) @@ -32,6 +33,7 @@ func All() []*cli.Command { readers.NewCommand(), receipts.NewCommand(), roles.NewCommand(), + tokens.NewCommand(), transactions.NewCommand(), version.NewCommand(), } diff --git a/internal/commands/memberships/memberships.go b/internal/commands/memberships/memberships.go index 7df2dd3..a3ccb5b 100644 --- a/internal/commands/memberships/memberships.go +++ b/internal/commands/memberships/memberships.go @@ -46,6 +46,10 @@ func NewCommand() *cli.Command { Name: "resource-type", Usage: "Filter memberships by the resource type.", }, + &cli.StringFlag{ + Name: "resource-id", + Usage: "Filter memberships by resource ID.", + }, &cli.StringFlag{ Name: "resource-name", Usage: "Filter memberships by resource name.", @@ -92,6 +96,9 @@ func listMemberships(ctx context.Context, cmd *cli.Command) error { if value := cmd.String("resource-name"); value != "" { params.ResourceName = &value } + if value := cmd.String("resource-id"); value != "" { + params.ResourceID = new(value) + } if cmd.Bool("sandbox") { value := true params.ResourceAttributesSandbox = &value diff --git a/internal/commands/merchants/merchants.go b/internal/commands/merchants/merchants.go index 83cf65c..5e427e8 100644 --- a/internal/commands/merchants/merchants.go +++ b/internal/commands/merchants/merchants.go @@ -29,7 +29,6 @@ func NewCommand() *cli.Command { Action: getMerchant, Flags: []cli.Flag{ merchantCodeFlag("Merchant code to retrieve information for. Falls back to context."), - versionFlag(), }, }), { @@ -42,7 +41,6 @@ func NewCommand() *cli.Command { Action: listPersons, Flags: []cli.Flag{ merchantCodeFlag("Merchant code whose people should be listed. Falls back to context."), - versionFlag(), }, }), apicommands.Bind("GetPerson", &cli.Command{ @@ -52,7 +50,6 @@ func NewCommand() *cli.Command { Action: getPerson, Flags: []cli.Flag{ merchantCodeFlag("Merchant code associated with the person. Falls back to context."), - versionFlag(), }, }), }, @@ -69,13 +66,6 @@ func merchantCodeFlag(usage string) cli.Flag { } } -func versionFlag() cli.Flag { - return &cli.StringFlag{ - Name: "version", - Usage: "Resource version to retrieve. Currently only latest is supported.", - } -} - func getMerchant(ctx context.Context, cmd *cli.Command) error { appCtx, err := app.GetAppContext(cmd) if err != nil { @@ -87,12 +77,7 @@ func getMerchant(ctx context.Context, cmd *cli.Command) error { return err } - version, err := resourceVersion(cmd) - if err != nil { - return err - } - - merchant, err := appCtx.Client.Merchants.Get(ctx, merchantCode, sumup.MerchantsGetParams{Version: version}) + merchant, err := appCtx.Client.Merchants.Get(ctx, merchantCode) if err != nil { return fmt.Errorf("get merchant: %w", err) } @@ -113,12 +98,7 @@ func listPersons(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } - version, err := resourceVersion(cmd) - if err != nil { - return err - } - - response, err := appCtx.Client.Merchants.ListPersons(ctx, merchantCode, sumup.MerchantsListPersonsParams{Version: version}) + response, err := appCtx.Client.Merchants.ListPersons(ctx, merchantCode) if err != nil { return fmt.Errorf("list persons: %w", err) } @@ -160,12 +140,7 @@ func getPerson(ctx context.Context, cmd *cli.Command) error { if err != nil { return err } - version, err := resourceVersion(cmd) - if err != nil { - return err - } - - person, err := appCtx.Client.Merchants.GetPerson(ctx, merchantCode, personID, sumup.MerchantsGetPersonParams{Version: version}) + person, err := appCtx.Client.Merchants.GetPerson(ctx, merchantCode, personID) if err != nil { return fmt.Errorf("get person: %w", err) } @@ -175,18 +150,6 @@ func getPerson(ctx context.Context, cmd *cli.Command) error { return renderPerson(appCtx.Output, person) } -func resourceVersion(cmd *cli.Command) (*string, error) { - if !cmd.IsSet("version") { - return nil, nil - } - - version := strings.TrimSpace(cmd.String("version")) - if version != "latest" { - return nil, fmt.Errorf("version must be latest") - } - return &version, nil -} - func renderMerchant(appCtx *app.Context, merchant *sumup.Merchant) error { if merchant == nil { return nil diff --git a/internal/commands/merchants/merchants_command_internal_test.go b/internal/commands/merchants/merchants_command_internal_test.go index a3edfd1..da3f6d8 100644 --- a/internal/commands/merchants/merchants_command_internal_test.go +++ b/internal/commands/merchants/merchants_command_internal_test.go @@ -18,15 +18,15 @@ import ( ) func TestPersonCommands(t *testing.T) { - t.Run("list sends the requested version and renders persons", func(t *testing.T) { + t.Run("list renders persons without a version query", func(t *testing.T) { cmd, out := newMerchantsCommand(t, func(request *http.Request) *http.Response { assert.Equal(t, http.MethodGet, request.Method) assert.Equal(t, "/v1/merchants/M123/persons", request.URL.Path) - assert.Equal(t, "latest", request.URL.Query().Get("version")) + assert.Empty(t, request.URL.RawQuery) return merchantsJSONResponse(http.StatusOK, `{"items":[{"id":"person_123","given_name":"Ada","family_name":"Lovelace","relationships":["representative"],"user_id":"user_123"}]}`) }) - err := cmd.Run(t.Context(), []string{"merchants", "persons", "list", "--version", "latest"}) + err := cmd.Run(t.Context(), []string{"merchants", "persons", "list"}) require.NoError(t, err) for _, want := range []string{"Persons", "person_123", "Ada Lovelace", "representative", "user_123"} { @@ -58,7 +58,7 @@ func TestPersonCommands(t *testing.T) { err := cmd.Run(t.Context(), []string{"merchants", "get", "--version", "draft"}) - require.EqualError(t, err, "version must be latest") + require.ErrorContains(t, err, "flag provided but not defined: -version") }) } diff --git a/internal/commands/readers/readers.go b/internal/commands/readers/readers.go index 1c76d69..f91b2df 100644 --- a/internal/commands/readers/readers.go +++ b/internal/commands/readers/readers.go @@ -2,7 +2,6 @@ package readers import ( "context" - "errors" "fmt" "io" "math" @@ -27,6 +26,7 @@ func NewCommand() *cli.Command { Name: "readers", Usage: "Commands for managing in-person readers.", Commands: []*cli.Command{ + settingsCommand(), apicommands.Bind("ListReaders", &cli.Command{ Name: "list", Usage: "List paired readers for a merchant.", @@ -169,6 +169,10 @@ func NewCommand() *cli.Command { Name: "return-url", Usage: "URL that receives the payment result.", }, + &cli.StringFlag{ + Name: "payment-type", + Usage: "Payment method: card (default) or pix (Brazilian merchants only).", + }, &cli.StringFlag{ Name: "card-type", Usage: "Optional card type hint (required for some countries).", @@ -183,7 +187,7 @@ func NewCommand() *cli.Command { }, &cli.Float64SliceFlag{ Name: "tip-rate", - Usage: "Provide multiple --tip-rate values to configure suggested tips (percentages 0.01-0.99).", + Usage: "Provide exactly three distinct --tip-rate values (fractions from 0.01 to 1.00 in increments of 0.01).", }, &cli.StringFlag{ Name: "affiliate-app-id", @@ -232,7 +236,7 @@ func NewCommand() *cli.Command { }, &cli.StringFlag{ Name: "tip-amount", - Usage: "Optional tip amount in major units, added on top of the total amount.", + Usage: "Portion of the total amount that is a tip, expressed in major units.", }, &cli.StringFlag{ Name: "affiliate-app-id", @@ -332,9 +336,6 @@ func addReader(ctx context.Context, cmd *cli.Command) error { } func formatCreateReaderError(err error) error { - if pErr := new(sumup.Problem); errors.As(err, &pErr) { - return fmt.Errorf("create reader: %w", pErr) - } return fmt.Errorf("create reader: %w", err) } @@ -395,6 +396,13 @@ func readerCheckout(ctx context.Context, cmd *cli.Command) error { if returnURL := cmd.String("return-url"); returnURL != "" { body.ReturnURL = &returnURL } + if paymentType := cmd.String("payment-type"); paymentType != "" { + value := sumup.CreateCheckoutRequestPaymentType(paymentType) + if value != sumup.CreateCheckoutRequestPaymentTypeCard && value != sumup.CreateCheckoutRequestPaymentTypePix { + return fmt.Errorf("unsupported payment type %q: supported values: card, pix", paymentType) + } + body.PaymentType = new(value) + } if cardType := cmd.String("card-type"); cardType != "" { ct := sumup.CreateCheckoutRequestCardType(cardType) body.CardType = &ct @@ -463,7 +471,7 @@ func goReaderCheckout(ctx context.Context, cmd *cli.Command) error { body := sumup.ReadersCreateGoCheckoutParams{ ClientTransactionID: cmd.String("client-transaction-id"), - TotalAmount: sumup.Amount{ + TotalAmount: sumup.ReaderPaymentRequestParamsTotalAmount{ Currency: currency.Code(parsedCurrency), Value: value, }, @@ -697,17 +705,17 @@ func buildAffiliatePayload(cmd *cli.Command) (*sumup.CreateCheckoutRequestAffili if appID == "" && key == "" && foreignID == "" { return nil, nil } - if appID == "" || key == "" || foreignID == "" { - return nil, fmt.Errorf("affiliate requires --affiliate-app-id, --affiliate-key, and --affiliate-foreign-transaction-id") + if appID == "" || key == "" { + return nil, fmt.Errorf("affiliate requires --affiliate-app-id and --affiliate-key") } - return &sumup.CreateCheckoutRequestAffiliate{ - AppID: appID, - Key: key, - ForeignTransactionID: foreignID, - }, nil + affiliate := &sumup.CreateCheckoutRequestAffiliate{AppID: appID, Key: key} + if foreignID != "" { + affiliate.ForeignTransactionID = new(foreignID) + } + return affiliate, nil } -func buildGoAffiliatePayload(cmd *cli.Command) (*sumup.Affiliate, error) { +func buildGoAffiliatePayload(cmd *cli.Command) (*sumup.ReaderPaymentRequestParamsAffiliate, error) { appID := cmd.String("affiliate-app-id") key := cmd.String("affiliate-key") if appID == "" && key == "" { @@ -716,7 +724,7 @@ func buildGoAffiliatePayload(cmd *cli.Command) (*sumup.Affiliate, error) { if appID == "" || key == "" { return nil, fmt.Errorf("affiliate requires --affiliate-app-id and --affiliate-key") } - return &sumup.Affiliate{ + return &sumup.ReaderPaymentRequestParamsAffiliate{ AppID: appID, Key: key, }, nil @@ -767,10 +775,8 @@ func renderReaderCheckout(appCtx *app.Context, w io.Writer, data *sumup.GetReade if paymentStatus := data.PaymentStatus.Value(); paymentStatus != nil { details.Add("Payment Status", attribute.Styled(*paymentStatus)) } - if data.PaymentFailureReason != nil { - if reason := data.PaymentFailureReason.Value(); reason != nil && *reason != "" { - details.Add("Failure Reason", attribute.Styled(*reason)) - } + if reason := data.PaymentFailureReason.Value(); reason != nil && *reason != "" { + details.Add("Failure Reason", attribute.Styled(*reason)) } if validUntil := data.ValidUntil.Value(); validUntil != nil { details.Add("Valid Until", attribute.Styled(util.TimeOrDash(appCtx, validUntil))) diff --git a/internal/commands/readers/readers_internal_test.go b/internal/commands/readers/readers_internal_test.go index 63380f1..813e7f0 100644 --- a/internal/commands/readers/readers_internal_test.go +++ b/internal/commands/readers/readers_internal_test.go @@ -2,6 +2,8 @@ package readers import ( "bytes" + "errors" + "net/http" "testing" "time" @@ -26,12 +28,26 @@ func TestNewCommand(t *testing.T) { } func TestFormatCreateReaderError(t *testing.T) { + t.Parallel() + t.Run("wraps sparse problem responses without panicking", func(t *testing.T) { err := formatCreateReaderError(&sumup.Problem{}) require.Error(t, err) assert.Contains(t, err.Error(), "create reader:") }) + t.Run("preserves HTTP response and typed problem", func(t *testing.T) { + problem := &sumup.Problem{} + apiErr := &sumup.APIError{StatusCode: http.StatusBadRequest, Err: problem} + + err := formatCreateReaderError(apiErr) + + require.ErrorIs(t, err, apiErr) + require.ErrorIs(t, err, problem) + got, ok := errors.AsType[*sumup.APIError](err) + require.True(t, ok) + assert.Equal(t, http.StatusBadRequest, got.StatusCode) + }) } func TestRenderReader(t *testing.T) { diff --git a/internal/commands/readers/settings.go b/internal/commands/readers/settings.go new file mode 100644 index 0000000..07be2c9 --- /dev/null +++ b/internal/commands/readers/settings.go @@ -0,0 +1,131 @@ +package readers + +import ( + "context" + "fmt" + "regexp" + + "github.com/urfave/cli/v3" + + sumup "github.com/sumup/sumup-go" + "github.com/sumup/sumup-go/secret" + + "github.com/sumup/sumup-cli/internal/apicommands" + "github.com/sumup/sumup-cli/internal/app" + "github.com/sumup/sumup-cli/internal/commands/util" + "github.com/sumup/sumup-cli/internal/display" + "github.com/sumup/sumup-cli/internal/display/attribute" +) + +var menuLockPasscodePattern = regexp.MustCompile(`^[0-9]{4}$`) + +func settingsCommand() *cli.Command { + return &cli.Command{ + Name: "settings", + Usage: "Manage Solo reader settings.", + Commands: []*cli.Command{ + apicommands.Bind("GetReaderSettings", &cli.Command{ + Name: "get", + Usage: "Get current settings, or the last saved settings if the reader is offline.", + ArgsUsage: "", + Action: getReaderSettings, + Flags: readerSettingsFlags(), + }), + apicommands.Bind("UpdateReaderSettings", &cli.Command{ + Name: "update", + Usage: "Update menu lock settings on an online Solo reader.", + ArgsUsage: "", + Action: updateReaderSettings, + Flags: append(readerSettingsFlags(), + &cli.BoolFlag{ + Name: "menu-lock-enabled", + Usage: "Enable menu lock; use --menu-lock-enabled=false to disable it.", + }, + &cli.StringFlag{ + Name: "menu-lock-passcode", + Usage: "Four-digit PIN for unlocking the reader menu.", + Sources: cli.EnvVars("SUMUP_READER_MENU_LOCK_PASSCODE"), + }, + ), + }), + }, + } +} + +func readerSettingsFlags() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: "merchant-code", + Usage: "Merchant code that owns the reader. Falls back to context.", + Sources: cli.EnvVars("SUMUP_MERCHANT_CODE"), + }, + } +} + +func getReaderSettings(ctx context.Context, cmd *cli.Command) error { + appCtx, err := app.GetAppContext(cmd) + if err != nil { + return err + } + merchantCode, err := app.GetMerchantCode(cmd, "merchant-code") + if err != nil { + return err + } + readerID, err := util.RequireSingleArg(cmd, "reader ID") + if err != nil { + return err + } + response, err := appCtx.Client.Readers.GetReaderSettings(ctx, merchantCode, readerID) + if err != nil { + return fmt.Errorf("get reader settings: %w", err) + } + if appCtx.JSONOutput { + return display.PrintJSON(appCtx.Output, response) + } + return display.DataList(appCtx.Output, []attribute.KeyValue{ + attribute.ID(readerID), + attribute.Optional("Menu Lock Enabled", response.Data.MenuLock.Enabled), + }) +} + +func updateReaderSettings(ctx context.Context, cmd *cli.Command) error { + appCtx, err := app.GetAppContext(cmd) + if err != nil { + return err + } + merchantCode, err := app.GetMerchantCode(cmd, "merchant-code") + if err != nil { + return err + } + readerID, err := util.RequireSingleArg(cmd, "reader ID") + if err != nil { + return err + } + if !cmd.IsSet("menu-lock-enabled") && !cmd.IsSet("menu-lock-passcode") { + return fmt.Errorf("provide --menu-lock-enabled or --menu-lock-passcode") + } + body := sumup.ReadersUpdateReaderSettingsParams{} + if cmd.IsSet("menu-lock-enabled") { + body.MenuLock.Enabled = new(cmd.Bool("menu-lock-enabled")) + } + if cmd.IsSet("menu-lock-passcode") { + passcode := cmd.String("menu-lock-passcode") + if !menuLockPasscodePattern.MatchString(passcode) { + return fmt.Errorf("menu lock passcode must contain exactly four digits") + } + body.MenuLock.Passcode = new(secret.New(passcode)) + } + response, err := appCtx.Client.Readers.UpdateReaderSettings(ctx, merchantCode, readerID, body) + if err != nil { + return fmt.Errorf("update reader settings: %w", err) + } + return display.RenderMutation(appCtx.Output, appCtx.StatusOutput, appCtx.JSONOutput, display.MutationResult{ + JSONValue: response, + SuccessMessage: "Reader settings updated", + Details: []attribute.KeyValue{ + attribute.ID(readerID), + attribute.Attribute("Status", attribute.Styled(response.Data.Status)), + attribute.Optional("Menu Lock Enabled", response.Data.MenuLock.Enabled), + }, + }) +} diff --git a/internal/commands/readers/settings_test.go b/internal/commands/readers/settings_test.go new file mode 100644 index 0000000..50a217b --- /dev/null +++ b/internal/commands/readers/settings_test.go @@ -0,0 +1,170 @@ +package readers + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/urfave/cli/v3" + + sumup "github.com/sumup/sumup-go" + sumupclient "github.com/sumup/sumup-go/client" + + "github.com/sumup/sumup-cli/internal/app" +) + +func TestReaderSettings(t *testing.T) { + t.Parallel() + + t.Run("get renders menu lock state", func(t *testing.T) { + t.Parallel() + + cmd, out := newReadersCommand(t, func(request *http.Request) *http.Response { + assert.Equal(t, http.MethodGet, request.Method) + assert.Equal(t, "/v0.1/merchants/M123/readers/reader_42/settings", request.URL.Path) + return readersJSONResponse(`{"data":{"menu_lock":{"enabled":true,"passcode":"0123"}}}`) + }) + + err := cmd.Run(t.Context(), []string{"readers", "settings", "get", "reader_42"}) + + require.NoError(t, err) + assert.Contains(t, out.String(), "true") + assert.NotContains(t, out.String(), "0123") + }) + + t.Run("update preserves false and omits unspecified passcode", func(t *testing.T) { + t.Parallel() + + cmd, out := newReadersCommand(t, func(request *http.Request) *http.Response { + assert.Equal(t, http.MethodPatch, request.Method) + assert.Equal(t, "/v0.1/merchants/M123/readers/reader_42/settings", request.URL.Path) + body, err := io.ReadAll(request.Body) + require.NoError(t, err) + assert.JSONEq(t, `{"menu_lock":{"enabled":false}}`, string(body)) + return readersJSONResponse(`{"data":{"status":"applied","menu_lock":{"enabled":false}}}`) + }) + + err := cmd.Run(t.Context(), []string{"readers", "settings", "update", "--menu-lock-enabled=false", "reader_42"}) + + require.NoError(t, err) + assert.Contains(t, out.String(), "applied") + assert.Contains(t, out.String(), "false") + }) + + t.Run("update preserves leading zeros in passcode", func(t *testing.T) { + t.Parallel() + + cmd, _ := newReadersCommand(t, func(request *http.Request) *http.Response { + body, err := io.ReadAll(request.Body) + require.NoError(t, err) + assert.JSONEq(t, `{"menu_lock":{"passcode":"0123"}}`, string(body)) + return readersJSONResponse(`{"data":{"status":"applied","menu_lock":{"passcode":"0123"}}}`) + }) + + err := cmd.Run(t.Context(), []string{"readers", "settings", "update", "--menu-lock-passcode", "0123", "reader_42"}) + + require.NoError(t, err) + }) + + for _, test := range []struct { + name string + args []string + want string + }{ + {name: "empty update", args: []string{"reader_42"}, want: "provide --menu-lock-enabled or --menu-lock-passcode"}, + {name: "invalid passcode", args: []string{"--menu-lock-passcode", "123", "reader_42"}, want: "menu lock passcode must contain exactly four digits"}, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + cmd, _ := newReadersCommand(t, func(*http.Request) *http.Response { + t.Fatal("API should not be called") + return nil + }) + + err := cmd.Run(t.Context(), append([]string{"readers", "settings", "update"}, test.args...)) + + require.EqualError(t, err, test.want) + }) + } +} + +func TestReaderCheckoutPayloads(t *testing.T) { + t.Parallel() + + t.Run("affiliate transaction reference is optional", func(t *testing.T) { + t.Parallel() + + cmd, _ := newReadersCommand(t, func(request *http.Request) *http.Response { + var body sumup.ReadersCreateCheckoutParams + require.NoError(t, json.NewDecoder(request.Body).Decode(&body)) + affiliate := body.Affiliate.Value() + require.NotNil(t, affiliate) + assert.Equal(t, "com.example.app", affiliate.AppID) + assert.Equal(t, "example-key", affiliate.Key) + assert.Nil(t, affiliate.ForeignTransactionID) + require.NotNil(t, body.PaymentType) + assert.Equal(t, sumup.CreateCheckoutRequestPaymentTypePix, *body.PaymentType) + response := readersJSONResponse(`{"data":{}}`) + response.StatusCode = http.StatusCreated + return response + }) + + err := cmd.Run(t.Context(), []string{"readers", "checkout", "--amount", "10", "--currency", "BRL", "--payment-type", "pix", "--affiliate-app-id", "com.example.app", "--affiliate-key", "example-key", "reader_42"}) + + require.NoError(t, err) + }) + + t.Run("go checkout sends amount and affiliate", func(t *testing.T) { + t.Parallel() + + cmd, _ := newReadersCommand(t, func(request *http.Request) *http.Response { + body, err := io.ReadAll(request.Body) + require.NoError(t, err) + assert.JSONEq(t, `{"client_transaction_id":"order-123","total_amount":{"currency":"EUR","value":1000},"affiliate":{"app_id":"com.example.app","key":"example-key"}}`, string(body)) + return readersJSONResponse(`{"data":{}}`) + }) + + err := cmd.Run(t.Context(), []string{"readers", "go-checkout", "--amount", "10", "--currency", "EUR", "--client-transaction-id", "order-123", "--affiliate-app-id", "com.example.app", "--affiliate-key", "example-key", "reader_42"}) + + require.NoError(t, err) + }) +} + +type readersRoundTripFunc func(*http.Request) (*http.Response, error) + +func (function readersRoundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) { + return function(request) +} + +func newReadersCommand(t *testing.T, handler func(*http.Request) *http.Response) (*cli.Command, *bytes.Buffer) { + t.Helper() + + var out bytes.Buffer + httpClient := &http.Client{Transport: readersRoundTripFunc(func(request *http.Request) (*http.Response, error) { + return handler(request), nil + })} + cmd := NewCommand() + cmd.Metadata = map[string]any{ + app.ContextKey: &app.Context{ + Client: sumup.NewClient(sumupclient.WithClient(httpClient), sumupclient.WithBaseURL("https://api.test")), + MerchantCode: "M123", + Output: &out, + StatusOutput: io.Discard, + }, + } + return cmd, &out +} + +func readersJSONResponse(body string) *http.Response { + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader(body)), + } +} diff --git a/internal/commands/tokens/tokens.go b/internal/commands/tokens/tokens.go new file mode 100644 index 0000000..ac9ac5f --- /dev/null +++ b/internal/commands/tokens/tokens.go @@ -0,0 +1,73 @@ +package tokens + +import ( + "context" + "fmt" + "slices" + + "github.com/urfave/cli/v3" + + sumup "github.com/sumup/sumup-go" + + "github.com/sumup/sumup-cli/internal/apicommands" + "github.com/sumup/sumup-cli/internal/app" + "github.com/sumup/sumup-cli/internal/display" +) + +func NewCommand() *cli.Command { + return &cli.Command{ + Name: "tokens", + Usage: "Create short-lived merchant tokens for SumUp SDKs.", + Commands: []*cli.Command{ + apicommands.Bind("CreateMerchantToken", &cli.Command{ + Name: "create", + Usage: "Create a merchant token for the requested actions. Requires --json to output the access token.", + Action: createToken, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "merchant-code", + Usage: "Merchant code to create a token for. Falls back to context.", + Sources: cli.EnvVars("SUMUP_MERCHANT_CODE"), + }, + &cli.StringSliceFlag{ + Name: "action", + Usage: "Action to allow (checkout). Repeat for multiple actions.", + Required: true, + }, + }, + }), + }, + } +} + +func createToken(ctx context.Context, cmd *cli.Command) error { + appCtx, err := app.GetAppContext(cmd) + if err != nil { + return err + } + if !appCtx.JSONOutput { + return fmt.Errorf("creating a merchant token requires --json to output the access token") + } + merchantCode, err := app.GetMerchantCode(cmd, "merchant-code") + if err != nil { + return err + } + body := sumup.TokensCreateParams{} + for _, action := range cmd.StringSlice("action") { + if action != string(sumup.TokensCreateParamsActionCheckout) { + return fmt.Errorf("unsupported token action %q: supported values: checkout", action) + } + value := sumup.TokensCreateParamsAction(action) + if !slices.Contains(body.Actions, value) { + body.Actions = append(body.Actions, value) + } + } + if len(body.Actions) == 0 { + return fmt.Errorf("provide at least one --action") + } + token, err := appCtx.Client.Tokens.Create(ctx, merchantCode, body) + if err != nil { + return fmt.Errorf("create merchant token: %w", err) + } + return display.PrintJSON(appCtx.Output, token) +} diff --git a/internal/commands/tokens/tokens_test.go b/internal/commands/tokens/tokens_test.go new file mode 100644 index 0000000..ee02463 --- /dev/null +++ b/internal/commands/tokens/tokens_test.go @@ -0,0 +1,75 @@ +package tokens + +import ( + "bytes" + "io" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + sumup "github.com/sumup/sumup-go" + sumupclient "github.com/sumup/sumup-go/client" + + "github.com/sumup/sumup-cli/internal/app" +) + +func TestCreateToken(t *testing.T) { + t.Parallel() + + for _, jsonOutput := range []bool{false, true} { + name := "requires JSON output before making a request" + if jsonOutput { + name = "JSON output includes token and expiry" + } + t.Run(name, func(t *testing.T) { + t.Parallel() + + var out bytes.Buffer + called := false + httpClient := &http.Client{Transport: tokensRoundTripFunc(func(request *http.Request) (*http.Response, error) { + called = true + assert.Equal(t, http.MethodPost, request.Method) + assert.Equal(t, "/v0.1/merchants/M123/tokens", request.URL.Path) + body, err := io.ReadAll(request.Body) + require.NoError(t, err) + assert.JSONEq(t, `{"actions":["checkout"]}`, string(body)) + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader(`{"access_token":"example-token","expires_in":900,"token_type":"Bearer"}`)), + }, nil + })} + cmd := NewCommand() + cmd.Metadata = map[string]any{ + app.ContextKey: &app.Context{ + Client: sumup.NewClient(sumupclient.WithClient(httpClient), sumupclient.WithBaseURL("https://api.test")), + MerchantCode: "M123", + Output: &out, + StatusOutput: io.Discard, + JSONOutput: jsonOutput, + }, + } + + err := cmd.Run(t.Context(), []string{"tokens", "create", "--action", "checkout"}) + + if jsonOutput { + require.NoError(t, err) + require.True(t, called) + assert.JSONEq(t, `{"access_token":"example-token","expires_in":900,"token_type":"Bearer"}`, out.String()) + } else { + require.EqualError(t, err, "creating a merchant token requires --json to output the access token") + assert.False(t, called) + assert.Empty(t, out.String()) + } + }) + } +} + +type tokensRoundTripFunc func(*http.Request) (*http.Response, error) + +func (function tokensRoundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) { + return function(request) +} diff --git a/internal/currency/currency.go b/internal/currency/currency.go index 078b1b1..26876f2 100644 --- a/internal/currency/currency.go +++ b/internal/currency/currency.go @@ -24,11 +24,11 @@ type currencyInfo struct { } var infoByCurrency = map[sumup.Currency]currencyInfo{ - sumup.CurrencyBGN: {symbol: "лв", decimals: 2, position: positionAfter}, sumup.CurrencyBRL: {symbol: "R$", decimals: 2, position: positionBefore}, sumup.CurrencyCHF: {symbol: "CHF ", decimals: 2, position: positionBefore}, sumup.CurrencyCLP: {symbol: "$", decimals: 0, position: positionBefore}, sumup.CurrencyCZK: {symbol: "Kč", decimals: 2, position: positionAfter}, + sumup.CurrencyCop: {symbol: "$", decimals: 2, position: positionBefore}, sumup.CurrencyDKK: {symbol: "kr", decimals: 2, position: positionAfter}, sumup.CurrencyEUR: {symbol: "€", decimals: 2, position: positionAfterNoSpace}, sumup.CurrencyGBP: {symbol: "£", decimals: 2, position: positionBefore}, @@ -42,10 +42,10 @@ var infoByCurrency = map[sumup.Currency]currencyInfo{ } var codeToCurrency = map[string]sumup.Currency{ - "BGN": sumup.CurrencyBGN, "BRL": sumup.CurrencyBRL, "CHF": sumup.CurrencyCHF, "CLP": sumup.CurrencyCLP, + "COP": sumup.CurrencyCop, "CZK": sumup.CurrencyCZK, "DKK": sumup.CurrencyDKK, "EUR": sumup.CurrencyEUR, @@ -104,10 +104,10 @@ func Parse(value string) (sumup.Currency, error) { // Supported returns the currency codes understood by the CLI. func Supported() []string { return []string{ - "BGN", "BRL", "CHF", "CLP", + "COP", "CZK", "DKK", "EUR", diff --git a/internal/currency/currency_test.go b/internal/currency/currency_test.go index 1361605..f6c2435 100644 --- a/internal/currency/currency_test.go +++ b/internal/currency/currency_test.go @@ -10,6 +10,20 @@ import ( "github.com/sumup/sumup-cli/internal/currency" ) +func TestParse(t *testing.T) { + t.Parallel() + + got, err := currency.Parse("cop") + require.NoError(t, err) + assert.Equal(t, sumup.CurrencyCop, got) + assert.Contains(t, currency.Supported(), "COP") + assert.Equal(t, "$10.50", currency.Format(10.5, got)) + + _, err = currency.Parse("BGN") + require.ErrorContains(t, err, "unsupported currency") + assert.NotContains(t, currency.Supported(), "BGN") +} + func TestParseMajorUnits32(t *testing.T) { t.Run("parses decimal strings without using float flags", func(t *testing.T) { got, err := currency.ParseMajorUnits32("10.10") diff --git a/openapi.json b/openapi.json index cb75d06..a08e3f0 100644 --- a/openapi.json +++ b/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "SumUp REST API", "version": "1.0.0", - "description": "SumUp’s REST API operates with [JSON](https://www.json.org/json-en.html) HTTP requests and responses. The request bodies are sent through resource-oriented URLs and use the standard [HTTP response codes](https://developer.mozilla.org/docs/Web/HTTP/Status).\n\nYou can experiment and work on your integration in a sandbox that doesn't affect your regular data and doesn't process real transactions. To create a sandbox merchant account visit the [dashboard](https://me.sumup.com/settings/developer). To use the sandbox when interacting with SumUp APIs [create an API](https://me.sumup.com/settings/api-keys) key and use it for [authentication](https://developer.sumup.com/api/authentication).", + "description": "SumUp's REST API lets you create and process payments, manage saved customers and payment instruments, and retrieve transactions, payouts, and receipt details. It uses resource-oriented URLs and standard [HTTP response codes](https://developer.mozilla.org/docs/Web/HTTP/Status). Send request bodies as [JSON](https://www.json.org/json-en.html) with `Content-Type: application/json`, unless an endpoint specifies another format.\n\nYou can experiment and work on your integration in a sandbox that doesn't affect your regular data and doesn't process real transactions. To create a sandbox merchant account visit the [dashboard](https://me.sumup.com/settings/developer). To use the sandbox when interacting with SumUp APIs [create an API](https://me.sumup.com/settings/api-keys) key and use it for [authentication](https://developer.sumup.com/api/authentication).", "license": { "name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html" @@ -27,7 +27,7 @@ }, { "name": "Customers", - "description": "Allow your regular customers to save their information with the Customers model.\n\nThis will prevent re-entering payment instrument information for recurring payments on your platform.\n\nDepending on the needs you can allow, creating, listing or deactivating payment instruments \u0026 creating, retrieving and updating customers.", + "description": "Customers represent payers in your integration. Create a customer with your own `customer_id` to associate their personal details and saved payment instruments with your business records.\n\nTo save a card, create a checkout for that customer with `purpose = SETUP_RECURRING_PAYMENT`, then process it with the payer's consent and mandate details. See the [tokenization guide](https://developer.sumup.com/online-payments/guides/tokenization-with-payment-sdk/).\n\nUse the Customers endpoints to create, retrieve, or update customer details and to list or deactivate saved payment instruments. For subsequent payments, process a new checkout with the saved instrument's `token` and its associated `customer_id`.", "x-core-objects": [ { "$ref": "#/components/schemas/Customer" @@ -49,7 +49,7 @@ }, { "name": "Receipts", - "description": "The Receipts model obtains receipt-like details for specific transactions.", + "description": "Retrieve structured receipt data for a transaction, including payment, merchant, and acquirer details. Use this data to display a receipt in your application. The response is JSON, rather than a rendered receipt document.", "x-core-objects": [ { "$ref": "#/components/schemas/Receipt" @@ -103,6 +103,15 @@ "$ref": "#/components/schemas/Merchant" } ] + }, + { + "name": "Tokens", + "description": "Merchant tokens let your application accept card-present payments with SumUp SDKs that support token-based authentication. Use them when you want users to sign in to your application with your own authentication, then accept payments for a SumUp merchant account.\n\nCreate tokens on your backend using one of the [supported authentication methods](https://developer.sumup.com/api/authentication). Keep your authentication credentials on your backend. Return only the short-lived merchant token to your application and pass its `access_token` to the SDK.\n\nBefore returning a token, authenticate the user in your application and check that they can accept payments for the requested merchant account. A merchant token is limited to that account and the requested actions, and does not grant permissions beyond those of the identity that created it.\n\nTokens expire 15 minutes after creation. Use `expires_in` to determine the remaining lifetime and create a new token when needed. This endpoint does not return a refresh token. Treat merchant tokens as secrets: send them over HTTPS and keep them out of URLs, logs, and source code.", + "x-core-objects": [ + { + "$ref": "#/components/schemas/Token" + } + ] } ], "paths": { @@ -110,7 +119,7 @@ "get": { "operationId": "GetPaymentMethods", "summary": "Get available payment methods", - "description": "Get payment methods available for the given merchant to use with a checkout.", + "description": "Lists the payment methods available to the merchant for checkout payments. Use the optional amount and currency filters to check eligibility for a particular payment before presenting payment options to the payer.", "tags": [ "Checkouts" ], @@ -133,7 +142,7 @@ "in": "query", "name": "amount", "required": false, - "description": "The amount for which the payment methods should be eligible, in major units.", + "description": "Payment amount in major units, for example `9.99` for EUR 9.99. When filtering by `amount`, also provide `currency`.", "schema": { "type": "number", "example": 9.99 @@ -143,7 +152,7 @@ "in": "query", "name": "currency", "required": false, - "description": "The currency for which the payment methods should be eligible.", + "description": "Three-letter ISO 4217 currency code for which the payment methods should be eligible, for example `EUR`.", "schema": { "type": "string", "example": "EUR" @@ -282,7 +291,7 @@ "currency": "EUR", "merchant_code": "MH4H92C7", "description": "Purchase", - "valid_until": "2020-02-29T10:56:56+00:00", + "valid_until": "2030-12-31T23:59:59Z", "redirect_url": "https://sumup.com" } }, @@ -349,7 +358,7 @@ "id": "88fcf8de-304d-4820-8f1c-ec880290eb92", "status": "PENDING", "date": "2020-02-29T10:56:56+00:00", - "valid_until": "2020-02-29T10:56:56+00:00", + "valid_until": "2030-12-31T23:59:59Z", "customer_id": "831ff8d4cd5958ab5670", "mandate": { "type": "recurrent", @@ -386,7 +395,7 @@ "id": "88fcf8de-304d-4820-8f1c-ec880290eb92", "status": "PENDING", "date": "2020-02-29T10:56:56+00:00", - "valid_until": "2020-02-29T10:56:56+00:00", + "valid_until": "2030-12-31T23:59:59Z", "customer_id": "831ff8d4cd5958ab5670", "redirect_url": "https://mysite.com/completed_purchase", "transactions": [ @@ -580,7 +589,7 @@ { "name": "checkout_reference", "in": "query", - "description": "Filters the list of checkout resources by the unique reference of the checkout.", + "description": "Filters checkouts by the merchant-defined `checkout_reference` supplied when creating the checkout. This is separate from the SumUp-generated checkout `id`.", "required": false, "schema": { "type": "string", @@ -645,7 +654,7 @@ "name": "checkout_id", "in": "path", "required": true, - "description": "Unique identifier of the checkout resource.", + "description": "SumUp-generated `id` returned when creating a checkout. Use this value to retrieve, update, process, or deactivate the checkout.", "schema": { "type": "string", "example": "4e425463-3e1b-431d-83fa-1e51c2925e99" @@ -655,7 +664,7 @@ "get": { "operationId": "GetCheckout", "summary": "Retrieve a checkout", - "description": "Retrieves an identified checkout resource. Use this request after processing a checkout to confirm its status and inform the end user respectively.", + "description": "Retrieves a checkout by its SumUp `checkout_id`. After processing a payment, returning from a redirect, or receiving a checkout notification, retrieve the checkout to confirm its current `status` before updating your order or displaying the payment outcome to the payer.", "tags": [ "Checkouts" ], @@ -781,7 +790,7 @@ "currency": "EUR", "description": "Updated purchase", "checkout_reference": "f00a8f74-b05d-4605-bd73-2a901bae5802", - "valid_until": "2020-02-29T10:56:56+00:00", + "valid_until": "2030-12-31T23:59:59Z", "customer_id": "831ff8d4cd5958ab5670" } } @@ -805,7 +814,7 @@ "id": "88fcf8de-304d-4820-8f1c-ec880290eb92", "status": "PENDING", "date": "2020-02-29T10:56:56+00:00", - "valid_until": "2020-02-29T10:56:56+00:00", + "valid_until": "2030-12-31T23:59:59Z", "customer_id": "831ff8d4cd5958ab5670", "transactions": [] } @@ -858,7 +867,7 @@ "put": { "operationId": "ProcessCheckout", "summary": "Process a checkout", - "description": ":::caution[PCI DSS compliance required]\nWhen you submit raw card details directly to the Checkout API, your systems store, process, or transmit cardholder data and are therefore subject to applicable [PCI DSS requirements](https://www.pcisecuritystandards.org/document_library/). You should only use this integration if your environment is appropriately PCI DSS compliant.\n:::\n\nProcessing a checkout will attempt to charge the provided payment instrument for the amount of the specified checkout resource initiated in the `Create a checkout` endpoint.\n\nFollow this request with `Retrieve a checkout` to confirm its status.", + "description": ":::caution[PCI DSS compliance required]\nWhen you submit raw card details directly to the Checkout API, your systems store, process, or transmit cardholder data and are therefore subject to applicable [PCI DSS requirements](https://www.pcisecuritystandards.org/document_library/). You should only use this integration if your environment is appropriately PCI DSS compliant.\n:::\n\nProcessing a checkout will attempt to charge the provided payment instrument for the amount of the specified checkout resource initiated in the `Create a checkout` endpoint.\n\nA processing response can require an additional payer action, such as a 3DS challenge or a payment-provider redirect. If `next_step` is returned, follow its instructions to continue the payment flow.\n\nRetrieve the checkout afterwards to confirm its payment status. Acceptance of the processing request does not by itself mean the checkout is paid.", "tags": [ "Checkouts" ], @@ -1033,9 +1042,9 @@ "description": "Purchase", "return_url": "http://example.com", "id": "4e425463-3e1b-431d-83fa-1e51c2925e99", - "status": "PENDING", + "status": "PAID", "date": "2020-02-29T10:56:56+00:00", - "valid_until": "2020-02-29T10:56:56+00:00", + "valid_until": "2030-12-31T23:59:59Z", "customer_id": "831ff8d4cd5958ab5670", "mandate": { "type": "recurrent", @@ -1072,7 +1081,7 @@ "merchant_code": "MH4H92C7", "description": "Purchase with token", "id": "4e425463-3e1b-431d-83fa-1e51c2925e99", - "status": "PENDING", + "status": "PAID", "date": "2020-02-29T10:56:56+00:00", "transaction_code": "TEENSK4W2K", "transaction_id": "410fc44a-5956-44e1-b5cc-19c6f8d727a4", @@ -1102,7 +1111,7 @@ } }, "CheckoutSuccessBoleto": { - "description": "Successfully processed checkout with Boleto", + "description": "Boleto payment initiated, awaiting payment by the payer", "value": { "checkout_reference": "f00a8f74-b05d-4605-bd73-2a901bae5802", "amount": 10.1, @@ -1138,7 +1147,7 @@ } }, "CheckoutSuccessiDeal": { - "description": "Successfully processed checkout with iDeal", + "description": "iDEAL processing response requiring a payer redirect", "value": { "next_step": { "url": "https://r3.girogate.de/ti/simideal", @@ -1156,7 +1165,7 @@ } }, "CheckoutSuccessBancontact": { - "description": "Successfully processed checkout with Bancontact", + "description": "Bancontact processing response requiring a payer redirect", "value": { "next_step": { "url": "https://r3.girogate.de/ti/simbcmc", @@ -1335,7 +1344,7 @@ }, "example": { "checkout_reference": "f00a8f74-b05d-4605-bd73-2a901bae5802", - "id": "817340ce-f1d9-4609-b90a-6152f8ee267j", + "id": "817340ce-f1d9-4609-b90a-6152f8ee267a", "amount": 2, "currency": "EUR", "merchant_code": "MH4H92C7", @@ -1343,7 +1352,7 @@ "purpose": "CHECKOUT", "status": "EXPIRED", "date": "2020-02-29T10:56:56+00:00", - "valid_until": "2020-02-29T10:56:56+00:00", + "valid_until": "2030-12-31T23:59:59Z", "merchant_name": "Sample Merchant", "transactions": [] } @@ -1426,7 +1435,7 @@ "name": "checkout_id", "in": "path", "required": true, - "description": "Unique identifier of the checkout resource.", + "description": "SumUp-generated `id` returned when creating a checkout. Use this value to retrieve, update, process, or deactivate the checkout.", "schema": { "type": "string", "example": "4e425463-3e1b-431d-83fa-1e51c2925e99" @@ -1438,7 +1447,7 @@ }, "x-scopes": [], "requestBody": { - "description": "The data needed to create an apple pay session for a checkout.", + "description": "Merchant validation details from the Apple Pay session in the payer's browser.", "content": { "application/json": { "schema": { @@ -1450,13 +1459,13 @@ "properties": { "context": { "type": "string", - "description": "the context to create this apple pay session.", + "description": "Hostname of the website displaying the Apple Pay payment sheet, without a URL scheme or path. Use the domain registered for Apple Pay.", "format": "hostname", "example": "example.com" }, "target": { "type": "string", - "description": "The target url to create this apple pay session.", + "description": "Apple Pay validation URL received as `validationURL` in the browser's `onvalidatemerchant` event.", "format": "uri", "example": "https://apple-pay-gateway-cert.apple.com/paymentservices/startSession" } @@ -1548,7 +1557,7 @@ "post": { "operationId": "CreateCustomer", "summary": "Create a customer", - "description": "Creates a new saved customer resource which you can later manipulate and save payment instruments to.", + "description": "Creates a customer using the `customer_id` you supply. Choose an identifier that maps to the payer in your own system and reuse it when retrieving the customer or associating checkouts and saved payment instruments with them.", "tags": [ "Customers" ], @@ -1602,29 +1611,7 @@ "$ref": "#/components/schemas/ErrorExtended" }, { - "type": "object", - "required": [ - "instance", - "error_code", - "error_message" - ], - "properties": { - "instance": { - "type": "string", - "description": "Unique identifier of this error occurrence.", - "example": "32a44c6c-85d3-49e8-86bf-a5bba98c4621" - }, - "error_code": { - "type": "string", - "description": "Platform code for the error.", - "example": "INVALID" - }, - "error_message": { - "type": "string", - "description": "Short description of the error.", - "example": "customer_id" - } - } + "$ref": "#/components/schemas/CustomerValidationError" } ] }, @@ -1711,7 +1698,7 @@ "name": "customer_id", "in": "path", "required": true, - "description": "Unique identifier of the saved customer resource.", + "description": "The `customer_id` you supplied when creating the customer.", "schema": { "type": "string", "example": "831ff8d4cd5958ab5670" @@ -1721,7 +1708,7 @@ "get": { "operationId": "GetCustomer", "summary": "Retrieve a customer", - "description": "Retrieves an identified saved customer resource through the unique `customer_id` parameter, generated upon customer creation.", + "description": "Retrieves a saved customer using the `customer_id` you supplied when creating the customer.", "tags": [ "Customers" ], @@ -1939,7 +1926,7 @@ "name": "customer_id", "in": "path", "required": true, - "description": "Unique identifier of the saved customer resource.", + "description": "The `customer_id` you supplied when creating the customer.", "schema": { "type": "string", "example": "831ff8d4cd5958ab5670" @@ -2055,7 +2042,7 @@ "name": "customer_id", "in": "path", "required": true, - "description": "Unique identifier of the saved customer resource.", + "description": "The `customer_id` you supplied when creating the customer.", "schema": { "type": "string", "example": "831ff8d4cd5958ab5670" @@ -2210,7 +2197,7 @@ "post": { "operationId": "RefundTransaction", "summary": "Refund a transaction", - "description": "Refunds an identified transaction either in full or partially.", + "description": "Refunds a transaction identified by its SumUp transaction ID. Omit the request body to request a full refund, or provide `amount` for a partial refund in the transaction's currency.\n\nRetrieve the transaction afterwards to inspect its refunded amount and refund events. The transaction must be eligible for a refund; see the error responses for invalid amounts, permissions, and processing failures.", "tags": [ "Transactions" ], @@ -2246,7 +2233,7 @@ "amount": { "type": "number", "format": "float", - "description": "Amount to be refunded. Eligible amount can't exceed the amount of the transaction and varies based on country and currency. If you do not specify a value, the system performs a full refund of the transaction.", + "description": "Amount to refund in major units of the transaction's currency, for example `5` for EUR 5.00. It must be greater than zero and cannot exceed the amount eligible for a refund. Eligibility depends on the transaction and country/currency rules. If omitted, the system requests a full refund.", "example": 5 } } @@ -2260,7 +2247,8 @@ "content": { "application/json": { "schema": { - "type": "object" + "type": "object", + "properties": {} }, "example": {} } @@ -2532,7 +2520,7 @@ "get": { "operationId": "ListTransactionsV2.1", "summary": "List transactions", - "description": "Lists detailed history of all transactions associated with the merchant profile.", + "description": "Lists transaction history for the merchant, with optional filters for payment type, status, and date range. The response contains the current page in `items` and pagination query strings in `links`.\n\nTo request another page, use the query string from the relevant link's `href` with this history endpoint. Use `changes_since` when retrieving transactions modified since a previous synchronization, including transactions created earlier whose status has changed.", "tags": [ "Transactions" ], @@ -2578,7 +2566,7 @@ { "name": "order", "in": "query", - "description": "Specifies the order in which the returned results are displayed.", + "description": "Sort direction for the transaction history. Use `ascending` or `descending`; the default is `ascending`.", "schema": { "type": "string", "enum": [ @@ -2591,7 +2579,7 @@ { "name": "limit", "in": "query", - "description": "Specifies the maximum number of results per page. Value must be a positive integer and if not specified, will return 10 results.", + "description": "Maximum number of transactions per page. Must be a positive integer. Defaults to `10` when omitted; a page can contain fewer results.", "schema": { "type": "integer", "example": 10 @@ -2600,7 +2588,7 @@ { "name": "users[]", "in": "query", - "description": "Filters the returned results by user email.", + "description": "Filters transactions by user email. For multiple values, repeat the query parameter, for example `users[]=first@example.com\u0026users[]=second@example.com`.", "required": false, "example": [ "merchant@example.com" @@ -2619,7 +2607,7 @@ { "name": "statuses[]", "in": "query", - "description": "Filters the returned results by the specified list of final statuses of the transactions.", + "description": "Filters transactions by the listed final statuses. For multiple values, repeat the query parameter, for example `statuses[]=SUCCESSFUL\u0026statuses[]=REFUNDED`.", "required": false, "schema": { "type": "array", @@ -2717,7 +2705,7 @@ { "name": "newest_ref", "in": "query", - "description": "Filters the results by the reference ID of transaction events and returns only transactions with events whose IDs are *smaller* than the specified value. This parameters supersedes the `newest_time` parameter (if both are provided in the request).", + "description": "Pagination reference that returns results before the specified reference. Use the value from a returned pagination link rather than constructing it yourself. This parameter takes precedence over `newest_time` when both are provided.", "required": false, "schema": { "type": "string", @@ -2738,7 +2726,7 @@ { "name": "oldest_ref", "in": "query", - "description": "Filters the results by the reference ID of transaction events and returns only transactions with events whose IDs are *greater* than the specified value. This parameters supersedes the `oldest_time` parameter (if both are provided in the request).", + "description": "Pagination reference that returns results after the specified reference. Use the value from a returned pagination link rather than constructing it yourself. This parameter takes precedence over `oldest_time` when both are provided.", "required": false, "schema": { "type": "string", @@ -3060,7 +3048,7 @@ "get": { "operationId": "GetReceipt", "summary": "Retrieve receipt details", - "description": "Retrieves receipt specific data for a transaction.", + "description": "Retrieves structured receipt data for a transaction belonging to the merchant specified by `mid`. The path accepts either the SumUp transaction ID or transaction code. Provide `tx_event_id` to include a specific transaction event, such as a refund, on the receipt.", "tags": [ "Receipts" ], @@ -5572,7 +5560,7 @@ } } }, - "description": "Creates a Checkout for a Reader.\n\nThis process is asynchronous and the actual transaction may take some time to be started on the device.\n\n\nThere are some caveats when using this endpoint:\n* The target device must be online, otherwise checkout won't be accepted\n* After the checkout is accepted, the system has 60 seconds to start the payment on the target device. During this time, any other checkout for the same device will be rejected.\n\n\n**Note**: If the target device is a Solo, it must be in version 3.3.24.3 or higher.\n", + "description": "Creates a Checkout for a Reader.\n\nThis process is asynchronous and the actual transaction may take some time to be started on the device.\n\n\nThere are some caveats when using this endpoint:\n* The target device must be online, otherwise checkout won't be accepted\n* After the checkout is accepted, the system has 60 seconds to start the payment on the target device. During this time, any other checkout for the same device will be rejected.\n\n\n**Note**: If the target device is a Solo, it must be in version 3.3.24.3 or higher.\n\n\nOptionally include an `affiliate` object to attach integrator metadata to the payment.\nProvide both `app_id` and `key` when including this object.\n", "operationId": "CreateReaderCheckout", "parameters": [ { @@ -5621,16 +5609,22 @@ "400": { "content": { "application/json": { + "example": { + "errors": { + "detail": "Invalid affiliate key.", + "type": "INVALID_AFFILIATE_KEY" + } + }, "schema": { "$ref": "#/components/schemas/CreateReaderCheckoutError" } }, "application/problem+json": { "example": { - "detail": "Bad Request", + "detail": "Invalid affiliate key.", "status": 400, - "title": "Bad Request", - "type": "https://developer.sumup.com/problem/bad-request" + "title": "Invalid Affiliate Key", + "type": "https://developer.sumup.com/problem/invalid-affiliate-key" }, "schema": { "$ref": "#/components/schemas/Problem" @@ -5689,11 +5683,23 @@ } }, "application/problem+json": { - "example": { - "detail": "Validation failed", - "status": 422, - "title": "Unprocessable Entity", - "type": "https://developer.sumup.com/problem/validation-error" + "examples": { + "PaymentMethodNotAllowed": { + "value": { + "detail": "Payment method not allowed for this merchant.", + "status": 422, + "title": "Payment Method Not Allowed", + "type": "https://developer.sumup.com/problem/payment-method-not-allowed" + } + }, + "ValidationError": { + "value": { + "detail": "Validation failed", + "status": 422, + "title": "Unprocessable Content", + "type": "https://developer.sumup.com/problem/validation-error" + } + } }, "schema": { "$ref": "#/components/schemas/Problem" @@ -5855,14 +5861,14 @@ ] } }, - "/v0.1/merchants/{merchant_code}/readers/{reader_id}/terminate": { - "post": { + "/v0.1/merchants/{merchant_code}/readers/{reader_id}/settings": { + "get": { "callbacks": {}, - "description": "Terminate a Reader Checkout stops the current transaction on the target device.\n\nThis process is asynchronous and the actual termination may take some time to be performed on the device.\n\n\nThere are some caveats when using this endpoint:\n* The target device must be online, otherwise terminate won't be accepted\n* The action will succeed only if the device is waiting for cardholder action: e.g: waiting for card, waiting for PIN, etc.\n* There is no confirmation of the termination.\n\nIf a transaction is successfully terminated and `return_url` was provided on Checkout, the transaction status will be sent as `failed` to the provided URL.\n\n\n**Note**: If the target device is a Solo, it must be in version 3.3.28.0 or higher.\n", - "operationId": "CreateReaderTerminate", + "description": "Returns the menu lock settings for a Solo reader.\n\nFor an online reader, returns the current settings. For an offline reader,\nreturns the last saved settings, which may differ from its current configuration.\nReturns `404` if the reader is offline and no saved settings are available.\n\nThe response may include the menu lock PIN.\n", + "operationId": "GetReaderSettings", "parameters": [ { - "description": "Merchant Code", + "description": "The merchant code of the account that owns the reader.", "example": "MC0X0ABC", "in": "path", "name": "merchant_code", @@ -5872,7 +5878,132 @@ } }, { - "description": "The unique identifier of the Reader", + "description": "The unique identifier of the reader.", + "example": "rdr_3MSAFM23CK82VSTT4BN6RWSQ65", + "in": "path", + "name": "reader_id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetReaderSettingsResponse" + } + } + }, + "description": "Reader settings retrieved successfully." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateReaderSettingsError" + } + }, + "application/problem+json": { + "example": { + "detail": "Unauthorized", + "status": 401, + "title": "Unauthorized", + "type": "https://developer.sumup.com/problem/unauthorized" + }, + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFound" + } + }, + "application/problem+json": { + "example": { + "detail": "The requested resource doesn't exist or does not belong to you.", + "status": 404, + "title": "Requested resource couldn't be found.", + "type": "https://developer.sumup.com/problem/not-found" + }, + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "The reader was not found, or it is offline and has no saved settings." + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateReaderSettingsUnprocessableEntity" + } + }, + "application/problem+json": { + "example": { + "detail": "Reader settings are only supported for Solo readers.", + "status": 422, + "title": "Unsupported Device Model", + "type": "https://developer.sumup.com/problem/unsupported-device-model" + }, + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "The reader model is not supported, or the reader could not return its settings." + } + }, + "security": [ + { + "apiKey": [] + }, + { + "oauth2": [ + "readers.read" + ] + } + ], + "summary": "Get Reader Settings", + "tags": [ + "Readers" + ], + "x-codegen": { + "method_name": "get_reader_settings" + }, + "x-permissions": [ + "readers.checkouts.view" + ], + "x-scopes": [ + "readers.read" + ] + }, + "patch": { + "callbacks": {}, + "description": "Updates the menu lock settings for a Solo reader. The reader must be online.\nA successful response confirms that the reader applied the update.\n\nInclude at least one of `menu_lock.enabled` or `menu_lock.passcode`.\nOmitted settings keep their current values. The response contains only the\nsettings included in the request.\n\nWhen enabling menu lock for a reader without a PIN, include a four-digit PIN\nin `menu_lock.passcode` in the same request.\n", + "operationId": "UpdateReaderSettings", + "parameters": [ + { + "description": "The merchant code of the account that owns the reader.", + "example": "MC0X0ABC", + "in": "path", + "name": "merchant_code", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "The unique identifier of the reader.", "example": "rdr_3MSAFM23CK82VSTT4BN6RWSQ65", "in": "path", "name": "reader_id", @@ -5884,23 +6015,31 @@ ], "requestBody": { "content": { - "application/json": {} + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateReaderSettingsRequest" + } + } }, - "description": "A checkout initial attributes", - "required": false + "description": "Menu lock settings to update.", + "required": true }, "responses": { - "202": { + "200": { "content": { - "application/json": {} + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateReaderSettingsResponse" + } + } }, - "description": "The Terminate action was successfully dispatched for the given reader." + "description": "Reader settings updated successfully." }, "400": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateReaderTerminateError" + "$ref": "#/components/schemas/UpdateReaderSettingsError" } }, "application/problem+json": { @@ -5915,13 +6054,13 @@ } } }, - "description": "Response when given params (or one of them) are invalid" + "description": "The request body is malformed." }, "401": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateReaderTerminateError" + "$ref": "#/components/schemas/UpdateReaderSettingsError" } }, "application/problem+json": { @@ -5957,13 +6096,19 @@ } } }, - "description": "Response when given reader is not found" + "description": "The reader was not found." }, "422": { "content": { "application/json": { + "example": { + "errors": { + "detail": "The device is offline.", + "type": "READER_OFFLINE" + } + }, "schema": { - "$ref": "#/components/schemas/CreateReaderTerminateUnprocessableEntity" + "$ref": "#/components/schemas/UpdateReaderSettingsUnprocessableEntity" } }, "application/problem+json": { @@ -5978,7 +6123,7 @@ } } }, - "description": "Response when given params (or one of them) are invalid" + "description": "The settings are invalid, the reader is offline, the reader model is not supported, or the reader could not apply the update." } }, "security": [ @@ -5991,26 +6136,26 @@ ] } ], - "summary": "Terminate a Reader Checkout", + "summary": "Update Reader Settings", "tags": [ "Readers" ], "x-codegen": { - "method_name": "terminate_checkout" + "method_name": "update_reader_settings" }, "x-permissions": [ - "readers.checkouts.delete" + "readers.checkouts.create" ], "x-scopes": [ "readers.write" ] } }, - "/v0.1/merchants/{merchant_code}/readers/{reader_id}/checkout/{checkout_id}": { - "get": { + "/v0.1/merchants/{merchant_code}/readers/{reader_id}/terminate": { + "post": { "callbacks": {}, - "description": "Get a Checkout for a Reader.\n", - "operationId": "GetReaderCheckout", + "description": "Terminate a Reader Checkout stops the current transaction on the target device.\n\nThis process is asynchronous and the actual termination may take some time to be performed on the device.\n\n\nThere are some caveats when using this endpoint:\n* The target device must be online, otherwise terminate won't be accepted\n* The action will succeed only if the device is waiting for cardholder action: e.g: waiting for card, waiting for PIN, etc.\n* There is no confirmation of the termination.\n\nIf a transaction is successfully terminated and `return_url` was provided on Checkout, the transaction status will be sent as `failed` to the provided URL.\n\n\n**Note**: If the target device is a Solo, it must be in version 3.3.28.0 or higher.\n", + "operationId": "CreateReaderTerminate", "parameters": [ { "description": "Merchant Code", @@ -6031,34 +6176,48 @@ "schema": { "type": "string" } - }, - { - "description": "The unique identifier of the Checkout", - "example": "74ecff66-1655-43ed-8ce3-193f49fa602f", - "in": "path", - "name": "checkout_id", - "required": true, - "schema": { - "type": "string" - } } ], + "requestBody": { + "content": { + "application/json": {} + }, + "description": "A checkout initial attributes", + "required": false + }, "responses": { - "200": { + "202": { + "content": { + "application/json": {} + }, + "description": "The Terminate action was successfully dispatched for the given reader." + }, + "400": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetReaderCheckoutResponse" + "$ref": "#/components/schemas/CreateReaderTerminateError" + } + }, + "application/problem+json": { + "example": { + "detail": "Bad Request", + "status": 400, + "title": "Bad Request", + "type": "https://developer.sumup.com/problem/bad-request" + }, + "schema": { + "$ref": "#/components/schemas/Problem" } } }, - "description": "The Checkout got successfully retrieved for the given reader." + "description": "Response when given params (or one of them) are invalid" }, "401": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateReaderCheckoutError" + "$ref": "#/components/schemas/CreateReaderTerminateError" } }, "application/problem+json": { @@ -6094,26 +6253,163 @@ } } }, - "description": "Response when given reader or checkout is not found" - } - }, - "security": [ - { - "apiKey": [] + "description": "Response when given reader is not found" }, - { - "oauth2": [ - "readers.read" - ] - } - ], - "summary": "Get a Reader Checkout", - "tags": [ - "Readers" - ], - "x-codegen": { - "method_name": "get_checkout" - }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateReaderTerminateUnprocessableEntity" + } + }, + "application/problem+json": { + "example": { + "detail": "The device is offline.", + "status": 422, + "title": "Reader Offline", + "type": "https://developer.sumup.com/problem/reader-offline" + }, + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "Response when given params (or one of them) are invalid" + } + }, + "security": [ + { + "apiKey": [] + }, + { + "oauth2": [ + "readers.write" + ] + } + ], + "summary": "Terminate a Reader Checkout", + "tags": [ + "Readers" + ], + "x-codegen": { + "method_name": "terminate_checkout" + }, + "x-permissions": [ + "readers.checkouts.delete" + ], + "x-scopes": [ + "readers.write" + ] + } + }, + "/v0.1/merchants/{merchant_code}/readers/{reader_id}/checkout/{checkout_id}": { + "get": { + "callbacks": {}, + "description": "Get a Checkout for a Reader.\n", + "operationId": "GetReaderCheckout", + "parameters": [ + { + "description": "Merchant Code", + "example": "MC0X0ABC", + "in": "path", + "name": "merchant_code", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "The unique identifier of the Reader", + "example": "rdr_3MSAFM23CK82VSTT4BN6RWSQ65", + "in": "path", + "name": "reader_id", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "The unique identifier of the Checkout", + "example": "74ecff66-1655-43ed-8ce3-193f49fa602f", + "in": "path", + "name": "checkout_id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetReaderCheckoutResponse" + } + } + }, + "description": "The Checkout got successfully retrieved for the given reader." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateReaderCheckoutError" + } + }, + "application/problem+json": { + "example": { + "detail": "Unauthorized", + "status": 401, + "title": "Unauthorized", + "type": "https://developer.sumup.com/problem/unauthorized" + }, + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "Unauthorized" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFound" + } + }, + "application/problem+json": { + "example": { + "detail": "The requested resource doesn't exist or does not belong to you.", + "status": 404, + "title": "Requested resource couldn't be found.", + "type": "https://developer.sumup.com/problem/not-found" + }, + "schema": { + "$ref": "#/components/schemas/Problem" + } + } + }, + "description": "Response when given reader or checkout is not found" + } + }, + "security": [ + { + "apiKey": [] + }, + { + "oauth2": [ + "readers.read" + ] + } + ], + "summary": "Get a Reader Checkout", + "tags": [ + "Readers" + ], + "x-codegen": { + "method_name": "get_checkout" + }, "x-permissions": [ "readers.checkouts.view" ], @@ -6121,6 +6417,157 @@ "readers.read" ] } + }, + "/v0.1/merchants/{merchant_code}/tokens": { + "post": { + "summary": "Create a merchant token", + "description": "Creates a short-lived token for the merchant account identified by `merchant_code` and the requested actions.\n\nCall this endpoint from your backend using a [supported authentication method](https://developer.sumup.com/api/authentication). You must have access to the merchant account. Before returning the token, check that the user signed in to your application is allowed to accept payments for that account.\n\nPass the returned `access_token` to the SDK. The token expires 15 minutes after creation; use `expires_in` for its remaining lifetime. To continue after it expires, create a new token through this endpoint.", + "operationId": "CreateMerchantToken", + "tags": [ + "Tokens" + ], + "security": [ + { + "apiKey": [] + }, + { + "oauth2": [] + } + ], + "x-scopes": [], + "x-codegen": { + "method_name": "create" + }, + "x-permissions": [ + { + "relation": "tokens.create", + "object_type": "merchant", + "object_id_param": "merchant_code" + } + ], + "parameters": [ + { + "in": "path", + "name": "merchant_code", + "description": "Unique code of the merchant account that will receive the payments.", + "required": true, + "schema": { + "type": "string", + "example": "MC0X0ABC" + } + } + ], + "requestBody": { + "description": "Actions the merchant token should allow.", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "actions" + ], + "properties": { + "actions": { + "type": "array", + "description": "Actions the token allows for this merchant account.", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "checkout" + ] + }, + "example": [ + "checkout" + ] + } + } + }, + "example": { + "actions": [ + "checkout" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Merchant token created. Pass its access_token to the SDK.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Token" + }, + "example": { + "access_token": "MERCHANT_TOKEN", + "token_type": "Bearer", + "expires_in": 899 + } + } + } + }, + "400": { + "description": "The request body is invalid or contains unsupported or duplicate actions.", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + }, + "example": { + "type": "https://developer.sumup.com/problem/bad-request", + "title": "Bad Request", + "status": 400, + "detail": "The request contains an unsupported action." + } + } + } + }, + "401": { + "description": "Authentication is missing, invalid, or expired.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Problem" + }, + "example": { + "type": "https://developer.sumup.com/problem/unauthorized", + "title": "Unauthorized", + "status": 401 + } + }, + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/Problem" + }, + "example": { + "type": "https://developer.sumup.com/problem/unauthorized", + "title": "Unauthorized", + "status": 401, + "detail": "Token is invalid or expired." + } + } + } + }, + "403": { + "description": "The authenticated identity does not have permission to create tokens for this merchant account.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Problem" + }, + "example": { + "type": "https://developer.sumup.com/problem/forbidden", + "title": "Forbidden", + "status": 403 + } + } + } + } + } + } } }, "components": { @@ -6242,7 +6689,7 @@ }, "payment_account_reference": { "type": "string", - "description": "PAR (Payment account reference) if available for the card.", + "description": "Payment Account Reference (PAR) defined by [EMVCo](https://www.emvco.com/emv-technologies/payment-tokenisation/). It links a card's primary account number (PAN) with its affiliated payment tokens, allowing transactions made with the physical card and tokenized versions of that card, such as digital wallets, to be correlated when PAR is available.\n\nThis reference cannot be used to initiate a payment and is separate from the saved payment instrument `token` used to process checkouts. Returned only when available for the card; integrations must handle its absence.", "example": "5665ABCDEFGHIJKLMNOPQRSTUVWXY" } } @@ -6328,7 +6775,7 @@ "properties": { "checkout_reference": { "type": "string", - "maxLength": 90, + "maxLength": 64, "description": "Merchant-defined reference for the checkout. Use it to correlate the SumUp checkout with your own order, cart, subscription, or payment attempt in your systems.", "example": "f00a8f74-b05d-4605-bd73-2a901bae5802" }, @@ -6354,7 +6801,7 @@ "return_url": { "type": "string", "format": "uri", - "description": "Optional backend callback URL used by SumUp to notify your platform about processing updates for the checkout.", + "description": "Optional backend callback URL for checkout status notifications. SumUp sends an HTTP POST with `event_type` and the checkout `id`. Retrieve the checkout to verify its current status before updating your order. See the [webhook guide](https://developer.sumup.com/online-payments/webhooks/) for the payload and response requirements.", "example": "http://example.com" }, "id": { @@ -6385,7 +6832,7 @@ "string", "null" ], - "example": "2020-02-29T10:56:56+00:00", + "example": "2030-12-31T23:59:59Z", "format": "date-time", "description": "Optional expiration timestamp. The checkout must be processed before this moment, otherwise it becomes unusable. If omitted, the checkout does not have an explicit expiry time." }, @@ -6446,7 +6893,7 @@ "checkout_reference": { "type": "string", "maxLength": 64, - "description": "Merchant-defined reference for the new checkout. It should be unique enough for you to identify the payment attempt in your own systems.", + "description": "Merchant-defined reference for the new checkout, up to 64 characters. Use it to correlate the checkout with an order or payment attempt in your own system. If a checkout already exists for the supplied unique parameters, creation returns `409` with `DUPLICATED_CHECKOUT`; see the conflict response.", "example": "f00a8f74-b05d-4605-bd73-2a901bae5802" }, "amount": { @@ -6471,7 +6918,7 @@ "return_url": { "type": "string", "format": "uri", - "description": "Optional backend callback URL used by SumUp to notify your platform about processing updates for the checkout.", + "description": "Optional backend callback URL for checkout status notifications. SumUp sends an HTTP POST with `event_type` and the checkout `id`. Retrieve the checkout to verify its current status before updating your order. See the [webhook guide](https://developer.sumup.com/online-payments/webhooks/) for the payload and response requirements.", "example": "http://example.com/" }, "customer_id": { @@ -6493,7 +6940,7 @@ "string", "null" ], - "example": "2020-02-29T10:56:56+00:00", + "example": "2030-12-31T23:59:59Z", "format": "date-time", "description": "Optional expiration timestamp. The checkout must be processed before this moment, otherwise it becomes unusable. If omitted, the checkout does not have an explicit expiry time." }, @@ -6534,7 +6981,7 @@ }, "checkout_reference": { "type": "string", - "maxLength": 90, + "maxLength": 64, "description": "Updated merchant-defined reference for the checkout.", "example": "f00a8f74-b05d-4605-bd73-2a901bae5802" }, @@ -6543,7 +6990,7 @@ "string", "null" ], - "example": "2020-02-29T10:56:56+00:00", + "example": "2030-12-31T23:59:59Z", "format": "date-time", "description": "Updated expiration timestamp. The checkout must be processed before this moment, otherwise it becomes unusable." }, @@ -6632,7 +7079,7 @@ }, "token": { "type": "string", - "description": "Saved-card token to use instead of raw card details when processing with a previously stored payment instrument.", + "description": "Token of a saved payment instrument returned by checkout processing or the customer's payment-instruments endpoint. To charge a saved card, set `payment_type` to `card` and provide both this `token` and the associated `customer_id` instead of raw card details.", "example": "ba85dfee-c3cf-48a6-84f5-d7d761fbba50" }, "customer_id": { @@ -6752,14 +7199,14 @@ "Customer": { "type": "object", "title": "Customer", - "description": "Saved customer details.", + "description": "Saved payer details identified by the `customer_id` supplied by your integration. A customer can have saved payment instruments for subsequent payments.", "required": [ "customer_id" ], "properties": { "customer_id": { "type": "string", - "description": "Unique identifier of the customer.", + "description": "Identifier you supply when creating the customer. Use an ID from your own system and retain it for subsequent customer, checkout, and saved-payment-instrument requests.", "example": "831ff8d4cd5958ab5670" }, "personal_details": { @@ -6767,6 +7214,33 @@ } } }, + "CustomerValidationError": { + "title": "Customer Validation Error", + "description": "Details of a customer validation error.", + "type": "object", + "required": [ + "instance", + "error_code", + "error_message" + ], + "properties": { + "instance": { + "type": "string", + "description": "Unique identifier of this error occurrence.", + "example": "32a44c6c-85d3-49e8-86bf-a5bba98c4621" + }, + "error_code": { + "type": "string", + "description": "Platform code for the error.", + "example": "INVALID" + }, + "error_message": { + "type": "string", + "description": "Short description of the error.", + "example": "customer_id" + } + } + }, "Error": { "title": "Error", "type": "object", @@ -7092,12 +7566,12 @@ "properties": { "rel": { "type": "string", - "description": "Relation.", + "description": "Pagination relation indicating which page the link retrieves, for example `next`.", "example": "next" }, "href": { "type": "string", - "description": "Location.", + "description": "Query string to use with the transaction history endpoint when requesting the linked page. Preserve the returned pagination references and query parameters.", "example": "limit=10\u0026oldest_ref=090df9bf-93b7-40f1-8181-fbdb236568a1\u0026order=ascending" } }, @@ -7178,7 +7652,7 @@ "properties": { "token": { "type": "string", - "description": "Unique token identifying the saved payment card for a customer.", + "description": "Token identifying the customer's saved payment card. Pass it as `token`, together with the associated `customer_id` and `payment_type = card`, when processing a checkout with this instrument.", "readOnly": true, "example": "bcfc8e5f-3b47-4cb9-854b-3b7a4cce7be3" }, @@ -7266,7 +7740,7 @@ }, "birth_date": { "type": "string", - "description": "Date of birth of the customer.", + "description": "Date of birth of the customer in `YYYY-MM-DD` format, without a time or timezone.", "format": "date", "example": "1993-12-31" }, @@ -7305,7 +7779,7 @@ "vat_rate": { "type": "number", "format": "decimal", - "description": "VAT rate applied to the product price.", + "description": "VAT rate as a decimal fraction, for example `0.19` for 19%.", "example": 0.19 }, "single_vat_amount": { @@ -7348,7 +7822,7 @@ "Receipt": { "type": "object", "title": "Receipt", - "description": "Receipt details for a transaction.", + "description": "Structured receipt details for a transaction. The transaction's `amount`, `vat_amount`, and `tip_amount`, as well as event amounts, are returned as decimal strings in major currency units, for example `\"10.10\"` for EUR 10.10.", "properties": { "transaction_data": { "$ref": "#/components/schemas/ReceiptTransaction" @@ -7797,7 +8271,7 @@ "amount": { "type": "number", "format": "decimal", - "description": "Amount of the event.", + "description": "Amount of the event in major units of the associated transaction's currency.", "example": 58.8 }, "due_date": { @@ -7814,7 +8288,7 @@ }, "installment_number": { "type": "integer", - "description": "Consecutive number of the installment that is paid. Applicable only payout events, i.e. `event_type = PAYOUT`.", + "description": "Consecutive number of the installment that is paid. Applicable only to payout events, i.e. `event_type = PAYOUT`.", "example": 1 }, "timestamp": { @@ -7837,13 +8311,13 @@ }, "transaction_code": { "type": "string", - "description": "Transaction code returned by the acquirer/processing entity after processing the transaction.", + "description": "SumUp transaction code, for example `TEENSK4W2K`. Use it to look up the transaction with the `transaction_code` query parameter. This is separate from the transaction's `id` and the card issuer's `auth_code`.", "example": "TEENSK4W2K" }, "amount": { "type": "number", "format": "float", - "description": "Total amount of the transaction.", + "description": "Total amount of the transaction in major units of `currency`, for example `10.1` for EUR 10.10.", "example": 10.1 }, "currency": { @@ -7882,13 +8356,13 @@ "vat_amount": { "type": "number", "format": "float", - "description": "Amount of the applicable VAT (out of the total transaction amount).", + "description": "VAT included in the total transaction amount, in major units of the transaction's currency.", "example": 6 }, "tip_amount": { "type": "number", "format": "float", - "description": "Amount of the tip (out of the total transaction amount).", + "description": "Tip included in the total transaction amount, in major units of the transaction's currency.", "example": 3 }, "entry_mode": { @@ -7991,7 +8465,7 @@ "refunded_amount": { "type": "number", "format": "decimal", - "description": "Total refunded amount.", + "description": "Total amount refunded for this transaction, in major units of the transaction's currency.", "example": 0 } } @@ -8001,7 +8475,7 @@ "PaymentType": { "title": "Payment Type", "type": "string", - "description": "Payment type used for the transaction.", + "description": "Payment category recorded on a transaction, for example `POS` for a point-of-sale card payment, `ECOM` for an online card payment, or `RECURRING` for a recurring card payment. These reporting values are separate from the lowercase `payment_type` values used to process checkouts.", "enum": [ "CASH", "POS", @@ -8020,7 +8494,7 @@ "EntryMode": { "title": "Entry Mode", "type": "string", - "description": "Entry mode of the payment details.", + "description": "How the payment details were captured, for example `CHIP` or `CONTACTLESS` for card-present payments and `CUSTOMER_ENTRY` for card details entered by the payer. For wallet and alternative payment methods, this can identify the method, such as `APPLE_PAY` or `BLIK`.", "enum": [ "BOLETO", "SOFORT", @@ -8059,6 +8533,7 @@ "enum": [ "ALELO", "AMEX", + "BANCOMAT", "CONECS", "CUP", "DINERS", @@ -8073,6 +8548,7 @@ "MAESTRO", "MASTERCARD", "PLUXEE", + "SUMUP", "SWILE", "TICKET", "VISA", @@ -8119,7 +8595,7 @@ "fee_amount": { "type": "number", "format": "decimal", - "description": "Transaction SumUp total fee amount.", + "description": "Total SumUp transaction fee in major units of the transaction's currency.", "example": 8 }, "lat": { @@ -8351,7 +8827,7 @@ "TransactionEventType": { "title": "Transaction Event Type", "type": "string", - "description": "Type of the transaction event.", + "description": "Financial event associated with a transaction.\n\n- `PAYOUT`: Funds from the transaction being prepared for or included in a merchant payout. Check the event status to determine whether they have been paid out.\n- `REFUND`: Money returned to the payer.\n- `CHARGE_BACK`: A reversal of the payment following a chargeback.\n- `PAYOUT_DEDUCTION`: An amount deducted from a merchant payout, for example to cover a refund or chargeback.", "enum": [ "PAYOUT", "CHARGE_BACK", @@ -8379,7 +8855,7 @@ "title": "Transaction Event ID", "type": "integer", "format": "int64", - "description": "Unique identifier of the transaction event.", + "description": "Numeric identifier of a transaction event. Use it as `tx_event_id` when requesting receipt details for a specific event. This is separate from the transaction ID and the transaction history pagination references.", "example": 9567461191 }, "HorizontalAccuracy": { @@ -8394,7 +8870,7 @@ "type": "number", "format": "float", "description": "Latitude value from the coordinates of the payment location (as received from the payment terminal reader).", - "minimum": 0, + "minimum": -90, "maximum": 90, "example": 52.520008 }, @@ -8403,7 +8879,7 @@ "type": "number", "format": "float", "description": "Longitude value from the coordinates of the payment location (as received from the payment terminal reader).", - "minimum": 0, + "minimum": -180, "maximum": 180, "example": 13.404954 }, @@ -8459,7 +8935,15 @@ "ReaderPaymentRequestParams": { "properties": { "affiliate": { - "$ref": "#/components/schemas/Affiliate" + "allOf": [ + { + "description": "Optional caller-supplied context about the integration initiating the payment.", + "type": "object" + }, + { + "$ref": "#/components/schemas/Affiliate" + } + ] }, "client_transaction_id": { "description": "Caller-supplied correlation identifier, used as the idempotency key.", @@ -8467,12 +8951,23 @@ "type": "string" }, "tip_amount": { - "description": "Optional tip amount in minor units, added on top of total_amount.", - "example": 100, + "description": "Portion of total_amount that is a tip, in minor units.", + "example": 200, "type": "integer" }, "total_amount": { - "$ref": "#/components/schemas/Amount" + "allOf": [ + { + "description": "Amount charged on the reader, including any tip. The amount is represented as an integer value altogether with the currency and the minor unit. For example, MXN 10.00 is represented as value 1000 with minor unit of 2.", + "example": { + "currency": "MXN", + "value": 1000 + } + }, + { + "$ref": "#/components/schemas/Amount" + } + ] } }, "required": [ @@ -8975,7 +9470,15 @@ "example": "10999" }, "country": { - "$ref": "#/components/schemas/CountryCode" + "allOf": [ + { + "description": "The ISO3166-1 Alpha-2 code of the address country.\n", + "example": "DE" + }, + { + "$ref": "#/components/schemas/CountryCode" + } + ] }, "city": { "type": "string", @@ -9504,7 +10007,8 @@ "maxLength": 60 }, "phone_number": { - "$ref": "#/components/schemas/PhoneNumber" + "$ref": "#/components/schemas/PhoneNumber", + "description": "The (mobile) phone number of the individual (used for verification) in [E.164](https://en.wikipedia.org/wiki/E.164) format.\n" }, "relationships": { "type": "array", @@ -9522,16 +10026,19 @@ } }, "ownership": { - "$ref": "#/components/schemas/Ownership" + "$ref": "#/components/schemas/Ownership", + "description": "Details about the ownership relationship between the Person and the Merchant. This is only set if the Person has a relationship of type `owner`.\n" }, "address": { - "$ref": "#/components/schemas/Address" + "$ref": "#/components/schemas/Address", + "description": "The address of the individual." }, "identifiers": { "$ref": "#/components/schemas/PersonalIdentifiers" }, "citizenship": { - "$ref": "#/components/schemas/CountryCode" + "$ref": "#/components/schemas/CountryCode", + "description": "The Alpha-2 ISO code of the country where the Person is a citizen.\n" }, "nationality": { "type": [ @@ -9579,19 +10086,23 @@ "pattern": "^[0-9]{4}$" }, "legal_type": { - "$ref": "#/components/schemas/LegalType" + "$ref": "#/components/schemas/LegalType", + "description": "The category identifying the legal structure of the company or legal entity.\n" }, "address": { - "$ref": "#/components/schemas/Address" + "$ref": "#/components/schemas/Address", + "description": "The company's primary address." }, "trading_address": { - "$ref": "#/components/schemas/Address" + "$ref": "#/components/schemas/Address", + "description": "A trading address is where your suppliers, banks or customers send you correspondence to. Trading address can be different to the company's registered address (`address`).\n" }, "identifiers": { "$ref": "#/components/schemas/CompanyIdentifiers" }, "phone_number": { - "$ref": "#/components/schemas/PhoneNumber" + "$ref": "#/components/schemas/PhoneNumber", + "description": "The company's phone number (used for verification) in [E.164](https://en.wikipedia.org/wiki/E.164) format.\n" }, "website": { "description": "HTTP(S) URL of the company's website.\n", @@ -9745,6 +10256,32 @@ "minLength": 8, "maxLength": 9 }, + "UpdateReaderSettingsError": { + "description": "Details of a failed reader settings request.", + "properties": { + "errors": { + "properties": { + "detail": { + "description": "Error message", + "type": "string" + }, + "type": { + "description": "Error code", + "type": "string" + } + }, + "required": [ + "detail" + ], + "type": "object" + } + }, + "required": [ + "errors" + ], + "title": "UpdateReaderSettingsError", + "type": "object" + }, "CreateReaderCheckoutUnprocessableEntity": { "description": "Unprocessable entity", "example": { @@ -9762,7 +10299,7 @@ "must be a valid URL" ], "tip_rates": [ - "must be between 0.01 and 0.99" + "has an invalid entry" ], "total_amount": [ "must be greater than 0" @@ -9780,6 +10317,15 @@ } } }, + "PaymentMethodNotAllowed": { + "description": "Error returned when the requested payment method is not supported for the merchant (e.g. pix for non-BR merchants).", + "value": { + "errors": { + "detail": "Payment method not allowed for this merchant.", + "type": "PAYMENT_METHOD_NOT_ALLOWED" + } + } + }, "ReaderBusy": { "description": "Error returned when a checkout was requested in the last 60 seconds.", "value": { @@ -9845,6 +10391,7 @@ "client_transaction_id": "00e33a36-c99b-4cb2-b635-b90c1455c9c8", "created_at": "2026-07-07T20:41:16.315434Z", "installments": 1, + "payment_failure_reason": null, "payment_status": "pending", "payment_type": "card", "reader_firmware_version": "3.3.3.21", @@ -9863,10 +10410,11 @@ "data": { "properties": { "card_type": { - "description": "Type of the card. Required for some countries", + "description": "Type of the card, or null when no card type was specified.", "enum": [ "credit", - "debit" + "debit", + null ], "type": [ "string", @@ -9895,14 +10443,14 @@ ] }, "payment_failure_reason": { - "description": "Payment failure reason", + "description": "Reason for the payment failure, or null when no failure reason is available.", "type": [ "string", "null" ] }, "payment_status": { - "description": "Payment status from payments v2 event", + "description": "Status of the payment, or null when no payment status is available.", "type": [ "string", "null" @@ -9991,6 +10539,7 @@ "reader_firmware_version", "installments", "payment_status", + "payment_failure_reason", "valid_until", "created_at", "updated_at", @@ -10006,6 +10555,70 @@ "title": "GetReaderCheckoutResponse", "type": "object" }, + "GetReaderSettingsResponse": { + "description": "Menu lock settings reported by the reader, or the last saved settings when offline.\n", + "example": { + "data": { + "menu_lock": { + "enabled": true, + "passcode": "1234" + } + } + }, + "properties": { + "data": { + "properties": { + "menu_lock": { + "description": "Menu lock configuration. Fields may be absent if no value is available.", + "properties": { + "enabled": { + "description": "Whether menu lock is enabled.", + "example": true, + "type": "boolean" + }, + "passcode": { + "description": "Four-digit PIN used to unlock the reader's menu.", + "example": "1234", + "format": "password", + "pattern": "^[0-9]{4}$", + "type": "string" + } + }, + "type": "object" + } + }, + "required": [ + "menu_lock" + ], + "type": "object" + } + }, + "required": [ + "data" + ], + "title": "GetReaderSettingsResponse", + "type": "object" + }, + "UpdateReaderSettingsUnprocessableEntity": { + "description": "The reader cannot complete the settings request, or the settings are invalid.", + "example": { + "errors": { + "detail": "Reader settings are only supported for Solo readers.", + "type": "UNSUPPORTED_DEVICE_MODEL" + } + }, + "properties": { + "errors": { + "additionalProperties": true, + "type": "object" + } + }, + "required": [ + "errors" + ], + "title": "UpdateReaderSettingsUnprocessableEntity", + "type": "object" + }, "StatusResponse": { "description": "Status of a device", "example": { @@ -10164,6 +10777,8 @@ "type": { "description": "Key indicating type of error", "enum": [ + "INVALID_AFFILIATE_KEY", + "APP_OUTDATED", "INVALID_BEARER_TOKEN", "INVALID_USER_AGENT", "NOT_ENOUGH_UNPAID_PAYOUTS", @@ -10200,7 +10815,7 @@ "type": "string" }, "client_transaction_id": { - "description": "The client transaction ID is a unique identifier for the transaction that is generated for the client.\n\nIt can be used later to fetch the transaction details via the [Transactions API](https://developer.sumup.com/api/transactions/get).\n", + "description": "Unique identifier for the transaction associated with this checkout.\nUse this value to match the checkout with payment events and transaction records.\n", "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "type": "string" } @@ -10217,6 +10832,95 @@ "title": "CreateReaderCheckoutResponse", "type": "object" }, + "UpdateReaderSettingsRequest": { + "description": "Menu lock settings to update for a Solo reader.", + "example": { + "menu_lock": { + "enabled": true, + "passcode": "1234" + } + }, + "properties": { + "menu_lock": { + "description": "Menu lock configuration. Provide at least one of `enabled` or `passcode`.", + "minProperties": 1, + "properties": { + "enabled": { + "description": "Whether menu lock is enabled.", + "example": true, + "type": "boolean" + }, + "passcode": { + "description": "Four-digit PIN used to unlock the reader's menu.", + "example": "1234", + "format": "password", + "pattern": "^[0-9]{4}$", + "type": "string" + } + }, + "type": "object" + } + }, + "required": [ + "menu_lock" + ], + "title": "UpdateReaderSettingsRequest", + "type": "object" + }, + "UpdateReaderSettingsResponse": { + "description": "Menu lock settings applied to the reader.", + "example": { + "data": { + "menu_lock": { + "enabled": true, + "passcode": "1234" + }, + "status": "applied" + } + }, + "properties": { + "data": { + "properties": { + "menu_lock": { + "description": "Settings included in the update. Omitted fields are not returned.", + "properties": { + "enabled": { + "description": "Whether menu lock is enabled.", + "example": true, + "type": "boolean" + }, + "passcode": { + "description": "Four-digit PIN used to unlock the reader's menu.", + "example": "1234", + "format": "password", + "pattern": "^[0-9]{4}$", + "type": "string" + } + }, + "type": "object" + }, + "status": { + "description": "Indicates that the reader applied the settings update.", + "enum": [ + "applied" + ], + "example": "applied", + "type": "string" + } + }, + "required": [ + "status", + "menu_lock" + ], + "type": "object" + } + }, + "required": [ + "data" + ], + "title": "UpdateReaderSettingsResponse", + "type": "object" + }, "Unauthorized": { "description": "401 Unauthorized", "example": { @@ -10253,7 +10957,7 @@ "type": "object" }, "CreateReaderCheckoutRequest": { - "description": "Reader Checkout", + "description": "Payment details for a Reader Checkout.\n", "example": { "aade": { "provider_id": "123", @@ -10264,7 +10968,10 @@ "app_id": "com.example.app", "foreign_transaction_id": "123456", "key": "ef7b684a-d6f4-4e93-9b1b-6acdd6564a8e", - "tags": {} + "tags": { + "custom_key_1": "custom_value_1", + "custom_key_2": "custom_value_2" + } }, "card_type": "debit", "description": "This is a description...", @@ -10310,26 +11017,26 @@ "type": "object" }, "affiliate": { - "description": "Affiliate metadata for the transaction.\nIt is a field that allow for integrators to track the source of the transaction.\n", + "description": "Optional affiliate metadata associated with the checkout's payment.\nProvide both `app_id` and `key` when including this object.\n", "properties": { "app_id": { - "description": "Application ID of the affiliate.\nIt is a unique identifier for the application and should be set by the integrator in the [Affiliate Keys](https://developer.sumup.com/affiliate-keys) page.\n", + "description": "Integrator application identifier from the [Affiliate Keys](https://developer.sumup.com/affiliate-keys) page.\nRequired when the affiliate object is provided.\n", "example": "com.example.app", "type": "string" }, "foreign_transaction_id": { - "description": "Foreign transaction ID of the affiliate.\nIt is a unique identifier for the transaction.\nIt can be used later to fetch the transaction details via the [Transactions API](https://developer.sumup.com/api/transactions/get).\n", + "description": "Integrator-owned transaction reference. Optional.\nCan be used to retrieve the transaction via the [Transactions API](https://developer.sumup.com/api/transactions/get).\n", "example": "19e12390-72cf-4f9f-80b5-b0c8a67fa43f", "type": "string" }, "key": { - "description": "Key of the affiliate.\nIt is a unique identifier for the key and should be generated by the integrator in the [Affiliate Keys](https://developer.sumup.com/affiliate-keys) page.\n", + "description": "Affiliate key generated in the [Affiliate Keys](https://developer.sumup.com/affiliate-keys) page.\nRequired when the affiliate object is provided.\n", "example": "123e4567-e89b-12d3-a456-426614174000", "type": "string" }, "tags": { "additionalProperties": true, - "description": "Additional metadata for the transaction.\nIt is key-value object that can be associated with the transaction.\n", + "description": "Optional key-value metadata associated with the transaction.\n", "example": { "custom_key_1": "custom_value_1", "custom_key_2": "custom_value_2" @@ -10339,8 +11046,7 @@ }, "required": [ "app_id", - "key", - "foreign_transaction_id" + "key" ], "title": "Affiliate", "type": [ @@ -10349,7 +11055,7 @@ ] }, "card_type": { - "description": "The card type of the card used for the transaction.\nIs is required only for some countries (e.g: Brazil).\n", + "description": "The card type of the card used for the transaction.\nIt is required only for some countries (e.g: Brazil).\n", "enum": [ "credit", "debit" @@ -10362,7 +11068,7 @@ "type": "string" }, "installments": { - "description": "Number of installments for the transaction.\nIt may vary according to the merchant country.\nFor example, in Brazil, the maximum number of installments is 12.\n\nOmit if the merchant country does support installments.\nOtherwise, the checkout will be rejected.\n", + "description": "Number of installments for the transaction.\nIt may vary according to the merchant country.\nFor example, in Brazil, the maximum number of installments is 12.\n\nOmit if the merchant country does not support installments.\nOtherwise, the checkout will be rejected.\n", "example": 1, "minimum": 1, "type": [ @@ -10370,6 +11076,16 @@ "null" ] }, + "payment_type": { + "default": "card", + "description": "The payment method type for the transaction. Defaults to `card`.\n\nUse `pix` to initiate a Pix QR-code payment flow on the device.\nPix is only supported for Brazilian merchants; other merchants will receive a 422 error.\n", + "enum": [ + "card", + "pix" + ], + "example": "card", + "type": "string" + }, "return_url": { "description": "Webhook URL to which the payment result will be sent.\nIt must be a HTTPS url.\n", "example": "https://www.example.com", @@ -10377,13 +11093,18 @@ "type": "string" }, "tip_rates": { - "description": "List of tipping rates to be displayed to the cardholder.\nThe rates are in percentage and should be between 0.01 and 0.99.\nThe list should be sorted in ascending order.\n", + "description": "Exactly three distinct tipping rates to be displayed to the cardholder.\nEach rate is a fraction between 0.01 and 1.00 in increments of 0.01.\nThe rates are sorted in ascending order by the server.\n", "items": { "format": "float", + "maximum": 1, + "minimum": 0.01, "multipleOf": 0.01, "type": "number" }, - "type": "array" + "maxItems": 3, + "minItems": 3, + "type": "array", + "uniqueItems": true }, "tip_timeout": { "default": 30, @@ -10528,6 +11249,36 @@ ], "title": "NotFound", "type": "object" + }, + "Token": { + "type": "object", + "description": "A short-lived credential for the requested merchant account and actions. Pass access_token to a SumUp SDK that supports merchant tokens.", + "required": [ + "access_token", + "token_type", + "expires_in" + ], + "properties": { + "access_token": { + "type": "string", + "description": "Secret token to pass to the SDK. Treat this value as an opaque string; do not parse it or depend on its format.", + "example": "MERCHANT_TOKEN" + }, + "token_type": { + "type": "string", + "description": "Authentication scheme used by the token. Always Bearer.", + "enum": [ + "Bearer" + ], + "example": "Bearer" + }, + "expires_in": { + "type": "integer", + "description": "Remaining lifetime of the token in seconds when the response is created. Tokens expire 15 minutes after creation. Request a new token when needed.", + "minimum": 0, + "example": 899 + } + } } }, "requestBodies": { @@ -10548,7 +11299,7 @@ "currency": "EUR", "merchant_code": "MH4H92C7", "description": "Purchase", - "valid_until": "2020-02-29T10:56:56+00:00", + "valid_until": "2030-12-31T23:59:59Z", "redirect_url": "https://sumup.com" } }, @@ -10606,7 +11357,7 @@ "currency": "EUR", "description": "Updated purchase", "checkout_reference": "f00a8f74-b05d-4605-bd73-2a901bae5802", - "valid_until": "2020-02-29T10:56:56+00:00", + "valid_until": "2030-12-31T23:59:59Z", "customer_id": "831ff8d4cd5958ab5670" } } @@ -10653,7 +11404,7 @@ "amount": { "type": "number", "format": "float", - "description": "Amount to be refunded. Eligible amount can't exceed the amount of the transaction and varies based on country and currency. If you do not specify a value, the system performs a full refund of the transaction.", + "description": "Amount to refund in major units of the transaction's currency, for example `5` for EUR 5.00. It must be greater than zero and cannot exceed the amount eligible for a refund. Eligibility depends on the transaction and country/currency rules. If omitted, the system requests a full refund.", "example": 5 } } @@ -10684,7 +11435,7 @@ "id": "88fcf8de-304d-4820-8f1c-ec880290eb92", "status": "PENDING", "date": "2020-02-29T10:56:56+00:00", - "valid_until": "2020-02-29T10:56:56+00:00", + "valid_until": "2030-12-31T23:59:59Z", "customer_id": "831ff8d4cd5958ab5670", "mandate": { "type": "recurrent", @@ -10721,7 +11472,7 @@ "id": "88fcf8de-304d-4820-8f1c-ec880290eb92", "status": "PENDING", "date": "2020-02-29T10:56:56+00:00", - "valid_until": "2020-02-29T10:56:56+00:00", + "valid_until": "2030-12-31T23:59:59Z", "customer_id": "831ff8d4cd5958ab5670", "redirect_url": "https://mysite.com/completed_purchase", "transactions": [ @@ -10820,7 +11571,7 @@ "id": "88fcf8de-304d-4820-8f1c-ec880290eb92", "status": "PENDING", "date": "2020-02-29T10:56:56+00:00", - "valid_until": "2020-02-29T10:56:56+00:00", + "valid_until": "2030-12-31T23:59:59Z", "customer_id": "831ff8d4cd5958ab5670", "transactions": [] } @@ -11140,7 +11891,7 @@ "CheckoutReference": { "name": "checkout_reference", "in": "query", - "description": "Filters the list of checkout resources by the unique reference of the checkout.", + "description": "Filters checkouts by the merchant-defined `checkout_reference` supplied when creating the checkout. This is separate from the SumUp-generated checkout `id`.", "required": false, "schema": { "type": "string", @@ -11151,7 +11902,7 @@ "name": "checkout_id", "in": "path", "required": true, - "description": "Unique identifier of the checkout resource.", + "description": "SumUp-generated `id` returned when creating a checkout. Use this value to retrieve, update, process, or deactivate the checkout.", "schema": { "type": "string", "example": "4e425463-3e1b-431d-83fa-1e51c2925e99" @@ -11161,7 +11912,7 @@ "name": "customer_id", "in": "path", "required": true, - "description": "Unique identifier of the saved customer resource.", + "description": "The `customer_id` you supplied when creating the customer.", "schema": { "type": "string", "example": "831ff8d4cd5958ab5670" @@ -11190,7 +11941,7 @@ "OrderFilter": { "name": "order", "in": "query", - "description": "Specifies the order in which the returned results are displayed.", + "description": "Sort direction for the transaction history. Use `ascending` or `descending`; the default is `ascending`.", "schema": { "type": "string", "enum": [ @@ -11203,7 +11954,7 @@ "LimitFilter": { "name": "limit", "in": "query", - "description": "Specifies the maximum number of results per page. Value must be a positive integer and if not specified, will return 10 results.", + "description": "Maximum number of transactions per page. Must be a positive integer. Defaults to `10` when omitted; a page can contain fewer results.", "schema": { "type": "integer", "example": 10 @@ -11212,7 +11963,7 @@ "UsersFilter": { "name": "users[]", "in": "query", - "description": "Filters the returned results by user email.", + "description": "Filters transactions by user email. For multiple values, repeat the query parameter, for example `users[]=first@example.com\u0026users[]=second@example.com`.", "required": false, "example": [ "merchant@example.com" @@ -11231,7 +11982,7 @@ "StatusesFilter": { "name": "statuses[]", "in": "query", - "description": "Filters the returned results by the specified list of final statuses of the transactions.", + "description": "Filters transactions by the listed final statuses. For multiple values, repeat the query parameter, for example `statuses[]=SUCCESSFUL\u0026statuses[]=REFUNDED`.", "required": false, "schema": { "type": "array", @@ -11329,7 +12080,7 @@ "NewestRefFilter": { "name": "newest_ref", "in": "query", - "description": "Filters the results by the reference ID of transaction events and returns only transactions with events whose IDs are *smaller* than the specified value. This parameters supersedes the `newest_time` parameter (if both are provided in the request).", + "description": "Pagination reference that returns results before the specified reference. Use the value from a returned pagination link rather than constructing it yourself. This parameter takes precedence over `newest_time` when both are provided.", "required": false, "schema": { "type": "string", @@ -11349,13 +12100,13 @@ }, "securitySchemes": { "apiKey": { - "description": "API keys allow you easily interact with SumUp APIs. API keys are static tokens. You can create API keys from the [Dashboard](https://me.sumup.com/settings/api-keys)", + "description": "Authenticate requests with an API key created in the [Dashboard](https://me.sumup.com/settings/api-keys). Send the key in the HTTP `Authorization` header as `Bearer YOUR_API_KEY`. See the [API key guide](https://developer.sumup.com/tools/authorization/api-keys/).", "type": "http", "scheme": "Bearer" }, "oauth2": { "type": "oauth2", - "description": "SumUp supports [OAuth 2.0](https://tools.ietf.org/html/rfc6749) authentication for platforms that want to offer their services to SumUp users.\n\nTo integrate via OAuth 2.0 you will need a client credentials that you can create in the [SumUp Dashboard](https://me.sumup.com/settings/oauth2-applications).\n\nTo maintain security of our users, we highly recommend that you use one of the [recommended OAuth 2.0 libraries](https://oauth.net/code/) for authentication.", + "description": "SumUp supports [OAuth 2.0](https://tools.ietf.org/html/rfc6749) authentication for platforms that want to offer their services to SumUp users.\n\nRegister an application in the [SumUp Dashboard](https://me.sumup.com/settings/oauth2-applications) to obtain a client ID and client secret. Use the authorization code flow to request a merchant's consent, then send the issued access token in the HTTP `Authorization` header as `Bearer ACCESS_TOKEN`. Request the scopes needed for the endpoints your integration calls.\n\nTo maintain security of our users, we highly recommend that you use one of the [recommended OAuth 2.0 libraries](https://oauth.net/code/) for authentication.", "flows": { "authorizationCode": { "authorizationUrl": "https://api.sumup.com/authorize", @@ -11379,27 +12130,6 @@ "payouts.read": "View payouts.", "user.subaccounts": "View and manage the user profile details of your employees." } - }, - "clientCredentials": { - "tokenUrl": "https://api.sumup.com/token", - "scopes": { - "payments": "Make payments by creating and processing checkouts.", - "checkouts.read": "View checkouts.", - "checkouts.write": "Create, process, and deactivate checkouts.", - "transactions.history": "View transactions and transaction history.", - "transactions.read": "View transactions and transaction history.", - "refunds.write": "Refund transactions.", - "receipts.read": "View receipts.", - "user.profile_readonly": "View user profile details.", - "user.profile": "View and manage your user profile.", - "user.app-settings": "View and manage the SumUp mobile application settings.", - "payment_instruments": "Manage customers and their payment instruments.", - "customers.read": "View customers and their payment instruments.", - "customers.write": "Create and manage customers and their payment instruments.", - "user.payout-settings": "View and manage your payout settings.", - "payouts.read": "View payouts.", - "user.subaccounts": "View and manage the user profile details of your employee." - } } } } @@ -11438,6 +12168,221 @@ } }, "webhooks": { + "checkouts.created": { + "post": { + "operationId": "CheckoutCreatedWebhook", + "tags": [ + "Checkouts" + ], + "summary": "Checkout created", + "description": "Sent once when a checkout is first observed in the checkout snapshot stream, even if it is already paid, failed, or expired. The event timestamp is the original checkout creation time.", + "x-object": { + "$ref": "#/components/schemas/Checkout" + }, + "x-object-type": "checkout", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + }, + "examples": { + "created": { + "value": { + "id": "event_01ARZ3NDEKTSV4RRFFQ69G5FAV", + "type": "checkouts.created", + "created_at": "2026-10-15T12:00:00Z", + "object": { + "id": "e552de3b-1777-4c91-bdb8-756967678572", + "type": "checkout", + "url": "https://api.sumup.com/v0.1/checkouts/e552de3b-1777-4c91-bdb8-756967678572" + } + } + } + } + } + } + }, + "responses": { + "2XX": { + "description": "Return any 2xx response to acknowledge successful delivery." + } + } + } + }, + "checkouts.failed": { + "post": { + "operationId": "CheckoutFailedWebhook", + "tags": [ + "Checkouts" + ], + "summary": "Checkout failed", + "description": "Sent once when a checkout is first observed with status FAILED. A failed checkout can subsequently be retried and paid. Later FAILED snapshots do not emit another failed event.", + "x-object": { + "$ref": "#/components/schemas/Checkout" + }, + "x-object-type": "checkout", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + }, + "examples": { + "failed": { + "value": { + "id": "event_01ARZ3NDEKTSV4RRFFQ69G5FAV", + "type": "checkouts.failed", + "created_at": "2026-10-15T12:00:00Z", + "object": { + "id": "e552de3b-1777-4c91-bdb8-756967678572", + "type": "checkout", + "url": "https://api.sumup.com/v0.1/checkouts/e552de3b-1777-4c91-bdb8-756967678572" + } + } + } + } + } + } + }, + "responses": { + "2XX": { + "description": "Return any 2xx response to acknowledge successful delivery." + } + } + } + }, + "checkouts.expired": { + "post": { + "operationId": "CheckoutExpiredWebhook", + "tags": [ + "Checkouts" + ], + "summary": "Checkout expired", + "description": "Sent once when a checkout is first observed with status EXPIRED. Expiry or deactivation without an upstream snapshot cannot produce this event. Later EXPIRED snapshots do not emit another expired event.", + "x-object": { + "$ref": "#/components/schemas/Checkout" + }, + "x-object-type": "checkout", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + }, + "examples": { + "expired": { + "value": { + "id": "event_01ARZ3NDEKTSV4RRFFQ69G5FAV", + "type": "checkouts.expired", + "created_at": "2026-10-15T12:00:00Z", + "object": { + "id": "e552de3b-1777-4c91-bdb8-756967678572", + "type": "checkout", + "url": "https://api.sumup.com/v0.1/checkouts/e552de3b-1777-4c91-bdb8-756967678572" + } + } + } + } + } + } + }, + "responses": { + "2XX": { + "description": "Return any 2xx response to acknowledge successful delivery." + } + } + } + }, + "checkouts.updated": { + "post": { + "operationId": "CheckoutUpdatedWebhook", + "tags": [ + "Checkouts" + ], + "summary": "Checkout updated", + "description": "Sent when a checkout snapshot is published, including creation and payment status updates. Fetch the checkout to inspect its current status. This is not a guarantee that every checkout mutation produces a notification.", + "x-object": { + "$ref": "#/components/schemas/Checkout" + }, + "x-object-type": "checkout", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + }, + "examples": { + "updated": { + "value": { + "id": "event_01ARZ3NDEKTSV4RRFFQ69G5FAV", + "type": "checkouts.updated", + "created_at": "2026-10-15T12:00:00Z", + "object": { + "id": "e552de3b-1777-4c91-bdb8-756967678572", + "type": "checkout", + "url": "https://api.sumup.com/v0.1/checkouts/e552de3b-1777-4c91-bdb8-756967678572" + } + } + } + } + } + } + }, + "responses": { + "2XX": { + "description": "Return any 2xx response to acknowledge successful delivery." + } + } + } + }, + "checkouts.paid": { + "post": { + "operationId": "CheckoutPaidWebhook", + "tags": [ + "Checkouts" + ], + "summary": "Checkout paid", + "description": "Sent once when a checkout is first observed with status PAID. Later PAID snapshots do not emit another paid event. The timestamp is the snapshot update time, not necessarily the payment completion time.", + "x-object": { + "$ref": "#/components/schemas/Checkout" + }, + "x-object-type": "checkout", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + }, + "examples": { + "paid": { + "value": { + "id": "event_01ARZ3NDEKTSV4RRFFQ69G5FAV", + "type": "checkouts.paid", + "created_at": "2026-10-15T12:00:00Z", + "object": { + "id": "e552de3b-1777-4c91-bdb8-756967678572", + "type": "checkout", + "url": "https://api.sumup.com/v0.1/checkouts/e552de3b-1777-4c91-bdb8-756967678572" + } + } + } + } + } + } + }, + "responses": { + "2XX": { + "description": "Return any 2xx response to acknowledge successful delivery." + } + } + } + }, "readers.created": { "post": { "operationId": "ReaderCreatedWebhook", @@ -11657,6 +12602,138 @@ } } } + }, + "roles.created": { + "post": { + "operationId": "RoleCreatedWebhook", + "tags": [ + "Roles" + ], + "summary": "Role created", + "description": "Sent when a role is created for a merchant account.", + "x-object": { + "$ref": "#/components/schemas/Role" + }, + "x-object-type": "role", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + }, + "examples": { + "created": { + "summary": "A role created webhook event.", + "value": { + "id": "evt_role_123", + "type": "roles.created", + "created_at": "2026-05-14T08:30:00Z", + "object": { + "id": "rol_123", + "type": "role", + "url": "https://api.sumup.com/v0.1/merchants/MC0DE/roles/rol_123" + } + } + } + } + } + } + }, + "responses": { + "2XX": { + "description": "Return any 2xx response to acknowledge successful delivery." + } + } + } + }, + "roles.updated": { + "post": { + "operationId": "RoleUpdatedWebhook", + "tags": [ + "Roles" + ], + "summary": "Role updated", + "description": "Sent when a role is updated for a merchant account.", + "x-object": { + "$ref": "#/components/schemas/Role" + }, + "x-object-type": "role", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + }, + "examples": { + "updated": { + "summary": "A role updated webhook event.", + "value": { + "id": "evt_role_123", + "type": "roles.updated", + "created_at": "2026-05-14T08:30:00Z", + "object": { + "id": "rol_123", + "type": "role", + "url": "https://api.sumup.com/v0.1/merchants/MC0DE/roles/rol_123" + } + } + } + } + } + } + }, + "responses": { + "2XX": { + "description": "Return any 2xx response to acknowledge successful delivery." + } + } + } + }, + "roles.deleted": { + "post": { + "operationId": "RoleDeletedWebhook", + "tags": [ + "Roles" + ], + "summary": "Role deleted", + "description": "Sent when a role is deleted for a merchant account.", + "x-object": { + "$ref": "#/components/schemas/Role" + }, + "x-object-type": "role", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Event" + }, + "examples": { + "deleted": { + "summary": "A role deleted webhook event.", + "value": { + "id": "evt_role_123", + "type": "roles.deleted", + "created_at": "2026-05-14T08:30:00Z", + "object": { + "id": "rol_123", + "type": "role", + "url": "https://api.sumup.com/v0.1/merchants/MC0DE/roles/rol_123" + } + } + } + } + } + } + }, + "responses": { + "2XX": { + "description": "Return any 2xx response to acknowledge successful delivery." + } + } + } } } } \ No newline at end of file