Skip to content

Requests and responses

The Merchant API is a single GraphQL endpoint. Every request is an HTTPS POST of JSON to the host root, with your authentication headers.

Sending a request

curl https://stg.api.apteanpay.com/ \
  -H 'content-type: application/json' \
  -H 'x-aptean-apim: <your api key>' \
  -H 'x-aptean-tenant: <your tenant id>' \
  -H 'x-aptean-tenant-secret: <your tenant secret>' \
  -d '{
    "query": "query Payment($id: String!) { payment(id: $id) { id status } }",
    "variables": { "id": "<payment id>" }
  }'

Good habits:

  • Use variables, never string-built queries. Put the operation in query and the values in variables.
  • Name your operations (query Payment, mutation CreatePayment). Aptean support can find a named operation in the logs much faster than an anonymous one.
  • Ask only for the fields you use. Smaller responses are faster, and your code is not exposed to fields it does not need.

Exploring the schema

In staging, the host serves an interactive GraphQL playground and allows introspection. Use it to browse the schema and to generate client types. Production allows neither, so generate types against staging. See Environments and URLs.

What a mutation returns

Every mutation returns the same envelope, plus the object it acted on:

{
  "data": {
    "createPayment": {
      "code": "SUCCESS",
      "message": "Payment created.",
      "error": null,
      "errorReason": null,
      "payment": { "id": "300f5a49-68f0-44a0-8f73-d2f42ced4a90", "status": "COMPLETED" }
    }
  }
}
Field Meaning
code SUCCESS, PENDING or ERROR.
message Human-readable outcome.
error Extra detail when code is ERROR.
errorReason Structured detail: { code, message, details: [{ code, message }] }.
payment, refund, ... The object, in its state after the call.

Three ways a call can go wrong

What you see What happened What to do
errors[] at the top level, data is null The request was refused before anything happened: bad credentials, invalid input, or an operation that is not allowed, such as voiding a settled payment. Read message. Fix the request; retrying it unchanged will fail the same way.
code: "ERROR" with the object The request ran and did not succeed. For a payment, the processor declined it and payment.status is FAILED. Read message and errorReason. See Errors.
code: "SUCCESS" with payment.status: "PENDING" Not an error. The payment is accepted and still in progress. Follow it up. See Payment status.

A refused request looks like this:

{
  "errors": [
    {
      "message": "This payment has already settled and can no longer be voided. Create a refund instead.",
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "errorCode": "INVALID_OPERATION",
          "reasonCode": "ALREADY_SETTLED"
        }
      }
    }
  ],
  "data": null
}

When extensions.exception.reasonCode is present, branch on it rather than on message. Codes are stable; wording can change. extensions.code alone is not useful for this: most refusals carry the generic INTERNAL_SERVER_ERROR. The exceptions are authentication failures, which carry FORBIDDEN or UNAUTHENTICATED. See Errors.

Check both places

A GraphQL client can treat HTTP 200 as success. A refused request often comes back as HTTP 200 with errors[] set. Always check for errors[] and the code in the envelope.

Lists and pagination

List queries such as payments and paymentRequests use cursor paging:

query Payments($after: String) {
  payments(first: 50, after: $after) {
    totalCount
    pageInfo { hasNextPage endCursor }
    nodes { id status amount }
  }
}
  • Pass first for the page size. It defaults to 25, and payments allows at most 50.
  • To get the next page, pass the previous page's pageInfo.endCursor as after. Stop when hasNextPage is false.
  • Results are also available as edges { cursor node } if you need a cursor per row.
  • Do not rely on last or hasPreviousPage. Page forwards.

Filter on the server rather than paging through everything. The payments filters are listed in Payment status.

Limits

Limit Value
JSON request body 512 KB
File upload (invoice PDF) 12 MB
Query nesting depth 10 levels

Aptean does not publish a rate limit. Treat the API as shared: poll only what is pending, page rather than repeatedly fetching everything, and back off when you retry. See Resilience and retries.