APG PayAPG Paydocs

API reference

How this API works

Every resource behaves the same way. Read this once and you can predict the shape of any endpoint you have not used yet — which is the point of it, and it is enforced by a conformance suite rather than by review.

Every resource on this API behaves the same way. Read this page once and you can predict the shape of any endpoint you have not used yet, that is the point of it, and it is enforced by a conformance suite rather than by review.

Resources and paths

A path is a collection and, where you are addressing one thing, its identifier:

/merchants                     the collection
/merchants/{merchantId}        one of them

Collections are plural and kebab-case. There are no verbs in paths, no audience segments (/enterprise/...), and no nesting of independent resources, a payout is at /payouts, not under the transaction it settles.

Paths carry no mount point. You call /merchants; which internal prefix that forwards to is a property of the deployment, not of the API.

The operations

RequestOperation idReturns
GET /{collection}/{id}get<Resource>one of them
GET /{collection}query<Resources>a page of them
POST /{collection}create<Resources><Prefix>CreateRsp, what was created, plus anything creating it produced
POST /{collection}/{id}actOn<Resource><Prefix>UpdateRsp, the resource as the action left it
PUT /{collection}/{id}replace<Resource>the view, a replacement returns what a read returns
PATCH /{collection}/{id}update<Resource><Prefix>UpdateRsp, the resource as it now stands, plus anything the update produced
PATCH /{collection}update<Resources>the same, where the instance is named by a selector in the body
DELETE /{collection}/{id}delete<Resource>

Not every resource offers all of them. A resource that cannot be created through the API, a dispute, an issuer transaction, has no POST, and says so in its overview.

The operation id is derived from the collection name, so you can predict it: /issuer-transactions gives getIssuerTransaction and queryIssuerTransactions.

Changes are fields, actions are a type, and neither is a verb in the path

A change to what a resource holds is a PATCH of its fields. Something you do to a resource whose effect goes beyond its fields, a business check, an account opened at a gateway, an email or a webhook, is an action: a POST to the resource itself, with the body's type naming the action.

POST /merchants/mcht_01H8…
{ "type": "MchtCmdActivation", "suspended": false, "checkoutEnabled": true }

A create is the same idea on the collection: POST /merchants with a type saying which kind of merchant to make. The path names what is acted on, so a body never carries its own resource's identifier.

The discriminator value is always exactly the schema component name, so the type you send is the schema you can look up. An unrecognised type on a request is a 400. Forward compatibility is a response-side concern, see below.

Leaving a field out keeps it, null removes it

An update changes only what it sends. Where an update's groups are …Patch shapes, a field you can remove is published as nullable, and null removes it:

PATCH /merchants/mcht_01H8…
{ "type": "MchtUpdateBranding",
  "branding": { "type": "MchtBrandingPatchAttributes", "brandColour": "#0B7285", "logoAltText": null } }

That sets the colour, removes the alt text, and leaves the logo and icon as they were. null on a field that is not nullable is a 400, never a quiet "unchanged". A list is replaced when it is sent, and [] empties it.

What a write answers with

A write never makes you follow it with a read. It hands back the resource, and anything the operation produced that the resource does not hold:

POST /api-keys
{ "type": "ApiKeyCmdCreate", "displayName": "Reporting", "domain": "Merchant" }
200
{ "type": "ApiKeyCreateRspIssued",
  "created": { "type": "ApiKeyView", "apiKeyId": "key_9f2…", "last4": "f31a", … },
  "secret": "sk_live_9f2Kd8QpLzXwR4mN" }

The secret is on the response and nowhere else. It is not a field on the token that happens to be null on every read, a field whose presence you have to learn from prose is a field you will get wrong, so the shape says when it is there.

The same on the way through:

OperationAnswers withCarries
POST on a collection<Prefix>CreateRspcreated, plus what creating produced
PATCH<Prefix>UpdateRspupdated, plus what the update produced
POST on one<Prefix>UpdateRspupdated, the resource as the action left it
PUTthe viewnothing else, a replacement produces nothing of its own

