Start an order, take a PIN terminal payment and invoice the items#

This guide walks through the exact flow the Afosto POS uses to ring up a sale: start an order, add items, route the payment to a PIN terminal, invoice the items and finish the order.

Overview#

  1. Start an order using createOrder
  2. Add items via addItemsToOrder
  3. List the available payment methods and terminals via the order query
  4. Attach the PIN payment method and terminal with addPaymentMethodToOrder
  5. Create a payment for the amount due (REST)
  6. Start the terminal transaction via the payment's action.url and wait for the result
  7. Finish the order with openOrder
  8. Generate the invoice with invoiceItems

All GraphQL operations are sent to https://afosto.app/graphql with a Bearer token in the Authorization header. The two REST calls in steps 5 and 6 use the same host and token.


Step 1 — Start an order#

Orders start in the CONCEPT state: you can freely add items, customers and payment details until the order is opened in step 7.

Input: CreateOrderInput!

NameTypeRequiredDescription
channel_id
String
OptionalThe channel to create the order in. Required when not authenticated with a storefront token.
currency
Currency
OptionalThe currency for the order, e.g. EUR.
customer
CustomerInput
OptionalAttach a contact or organisation to the order.

Returns: Order

NameTypeRequiredDescription
id
ID!
RequiredThe ID
number
String!
RequiredOrder number
total
Money!
RequiredTotal value
currency
Currency!
RequiredCurrency code
mutation CreateOrder($input: CreateOrderInput!) {
  createOrder(input: $input) {
    order {
      id
      number
      status
      currency
      total
    }
  }
}
{
  "input": {
    "channel_id": "3c7d1e5f-9a2b-4f8e-b6d0-1234abcd5678"
  }
}

Save the returned order.id — every following step needs it.


Step 2 — Add items#

Input: AddItemsToOrderInput!

NameTypeRequiredDescription
order_id
String!
RequiredThe order to add items to.
items
[OrderItemInput!]!
RequiredThe items to add. Pass the sku and quantity; pricing is resolved from your catalog.

Returns: Order

NameTypeRequiredDescription
id
ID!
RequiredThe ID
number
String!
RequiredOrder number
total
Money!
RequiredTotal value
currency
Currency!
RequiredCurrency code
mutation AddItemsToOrder($input: AddItemsToOrderInput!) {
  addItemsToOrder(input: $input) {
    order {
      id
      items {
        ids
        sku
        label
        quantity
        subtotal
        total
      }
      subtotal
      total
    }
  }
}
{
  "input": {
    "order_id": "72fca344-2a6f-4c3e-b4ca-029920b2522a",
    "items": [
      { "sku": "BLK-HOODIE-M", "quantity": 1 },
      { "sku": "BLK-HOODIE-L", "quantity": 2 }
    ]
  }
}
·
Note:

Each order line exposes ids — one ID per unit. The invoice in step 8 references these item IDs, but you don't need to track them yourself: the order's actions field tells you which IDs are ready to invoice.


Step 3 — List payment methods and terminals#

The payment options for an order — including the PIN terminals configured for your tenant — are exposed on the order itself:

query GetOrderPaymentMethods($id: String!) {
  order(id: $id) {
    id
    options {
      payment {
        methods {
          id
          code
          name
          description
          is_manual
          pricing {
            fixed
            percentage
          }
          issuers {
            id
            label
          }
          terminals {
            id
            label
            is_online
            currency
          }
        }
      }
    }
  }
}
{
  "id": "72fca344-2a6f-4c3e-b4ca-029920b2522a"
}
Tip:

The PIN payment method is the one that exposes terminals. Pick a terminal with is_online: true and save both the method.id and the terminal.id for the next step.


Step 4 — Attach the PIN payment method and terminal#

Use addPaymentMethodToOrder to register the PIN payment method on the order, passing the terminal_id to route the transaction to the correct device.

Input: AddPaymentMethodToOrderInput!

NameTypeRequiredDescription
order_id
String!
RequiredThe order ID.
method_id
String!
RequiredThe payment method ID from step 3.
terminal_id
String
OptionalThe PIN terminal ID. Required for in-person PIN payments.
issuer_id
String
OptionalThe issuer ID, for issuer-based methods such as iDEAL. Not used for PIN.
drawer_id
String
OptionalThe cash drawer ID, for cash payments. Not used for PIN.

Returns: Order

