# 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.

**Category:** Integration

**Last reviewed:** 8 August 2026

# Getting Started with the API

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 afterwards: 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.

### Start from the screen you already know

You do not 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 is what you can invoke.

That is 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.

## Authenticating with an API key

An API key is the primary credential for a server-side integration. It is 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 is 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:

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

That is the whole handshake. There is 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](/help/guides/creating-and-managing-users) for building the role and choosing the scope.

### 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 click-through for 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 brand new source address is recorded promptly, which is the signal worth watching: if a key starts authenticating from somewhere you do not recognise, 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; see [Configuring Notifications](/help/guides/configuring-notifications) for how to set that up.

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 four values are stable, and each one points at a different fix.

```json
{
  "error": "Unauthorized",
  "code": "KEY_EXPIRED",
  "message": "API key has expired."
}
```

| Code | What happened | What to do |
|------|---------------|------------|
| `KEY_INVALID` | The value you sent does not match any key, or is not 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 will not bring it back. |
| `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 is no longer trading live. | Create a new key for the merchant now. A key's environment is fixed when it is created and is never re-stamped, so a key that spans a merchant's go-live has to be replaced. |

Two things worth building into your client:

- **Treat all four as terminal, not retryable.** None of them 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.
- **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.

## Bearer tokens

Where an API key does not 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.

## Making 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:

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

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

Once deduplication is enabled for your merchant (on the merchant's **Processing** settings, and your integration contact can arrange it), 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 is not, because a retry would carry a different key and charge again.
- **Do not 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 are rejected on every create. Prefix your keys with something of your own.

Whatever the deduplication setting, the key you send is stored with the transaction, and there is a lookup-by-idempotency-key operation that 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, and the limits that apply are tuned per deployment 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.

A limited request comes back as **HTTP 429** with a `Retry-After` header in whole seconds and an `application/problem+json` body:

```
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/problem+json
```

```json
{
  "type": "https://httpstatuses.io/429",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "The request was rejected because a rate limit was exceeded.",
  "retryAfterSeconds": 12
}
```

`Retry-After` and `retryAfterSeconds` carry the same figure, so read whichever suits your HTTP client. It is always at least one second.

What a good client does with that:

- **Wait at least the stated interval before retrying.** Retrying sooner is refused again.
- **Back off exponentially past the first retry**, with a little random jitter, so a fleet of workers that all got limited at once does not come back in lockstep.
- **Cap the number of retries** and surface a failure rather than looping.
- **Pair retries of a create with an idempotency key**, as above, so a retry that turns out to have been unnecessary replays instead of charging again.

Treat 429 as normal operating feedback, not as an error condition. A queue-driven integration that honors `Retry-After` will not notice it.

## 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 there are two rules, and they 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](/help/guides/how-timezones-are-handled) covers the whole model, including what happens in exports and generated documents.

## Moving an existing v1 integration

If you already have code written against the previous generation of the API, there is a compatibility surface that speaks the v1 contract, so an existing integration keeps working without being rewritten. It is 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.

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

## See also

- [Webhook Integration](/help/guides/webhook-integration) for receiving platform events at your own endpoint once you are making calls, including the signature scheme and deduplication.
- [The Embedded Payments SDK](/help/guides/embedded-payments-sdk) for collecting card details in your own page without the card data reaching your servers.
- [Reusing a Saved Card with Payment Tokens](/help/guides/reusing-saved-cards) for charging a stored card again from your own code.

## See also

- [All documentation](https://docs.winkpg.io/llms.txt): the machine-readable index of every public page on this site.