Both envelopes are tagged unions, and most have one arm today. That is deliberate: a new outcome arrives as a new arm, which is not a breaking change, whereas a new optional field on a flat body leaves you unable to tell which combinations are real.

created and updated are named the same on every resource, so one helper reads any of them.

Naming one thing that has no identifier of yours

Normally you address a resource by its identifier in the path. Some can also be named by a key you already hold, an issuer transaction by the card and your own reference for the purchase, for instance, when you never stored ours.

Those go through a collection-level PATCH, with a tagged selector in the body:

PATCH /issuer-transactions
{ "type": "ItxnUpdateAttributes",
  "selector": { "type": "ItxnSelectorCardReference",
                "cardReference": "dcard_9f2…",
                "referenceTransactionId": "booking-88213",
                "merchantId": "mcht_9f2…" },
  "status": "Captured" }

The reason it is a body rather than a path or a query is that one of these keys can contain a card number, and a card number must never appear in a URL, URLs are logged, proxied and kept in browser history.

A selector must match exactly one: none is a 404, more than one is a 409. It is a way of naming an instance, never a way of changing several.

Doing many at once

Bulk work is a resource, not an array. POST /transaction-batches creates a batch, and the batch is a thing you can read afterwards:

POST /transaction-batches
{ "type": "TranbatchCmdCreate", "transactions": [ … ] }

This is because five hundred creates with three rejections is neither a success nor a failure. The batch records what happened to each item, so a partial result is something you can read and act on rather than something you have to infer from a status code.

One read, and you choose how it pages

There is no /search twin. GET /{collection} carries the filters and the pagination the collection serves, and you pick:

You wantSend
Offset pages?page=2&limit=50
Cursor pages?startingAfter=<id>&limit=50 (or endingBefore to go back)
Free-text search?q=acme, combines with either, and with filters
Sorting?sort=-createdAt,updatedAt, - for descending, in priority order

Asking for both paginations at once is a 400, not a silently-preferred mode.

A collection publishes only the paging parameters it serves. Most serve both; a list read whole, such as a merchant's balances, takes page alone, and a list walked forward, such as your exports, takes startingAfter alone. A paging parameter a collection does not publish is a 400 naming it, as any parameter it does not declare is.

Each collection lists the fields it can be sorted by, and any other field is a 400 naming them. A collection with no sort parameter returns its own fixed order.

Counts come back in the body rather than in headers, because generated clients drop headers:

{ "data": [ … ],
  "hasMore": true,
  "nextStartingAfter": "mcht_01H8…",   // cursor mode
  "totalCount": 412,
  "page": 1, "totalPages": 9 }         // offset mode

Getting a spreadsheet out

Every collection read renders as CSV as well as JSON. Ask for it with Accept:

GET /transactions?status=Settled&sort=-createdAt&limit=200
Accept: text/csv

Filters and sorting work exactly as they do for JSON, because it is the same read. Choose the columns, in the order you want them, with columns:

GET /refunds?columns=RefundId,AmountsAmount,SettledAt
Accept: text/csv

The available columns are on each resource as an enum, RfndColumn, TranColumn, and they are the view's own fields, so anything you can read in JSON you can have as a column. Nested objects flatten one level (AmountsAmount); arrays do not become columns, because a row holds one value, so export the sub-collection instead. A field that is one of several shapes, a refund's subject, is not a column either. A customer is a consumer or an organisation, so its CSV has a column for every field of both, after a Type column naming which each row is; a field the row's shape lacks is empty. Without columns you get every column, in the enum's order.

columns applies to CSV only. Sent with application/json it is a 400, because a JSON response with a caller-chosen field list is a response this document could not describe.

A CSV response is a page, and the same limit applies as for JSON. It is the same read, so it holds what the JSON page holds: a field you may not read in JSON, a customer's name or email address without access to customers' personal details, is empty in CSV. It is written exactly as an export of the list writes it, cell for cell. A page too large to write in the request, which only a large limit over wide rows comes near, is a 400 saying so: ask for a smaller limit, or export the whole list.

