View as Markdown

llms.txt

This guide isn't available right now

This instance couldn't load its guide catalog. The guide returns as soon as the catalog is readable again.

Back to the guides

No such guide

This instance publishes no guide under that address. It may have been renamed, or it may belong to a feature this installation hasn't enabled.

Back to the guides

That guide is part of the product documentation

This guide is written for someone operating WinkPG through its screens rather than integrating against it, so it lives in the application's own help section instead of here. Sign in to WinkPG and open Help to read it.

Back to the guides

Guides Integration

Getting started with the API

Find the API reference, create an API key, authenticate your first call, and handle idempotency, rate limits, and timestamps correctly.

Everything you can do in the WinkPG portal, you can do over the API: take a payment, save a card, raise an invoice, look up a customer, pull a report. This guide gets you from a portal login to an authenticated call, shows you where the reference lives, and covers the three things every integration needs to get right afterward: idempotency, rate limits, and timestamps.

You only need two things to start: an account in the portal, and a few minutes to create an API key.

Where the reference lives

The reference is inside the product, at /api-docs. Sign in and open it, and you get the live specification for the deployment you are signed in to, so the paths and schemas you read are the ones your calls will hit. It lists every operation the deployment publishes, which is what makes it useful for planning an integration: you can see an operation, and what it requires, before you hold the permission to run it.

Four things make it worth using rather than working from a copied specification file:

  • Each operation states the permission it needs. Select an operation and its required permissions are listed alongside the request and response detail, so you can see what a key's owner has to hold before you write the call. Permissions are enforced when the call runs, so the reference and the runtime agree.
  • Schemas expand on demand. Select an operation to see its request and response shapes, then drill into any nested type from the schema explorer.
  • Try it runs a real call. The Try-it pane builds a request, prefills the first example payload for the operation, and posts it. The auth toggle defaults to API Key, which is how an integration authenticates, so what you exercise there matches what your code will do. The console below it keeps a log of requests and responses across operation switches, so you can compare two calls side by side.
  • Code samples come with the operation. Each one renders as cURL, Python, and .NET, with the API key header as the primary auth mechanism.

The page toolbar also offers the OpenAPI document itself as JSON or YAML, if you want to generate a client from it.

Pull the specification file

Two places publish the specification, and they answer different questions.

Your deployment's live contract. Pull it from the deployment you integrate against:

GET https://your-deployment.example.com/openapi/v1.json
GET https://your-deployment.example.com/openapi/v1.yaml

Both are anonymous, so a code generator can fetch them without a credential. The older /swagger/v1/swagger.json path still works and redirects to the JSON address, so update any bookmark or build script that still uses it. A successful response carries an ETag and Cache-Control: no-cache, so send If-None-Match on later pulls and a 304 Not Modified tells you the contract hasn't changed. A 503 carries neither, so don't treat it as a cacheable answer. If an instance answers 503 with a Retry-After header, it hasn't produced a document yet. Usually it's still starting up, but a 503 that persists past a few retries means generation is failing on that instance, so report it rather than continuing to poll. You never receive a partial document.

Don't point a generator at /api-docs. That's the reference page for people, it requires a sign-in, and a generator asking it for a specification gets sign-in HTML back.

The versioned artifact behind the published SDKs. The samples repository your portal's SDKs and samples catalog links to holds the contract as spec/openapi.raw.json, on its bot/api-spec branch, alongside a changelog and a version record. It's republished with a new semantic version whenever the contract changes, so it's the one to pin against if you want a stable, reviewable baseline rather than whatever a deployment serves today.

What the specification does and doesn't depend on

Every caller gets the same document. It isn't rendered for your API key, your permissions, your merchant, or your account. Two callers pulling the same instance at the same moment receive identical bytes. What you can successfully call still depends on your permissions, but the document doesn't shrink to match them.

It reflects the deployment's feature settings at the moment it was generated. Operations behind an optional feature, such as digital wallets or surcharging, are present only when that feature is turned on. Once an instance is warmed up it regenerates on its own schedule rather than on your request, so two pulls taken minutes apart while a feature is being turned on or off can legitimately differ, and during a rolling update two instances behind one address can serve different documents for a short period. This is the usual explanation for a diff you didn't expect between two same-day pulls. It isn't caching, and it isn't the document being tailored to you.

