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
queryand the values invariables. - 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
firstfor the page size. It defaults to 25, andpaymentsallows at most 50. - To get the next page, pass the previous page's
pageInfo.endCursorasafter. Stop whenhasNextPageisfalse. - Results are also available as
edges { cursor node }if you need a cursor per row. - Do not rely on
lastorhasPreviousPage. 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.