For every row of a list at once, of any size, export it: POST /exports with the collection as the source and the list's own query parameters, and the file is answered at once when it is small or written in the background when it is not. For every row with totals, run a report. There are four, transactions or refunds over a period, one payout's contents and a merchant's settlement, and the answer is the report itself:

POST /reports
{ "type": "RprtCmdTransactions",
  "merchantId": "mcht_9f2…",
  "from": "2026-09-01T00:00:00+10:00",
  "to": "2026-10-01T00:00:00+10:00",
  "timeZone": "+10:00" }

Nothing is stored, so there is nothing to poll and nothing to read back.

Getting a file out

A document the platform has already rendered and stored is served as the file itself, under its own media type, not inside JSON. A loan statement's CSV, PDF and XML are read this way:

GET /loan-statements/{loanStatementId}/documents/Pdf
Accept: application/pdf

Content-Disposition names the file. An Accept that matches none of the operation's media types, application/json for one, is a 406 with an empty body.

Reading a sub-collection in context

Some things belong to something else and are worth reading from it:

GET /transactions/{transactionId}/refunds
GET /transactions/{transactionId}/refunds/{refundId}
GET /transactions/{transactionId}/disputes
GET /transactions/{transactionId}/disputes/{disputeId}

and the same four under /issuer-transactions/{issuerTransactionId}, because a refund of a card the platform issued is a refund like any other.

These are reads. The refund itself is created at POST /refunds naming the transaction, and its address is /refunds/{refundId}, that is what a webhook sends you and what to store. The paths above are for reading in context and return the same RfndView you get anywhere else.

The parent is checked rather than assumed: asking for a refund of one transaction under a different one is a 404, not a redirect and not a shrug.

Identifiers

Every identifier carries a prefix naming what it identifies, mcht_, cust_, tran_, psrc_, ordr_, loan_, payout_, dcard_, itxn_. Passing an identifier of the wrong kind is a 400, not a 404: you are told you sent the wrong kind of id rather than that the thing does not exist.

Treat ids as opaque strings. The prefix is a contract; the rest is not.

Enumerations

Enumerated values are PascalCase, AwaitingActivation, AmericanExpress, LostOrStolen. No spaces, no snake_case, no empty strings. Codes defined by an external standard keep that standard's casing: currencies are AUD, countries are AU.

Decode unknown values rather than failing on them. Enumerations that appear in responses carry an Unknown member. A value your client does not recognise is a state added since your integration was written, not a protocol error, and treating it as one is the most common way an integration breaks on a release it did not need to care about.

Money

A single amount is an IsoCurrencyAmount, the amount in the currency's minor units, and the currency:

{ "minorCurrencyUnits": 5000, "currency": "AUD" }

Several amounts that necessarily share a currency are grouped, with the currency stated once:

{ "currency": "AUD", "authorised": 5000, "captured": 5000, "refunded": 2000, "fee": 145 }

A bare integer amount with no currency to interpret it does not appear anywhere on this API.

Dates and times

Timestamps are ISO-8601 with an offset, and are named for what happened: createdAt, capturedAt, authorisedAt. Dates without a time, a settlement date, are YYYY-MM-DD.

Errors

Every operation returns the same error shape. A 400 means the request was wrong, a malformed id, an unknown command type, both paginations at once. A 403 means your credentials are valid but do not carry the authority the operation requires.

Card data

Card numbers never reach this service in the clear. Requests carrying them go through the PCI vault, which is transparent to you: you call the same path with the card number, and the service receives a token. Responses that return card data are encrypted under a key you nominate with the Encryption-Key-Name request header, and the response carries Encryption-Key.

Credentials

Each resource states the credentials it accepts, an API secret key, or a JWT for a merchant, marketplace, admin or customer session. Beyond authentication, each *operation* requires an authority, shown on the operation itself.