If two pulls disagree, pull again once the change has settled and compare the ETag values. Matching ETags mean matching documents.

Which document to generate a client from

  • Prefer a published SDK. The catalog in the portal lists the client libraries built from the versioned artifact, already tested against a sandbox.
  • Generating your own client for a specific deployment? Use that deployment's /openapi/v1.json. It's the only source that includes the optional features that deployment has turned on.
  • Want a fixed target to pin and review? Use spec/openapi.raw.json from the samples repository. It's generated with every optional feature at its default setting, so operations behind digital wallets or surcharging aren't in it even if your deployment has them enabled. Take the live document instead when you need those.

One more document can be mistaken for this one. Deployments that still host the older compatibility API expose a separate specification, also labeled v1, describing that older surface. It isn't the contract described here, and it isn't the one to build a new integration against.

Start from the screen you already know

You don't have to hunt through the tree to find the operations behind a workflow. Portal pages carry a </> button in the toolbar that lists the API operations that page uses, split into the ones it owns and the ones it touches incidentally. Each entry deep-links into /api-docs with the operation already selected. The button appears on a page whose operations you hold the permissions to call, so what you see through it matches what you can invoke.

That's usually the fastest route into the reference: do the thing once in the portal, press </> on that screen, and read the operations that did it.

Authenticate with an API key

An API key is the primary credential for a server-side integration. It's a long random string you send on every request, and it needs no interactive login.

Create a key

Go to /ApiKeys and create one. You give it two things:

  • A name, so you can tell your integrations apart later.
  • An optional expiration date, from tomorrow up to a year out. The key stops working at the start of that day in your timezone, and the picker states which timezone that is.

Copy the key when it's shown. The full value is revealed once, at creation, with a copy button next to it. After that the portal shows only a masked form, and the key's detail page carries no field that could hold the secret. If a key is lost, delete it and create a new one.

Store the key the way you store any other production secret: in your secret manager or environment configuration, never in source control and never in browser-side code.

Send the key

Put the key in the api-key request header on every call:

POST /api/transactions
api-key: YOUR_API_KEY
Content-Type: application/json

As cURL:

curl -X POST https://your-gateway-host/api/transactions \
  -H "api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "...": "..." }'

That's the whole handshake: no separate login step, and no token to refresh.

What a key can do

A key belongs to the user who created it and carries that user's identity: the same permissions, the same tenant, and the same merchant or reseller scope. A request made with the key flows through exactly the same authorization checks an interactive session does, which means a key can never reach further than the person holding it.

That makes scoping an integration a matter of choosing the right owner. Give each integration its own user, with a role that grants only what that integration needs, then sign in as that user to create its key. A reporting job and a payment service can then hold genuinely different reach, and revoking one is a matter of deleting one key. See Creating and managing users for building the role and choosing the scope.

Narrow a key with scopes

A key can also be restricted to a set of capability scopes, which is a second, narrower boundary drawn on top of the owner's permissions.

Scopes only restrict. They never grant. A key still authenticates as its owner and still runs through exactly the same authorization checks, so what the key can actually do is the overlap between the two: the owner's permissions, narrowed by the key's scopes. Selecting a scope for something the owning account can't do changes nothing, and no combination of scopes can reach past what that account holds.

That's why scopes are worth adding even where the owner is already a purpose-built user. The owner decides the ceiling; the scopes decide how much of that ceiling one particular credential can use. A key you paste into a batch job can be limited to reading transactions even though its owner could also take payments, so a leaked copy of that key can't.

The vocabulary is fixed:

Scope What it reaches
transactions:read Look up transactions, transaction reports and batch files; read hosted payment page configuration and sessions, campaigns, promotion codes, shipping origins and parcel presets; request shipping rate quotes.
transactions:write Create and update transactions, including fraud review and partial approval decisions; upload and manage batch files; create and manage hosted payment pages, hosted payment page sessions, campaigns, promotion codes, shipping origins and parcel presets; run sandbox settlement and recurring billing on demand. Includes everything transactions:read reaches.
transactions:refund Run follow-up operations against an existing transaction, including refunds, voids and captures. Includes everything transactions:read reaches.
customers Read and manage customer records and their stored payment methods.
tokens Create, resolve and manage payment tokens, including wallet tokenization.
webhooks Manage webhook destinations, channels and event subscriptions.
reports Read usage, settlement and recurring billing reports.
admin The back office: merchants, resellers, users, roles, invoicing, announcements, rate limits and key management. Most integrations don't need this.

Know four things before you use them:

  • No scopes means no restriction. That's the default, and it's what every key created before scopes existed carries. Those keys keep working exactly as they did. Clearing every scope on a key returns it to that state.
  • A refund isn't a write. The operation on a follow-up call is named in the request body, so the platform can't tell a refund from a capture by the route alone. The whole follow-up family therefore sits behind transactions:refund, and a key holding only transactions:write is refused it.
  • The picker dims what the owner can't use. On both the portal and the back office, a scope the key's owning account holds no permission for is shown but can't be selected, with a note saying why. In the back office that's judged against the key's owner, not against you, because the key will authenticate as its owner.
  • Rotation carries scopes across. A rotated key is a replacement with the same access, so it inherits both the scopes and the source-address allowlist of the key it replaces.

Scopes are stored on the key and can be changed later from /ApiKeys without reissuing it. A change takes effect on the next request.

Watch a key in use

Open a key from the list at /ApiKeys to see what it has been doing:

  • Last Used, the most recent moment the key authenticated anything at all, including calls that create no transaction.
  • Recent Transactions, the newest transactions submitted with the key, each linking to its detail page.
  • Source IPs, the addresses the key has recently authenticated from, newest first, with first-seen and last-seen times and a link out to geolocation.

Last Used and Source IPs are recorded on the authentication path and written in batches, so they can trail live traffic by a few minutes. A source address never seen before is recorded without that delay, which is the signal to watch: if a key starts authenticating from somewhere you don't recognize, delete it and issue a new one.

Expiration

A key with an expiration date is warned about before it lapses, so the first sign is never a failed call. By default the owner is notified 30 days out, again at 7 days, and again on the last day. Each warning arrives as a notification in the portal for the key's owner, and the ApiKey.Expiring event can also be routed to email or any other destination through a notification subscription. API key expiration reminders covers the full story: the reminder cadence, how to route the event, its filter fields, and what a webhook receiver gets.

You don't have to wait for a warning to find out. A key inside the same window shows an Expiring soon badge everywhere keys are listed, both in the API Keys grid and in the Developer Portal, so the countdown is visible the moment you look rather than only when a reminder arrives. The badge uses the same window the reminders use, so if your administrator moves the threshold, the badge moves with it. A key with no expiration date never shows the badge, and neither does anything already revoked, expired, or mid-rotation: those states are shown instead, because each one tells you something more urgent.

Plan the rollover the same way you would a certificate: create the replacement key, deploy it, confirm traffic has moved by watching Last Used on both keys, then delete the old one.

Common authentication errors

A refused key comes back as HTTP 401 with a JSON body carrying a code. The code is the part to branch on: the six values are stable, and each one points at a different fix.