NameTypeRequiredDescription
id
ID!
RequiredThe ID
number
String!
RequiredOrder number
total
Money!
RequiredTotal value
currency
Currency!
RequiredCurrency code
mutation AddPaymentMethodToOrder($input: AddPaymentMethodToOrderInput!) {
  addPaymentMethodToOrder(input: $input) {
    order {
      id
      billing {
        payment {
          method {
            id
          }
          terminal {
            id
          }
        }
      }
      options {
        proceeding_step {
          is_action_required
          proceeding {
            action
            options {
              type
              method
              url
            }
          }
        }
      }
    }
  }
}
{
  "input": {
    "order_id": "72fca344-2a6f-4c3e-b4ca-029920b2522a",
    "method_id": "e5f6a7b8-c9d0-1234-5678-90abcdef0123",
    "terminal_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}
·
Note:

options.proceeding_step describes what is needed to proceed with checkout. For a PIN payment the next action is creating and executing a payment.


Step 5 — Create the payment#

Create a payment on the order for the amount due. This is a REST call:

POST https://afosto.app/api/commerce/orders/{order_id}/payment
{
  "data": {
    "amount": 14994
  }
}

Returns: the payment, including its id, status (PENDING) and an action describing how to proceed. For a payment routed to a PIN terminal the action type is START:

{
  "data": {
    "id": "d4e5f6a7-b8c9-0123-4567-890abcdef012",
    "status": "PENDING",
    "amount": 14994,
    "amount_total": 14994,
    "action": {
      "type": "START",
      "url": "https://afosto.app/api/commerce/payments/public/{token}/start"
    }
  }
}
Tip:

amount may be lower than the order total — create multiple payments to support split payments. Save the payment's action.url for the next step.


Step 6 — Start the terminal transaction and wait for the result#

Perform a GET request on the payment's action.url. This starts the transaction on the terminal — the customer is prompted to tap or insert their card — and the request stays open until the terminal reports a result, so there is no need to poll yourself:

GET https://afosto.app/api/commerce/payments/public/{token}/start

Returns: the updated payment once the transaction settles.

{
  "data": {
    "id": "d4e5f6a7-b8c9-0123-4567-890abcdef012",
    "status": "PAID",
    "amount": 14994,
    "amount_paid": 14994,
    "is_paid": true,
    "paid_at": 1781085600
  }
}
·
Note:

Check is_paid on the response. When the customer aborts the transaction on the device, the payment comes back unpaid (PENDING, or CANCELLED/EXPIRED) — call the action.url again to restart the terminal transaction, or create a new payment.

The action.type depends on the payment method: START for PIN terminal payments, PAY for manual methods such as cash (mark the payment as paid yourself), and FOLLOW for external payment pages (redirect the customer to the URL).


Step 7 — Finish the order#

Once the full amount is paid, finish checkout by opening the order. This transitions the order from CONCEPT to OPEN and starts processing: stock is allocated and the items become ready to invoice.

Input: OpenOrderInput!

NameTypeRequiredDescription
order_id
String!
RequiredThe order to open.

Returns: Order

NameTypeRequiredDescription
id
ID!
RequiredThe ID
number
String!
RequiredOrder number
total
Money!
RequiredTotal value
currency
Currency!
RequiredCurrency code
mutation OpenOrder($input: OpenOrderInput!) {
  openOrder(input: $input) {
    order {
      id
      number
      status
      actions {
        action
        ids
      }
    }
  }
}
{
  "input": {
    "order_id": "72fca344-2a6f-4c3e-b4ca-029920b2522a"
  }
}

The order now starts processing — the next step waits for the items to become invoiceable.


Step 8 — Invoice the items#

After the order is opened, the order exposes an invoice action listing the item IDs that are ready to be invoiced. Processing is asynchronous, so poll the order until the action appears (for example poll every 250 ms, with a 15 second timeout):

query GetOrderInvoiceAction($id: String!) {
  order(id: $id) {
    id
    actions {
      action
      ids
    }
  }
}
{
  "id": "72fca344-2a6f-4c3e-b4ca-029920b2522a"
}

When the action with action: "invoice" contains IDs, pass them to invoiceItems:

Input: InvoiceItemsInput!

NameTypeRequiredDescription
items
[String!]!
RequiredThe item IDs from the order's invoice action.
invoice_id
String
OptionalAn existing invoice ID to add the items to. Omit to create a new invoice.

Returns: InvoiceItemsPayload

NameTypeRequiredDescription
invoice
Invoice
OptionalThe created invoice
mutation InvoiceItems($input: InvoiceItemsInput!) {
  invoiceItems(input: $input) {
    invoice {
      id
      number
      currency
      subtotal
      total
      created_at
    }
  }
}
{
  "input": {
    "items": [
      "5a6b7c8d-9e0f-1234-5678-9abcdef01234",
      "6b7c8d9e-0f1a-2345-6789-0abcdef12345",
      "7c8d9e0f-1a2b-3456-789a-bcdef0123456"
    ]
  }
}

The order is now fully processed: paid via the PIN terminal, opened and invoiced.


Notes#

  • Money values (total, amount, item prices) are integers in cents — e.g. 14994 = € 149,94.
  • Order states are CONCEPTOPENCLOSED. The POS works on a CONCEPT order throughout checkout and opens it only after the payment is settled.
  • You can invoice a subset of items by passing only some of the IDs from the invoice action — useful for partial invoicing.
  • For cash payments the same flow applies, but pass a drawer_id instead of a terminal_id in step 4. The payment's action is then PAY instead of START: mark the payment as paid via PUT https://afosto.app/api/commerce/payments/management/{payment_id}/pay with body { "data": { "amount": <amount> } } instead of calling a start URL.
  • See Authentication for how to obtain and pass your API token.
Query Runnerhttps://afosto.app/graphql

No query loaded

Click play on any code block in the docs to load a query here.