{
  "error": "Unauthorized",
  "code": "KEY_EXPIRED",
  "message": "API key has expired."
}
Code What happened What to do
KEY_INVALID The value you sent doesn't match any key, or isn't a key at all. A mistyped, truncated, or already-deleted key all land here. Check that the header is api-key and that the value is the full string you copied at creation, with no whitespace and nothing trimmed. If the key was deleted, create a new one.
KEY_REVOKED The key exists but has been taken out of service, and it will never be accepted again. Create a replacement key and deploy it. Extending anything on the old key won't bring it back.
KEY_SUSPENDED The key has been taken out of service temporarily, either by an administrator or automatically after the platform saw it used from a network it had never been used from before. It hasn't been revoked. Ask an administrator to reinstate the key from its detail page at /ApiKeys. Reinstating lifts the suspension from the same key value. If anything else is wrong with the key, the next response names that code instead. Don't create a replacement: a new key leaves the suspended one to be reinstated behind you.
KEY_EXPIRED The key exists and was in service, but it has passed the expiration date it was created with. Create a replacement key. The owner is warned 30 days, 7 days, and 1 day ahead, so wire those notifications somewhere your team reads.
KEY_ENVIRONMENT_MISMATCH The key is in service, but it was minted for the other environment than the one its merchant is in today. An sk_test_ key against a merchant that has since gone live, or an sk_live_ key against a merchant that's no longer trading live. Create a new key for the merchant now. A key's environment is fixed when it's created and is never re-stamped, so a key that spans a merchant's go-live has to be replaced.
TRIAL_EXPIRED The key is valid and in service, but the merchant it belongs to is on a time-limited plan whose deadline has passed. Nothing is wrong with the credential. Talk to your integration contact about extending the trial. Creating a new key won't help: no key for that merchant is accepted until the trial is extended, and the existing keys work again as soon as it is.

Three things worth building into your client:

  • Don't retry any of them. None is a transient condition, so retrying the same key produces the same answer. Surface the code and stop; a retry loop against a revoked key just fills your logs.
  • Route each code to the fix it names. KEY_INVALID, KEY_REVOKED, KEY_EXPIRED, and KEY_ENVIRONMENT_MISMATCH are terminal for the key you sent: correct the value or create a new key. KEY_SUSPENDED isn't terminal: an administrator can reinstate the same key, so send it to someone who can do that rather than to your key-rotation path. TRIAL_EXPIRED is about the merchant's plan rather than the key, so send it to whoever manages that plan; a new key won't clear it.
  • Log the code, never the key. The code is the diagnostic; the value you sent is a live credential and belongs nowhere but your secret store.

The response never says which environment the merchant is in, and never confirms whether a value it rejected corresponds to a real key belonging to somebody else. If you need to know why a specific key stopped working, its detail page at /ApiKeys carries the state and the expiry.

When the key is fine but the scope isn't

One more code isn't an authentication failure at all. A key that authenticates but isn't scoped for the operation it addressed comes back as HTTP 403:

{
  "error": "Forbidden",
  "code": "KEY_SCOPE_INSUFFICIENT",
  "message": "This API key is not scoped for this operation. It requires the 'transactions:write' scope.",
  "requiredScope": "transactions:write"
}
Code What happened What to do
KEY_SCOPE_INSUFFICIENT The key is valid and in service. It simply doesn't carry a scope covering the operation you called. Add the scope named in requiredScope to the key at /ApiKeys, or call the operation with a key that already holds it. The change takes effect on the next request.

Three things follow from it being a 403 rather than a 401:

  • Don't go looking at the credential. Nothing is wrong with it. Rotating or replacing the key won't help, and a client that treats this like a 401 will loop.
  • The response names the scope it wanted, and only that. It never lists the scopes the key does hold, because you can read those off the key itself.
  • An unscoped key never sees this code. If you haven't restricted a key, it can't be refused for a scope.

A key can still be refused by the ordinary permission checks after the scope gate lets it through, because scopes narrow and never widen. If you have added the scope and the call is still refused, the next thing to check is what the key's owning account is allowed to do.

Routes that need a signed-in user

A few routes end in self, and they behave differently from every other route in the API. self means the merchant the signed-in user belongs to, and that association is written into the session when a person signs in interactively. An API key carries no such association, so these routes can't serve one:

GET /api/merchants/self/custom-fields
{
  "error": {
    "code": "Merchants:NoCurrentMerchant",
    "message": "No merchant is associated with the current user. The self-service routes resolve the merchant from the signed-in user, so an API key cannot reach them. Read these settings from the merchant route that takes a merchant id in the path; updating them there needs the merchant update permission."
  }
}

The failure is HTTP 400, not a 401 or a 403, and it arrives after the key has already authenticated and cleared its scope check. Adding a scope, widening a permission, or rotating the key changes nothing.

The route that takes the merchant id in the path carries the same settings, so that's where to go instead:

Instead of Call Permission needed
GET /api/merchants/self/custom-fields GET /api/merchants/{id}, read customFields None beyond authentication
GET /api/merchants/self/order-data-defaults GET /api/merchants/{id}, read processing.orderDataDefaults None beyond authentication
PUT /api/merchants/self/custom-fields PUT /api/merchants/{id} with customFields set Merchants.Merchants.Update
PUT /api/merchants/self/order-data-defaults PUT /api/merchants/{id} with processing.orderDataDefaults set Merchants.Merchants.Update

Two things gate the fallback, and both are worth checking before you build against it. First, the whole /api/merchants surface sits in the admin scope, so a key you have restricted needs that scope to reach these routes at all. An unscoped key is unaffected. Second, writing needs a permission that reading doesn't. PUT /api/merchants/{id} requires Merchants.Merchants.Update, and the built-in Merchant role doesn't hold that permission: it carries the self-service settings permissions instead, which is precisely what the self routes ask for. So a key owned by a merchant account can read these settings over the API but can't write them by any route. A key owned by a reseller or an administrator account holds the update permission and can do both.

If you need a merchant-owned key to change its own custom fields or order data defaults, raise it with your integration contact rather than working around it: the permission is the constraint, and it's granted on the owning account, not on the key.

The self routes exist for the merchant-facing screens in the portal, where a user is always present. Server-to-server integrations should address merchants by id throughout: the id is stable, it's on every merchant record you already read, and it keeps a single integration able to serve more than one merchant.

Bearer tokens

Where an API key doesn't fit, WinkPG also issues OAuth 2.0 access tokens from the token endpoint at /connect/token. Use this when the caller is a person rather than a service, or when your platform already speaks OAuth:

  • An interactive application that signs users in and calls the API on their behalf uses the authorization code flow, and renews with the refresh token grant.
  • A confidential server-side client that acts as itself, with no user present, uses the client credentials grant.
  • A trusted first-party client that collects credentials directly uses the resource owner password grant, with refresh tokens for renewal.

A token authorizes exactly what its subject is permitted to do, the same way an API key does, so nothing downstream changes based on which credential you presented. Send the token as Authorization: Bearer <token>.

Your integration contact issues the client registration for the flow you need. For a straightforward server-to-server integration, an API key is the shorter path and the one to reach for first.

Make a create idempotent

A payment request that times out in transit leaves you with a real question: did it charge? Idempotency answers it. Set idempotencyKey on the transaction create request body to a value you generate and can reproduce on retry:

{
  "idempotencyKey": "order-48213-attempt-1",
  "transactionType": "Sale",
  "invoiceData": {
    "amounts": { "base": 49.00 }
  }
}

It's a property of the request body, so it travels with the payload rather than in a header.

The create response tells you what the key did

Every transaction create response carries an idempotencyStatus field reporting what idempotency actually did to that request. Read it rather than inferring anything from the 2xx:

idempotencyStatus What it means
NotRequested You sent no key. A repeat send will charge again.
KeyIgnored You sent a key, but deduplication isn't enabled for this merchant, so the key had no deduplication effect. A repeat send will charge again.
KeyAccepted You sent a key and deduplication is enabled. This is the original create, and the key now protects it.
Replayed Your key matched a create that had already completed. Nothing was charged; the body is that original transaction.

A single check for KeyIgnored on your first call against a new merchant is the fastest way to confirm your integration is actually protected.

The field is populated on create responses only. It's absent (null) when you read a transaction back later, because it describes a request rather than anything stored on the transaction.

Turn deduplication on

Deduplication is off until it's switched on for your merchant, and your integration contact can arrange it. It's switched on in one of two ways: directly, on the merchant's Processing settings, or by a default your provider sets across its whole portfolio, which applies to every merchant that hasn't set the value itself. A value set on the merchant always wins, so a merchant switched on individually stays on under a portfolio default of off, and a merchant switched off individually stays off under a portfolio default of on.

Once deduplication is on for your merchant, the key does three things:

  • A repeat is replayed, not recharged. Send the same key for the same merchant again and you get the original transaction back. The window is 48 hours from the original create; past that, the same key is treated as a fresh request and will charge.
  • A retry that arrives while the original is still running is told so. Rather than holding your request open or charging twice, WinkPG answers immediately with an "already being processed" rejection. Retry shortly, or look the transaction up by its key.
  • The key is validated. Up to 128 characters, made of letters, digits, and the characters ., _, :, and -.

Two rules to build into your key generator:

  • Make the key specific to the attempt you want deduplicated. An order id is a good basis; a timestamp or a fresh GUID per retry isn't, because a retry would carry a different key and charge again.
  • Don't start a key with rb:, inv-charge:, or inv-installment:. Those prefixes are reserved for the scheduled charges recurring billing and invoicing generate for themselves, and they're rejected on every create. Prefix your keys with something of your own.

While deduplication is off, a key you send is accepted but does nothing. The create returns a normal 2xx, the key is stored on the transaction, and it's not validated and not deduplicated: a repeat send charges again. A successful response is therefore not confirmation that deduplication is active, which is the one thing to be careful about here. The response says so explicitly: idempotencyStatus comes back as KeyIgnored. Treat that as a configuration problem to raise with your integration contact, not as a protected charge.

Whatever the deduplication setting, the key you send is stored with the transaction, and a lookup-by-idempotency-key operation returns the transaction a given key produced for a merchant. That lookup is worth wiring into your retry path regardless: before re-sending after a timeout, ask whether the key already produced a transaction.

Rate limits and the 429 response

Requests are rate limited per API key, and the limits that apply are tuned per deployment and per account rather than published as a fixed number. Design for the response instead of for a specific ceiling and your integration stays correct wherever it runs.

The short version: a limited request comes back as HTTP 429 with a Retry-After header in whole seconds and an application/problem+json body. When a limit measures a request, the response carries X-RateLimit-Limit and X-RateLimit-Remaining, so you can see where you stand without waiting for a rejection. They arrive together or not at all, and their absence means no limit measured the request rather than an allowance of zero. Wait at least the stated interval, back off exponentially with jitter past the first retry, and pair retries of a create with an idempotency key.

The rate limits reference states that response in full, and needs no account to read. Rate limits and your allowance covers your side of it: where to see your own current limits and usage in the portal, how to find the calls that were refused, and how to pace an integration so it stays clear of its limits.

Timestamps

Every instant WinkPG stores and returns is UTC. That part is simple and never varies. The string form is what to be careful with, because it differs across the API: some fields serialize a UTC instant with a Z suffix, others with an explicit +00:00 offset. Both mean the same moment.

So two rules cover every case:

  • Always send an explicit offset. Write 2026-08-06T14:30:00Z or 2026-08-06T14:30:00+00:00, never a bare 2026-08-06T14:30:00. A value with no offset leaves the reading open to interpretation, and stating the offset removes the question entirely.
  • Always parse with an offset-aware type. Use DateTimeOffset in .NET, an aware datetime in Python, or OffsetDateTime in Java. Parsing into a naive local type is where a correct payload turns into a wrong hour.

Anything you show a person should be converted from UTC at the point of display, using that person's timezone. How timezones are handled covers the whole model, including what happens in exports and generated documents.

Move an existing v1 integration

If you already have code written against the previous generation of the API, a compatibility surface speaks the v1 contract, so an existing integration keeps working without being rewritten. It's the supported path for v1 code, and you can adopt the current API for new work at your own pace.

What to expect from it:

  • v1 property naming and enums. Properties come back PascalCase, and enum values are strings, exactly as v1 published them.
  • v1 authentication. Callers authenticate with POST /api/Authenticate and use the token it returns, rather than with an API key.
  • A decline is a normal outcome, not an error. A refused payment comes back in the standard v1 transaction envelope with the outcome in ResultCode and ResultText, the same envelope an approval uses. Branch on the result code, not on the HTTP status alone. The current API answers a refusal the same way, and Understanding declines and rejections covers how to read one: the three result fields, the refusal families, which ones leave a hold, and what you can retry.

New integrations should target the current API described above: it's the one the in-app reference documents, and it's where new capability lands.

See also

Reconnecting to the server

Could not reconnect

This session has ended

Attempt 1

Your work on this page is still here. Retrying keeps it; reloading starts the page again.

The server no longer holds this page's state, so it has to be loaded again.