Arta Pay Integration Reference

Arta Pay is finance tailored for art, luxury, and collectibles. Integrating with Arta Pay enables pre-approved buyers to use an Arta credit line at your checkout. You add the @artaio/arta-browser SDK to your site and call the Arta API from your server.

Use each integration point for a different job:

  • Customer context API — Create a purchase request and retrieve its result. Role: authoritative.
  • Arta Pay widget — Let the buyer sign in, choose a deposit and confirm. Role: buyer interaction.
  • SDK callbacks — Update your checkout while the buyer is present. Role: browser notification only.
  • webhooks — Receive purchase and payout changes on your server. Role: signed server notification.

Do not fulfill an order from an SDK callback alone. Confirm the purchase from your server first.

Understand the resources

Purchase requests and purchases represent different stages of the transaction.

  • Purchase request — Created by the seller. Contains proposed order data, a buyer email hint, checkout status and the resulting purchase ID when confirmed.
  • Purchase — Created by Arta. Contains the authenticated buyer, confirmed order snapshot, financing breakdown and seller payout status.

A purchase request contains the seller order, proposed amounts and items. It can end without a purchase if the buyer leaves, declines or cannot complete the flow.

Arta creates one purchase only after confirming the request. The purchase keeps a snapshot of the confirmed order and links back to its purchase request.

The lifecycle is:

purchase request created
  -> in progress
  -> processing
  -> confirmed -> purchase created
  -> declined, cancelled or expired -> no purchase

Sellers create purchase requests through POST /purchase_requests. There is no POST /purchases endpoint.

Before you start

You need:

  • an Arta organization with Arta Pay and API access enabled
  • a private API key for your server
  • a public API key for your browser
  • at least one purchase payout account, with one set as the default
  • an HTTPS endpoint that can receive Arta webhooks

Test and live are separate grants on your organization. Test access works while your organization is still pending, and live access requires an activated one. You can be fully working in test mode and still get a 403 from a live key.

Never include a private API key in browser code. A public key identifies your organization but cannot create or retrieve purchases.

The examples use these values:

  • API base URL: https://api.arta.io
  • Default API version: 2021-01-01
  • Private API key: ulbZij4WHmZK4pC8xR2nQ6sT
  • Public API key: fpFJOll1kQ7mN3vB8sD2xR5T

The API uses 2021-01-01 by default, so the examples omit the ARTA-VERSION header.

Use testing keys while you build. Testing and production data remain separate. “Build against test mode”, below, covers the scenarios and the simulation endpoints.

Timestamps come back as naive ISO 8601 with microseconds and no zone suffix, for example 2026-07-20T19:04:31.062312. Treat them as UTC.

Follow the integration flow

The browser callback and webhook can arrive in either order. Your server should handle both paths safely.

sequenceDiagram
    actor Buyer
    participant Browser as Seller checkout
    participant Seller as Seller server
    participant API as Arta API
    participant Widget as Arta Pay widget

    Buyer->>Browser: Selects Arta Pay
    Browser->>Seller: Requests an Arta Pay checkout
    Seller->>API: Creates purchase request
    API-->>Seller: Returns request ID and client token
    Seller-->>Browser: Returns request ID and client token
    Browser->>Widget: Opens checkout
    Buyer->>Widget: Signs in and confirms deposit
    Widget->>API: Confirms request
    API->>API: Creates purchase
    API-->>Seller: Sends signed webhook
    Widget-->>Browser: Calls onComplete
    Browser->>Seller: Asks server to confirm result
    Seller->>API: Retrieves purchase request or purchase
    API-->>Seller: Returns authoritative status
    Seller-->>Browser: Confirms order or shows processing state

Install the browser SDK

Install the SDK from npm:

npm install @artaio/arta-browser

Then import and initialize it once:

import Arta from '@artaio/arta-browser';

Arta.init('fpFJOll1kQ7mN3vB8sD2xR5T');

The public key identifies your organization to the SDK. The client token you pass to Arta.pay authorizes one purchase request.

init takes an optional second argument, {host, httpSchema, payOrigin}. Leave it out in production. payOrigin selects the host that serves the widget and the sign-in pages, and exists for local and QA environments; it does not select test mode. Your API keys decide that.

You can also load a pinned SDK version from the jsDelivr CDN:

<script src="https://cdn.jsdelivr.net/npm/@artaio/arta-browser@<VERSION>/dist/bundle.js"></script>
<script>
  Arta.init('fpFJOll1kQ7mN3vB8sD2xR5T');
</script>

Pin a tested version in production. Do not use @latest because it can change without a deployment from you.

Choose a purchase payout account

Your organization can have several purchase payout accounts. Each account has a name, account holder, routing number and account number. One account is the default.

Fetch the available accounts from your server:

curl 'https://api.arta.io/purchase_payout_accounts?page=1&page_size=20' \
  --header 'Authorization: ARTA_APIKey ulbZij4WHmZK4pC8xR2nQ6sT'
{
  "items": [
    {
      "id": "38190375-f6d0-4f57-8e66-24914c2b70ad",
      "name": "Operating",
      "account_type": "checking",
      "account_holder_name": "Patton Gallery LLC",
      "routing_number": "021000021",
      "account_number_last_four": "6789",
      "is_default": true,
      "created_at": "2026-09-21T16:25:21.181314"
    },
    {
      "id": "dbefc07d-ad38-45b1-a82b-fb5d02042b9b",
      "name": "Reserve",
      "account_type": "checking",
      "account_holder_name": "Patton Gallery LLC",
      "routing_number": "026009593",
      "account_number_last_four": "4412",
      "is_default": false,
      "created_at": "2026-09-21T15:45:23.171826"
    }
  ],
  "metadata": {
    "is_estimate": false,
    "page": 1,
    "page_size": 20,
    "total_count": 2
  }
}

The endpoint returns only accounts that belong to your organization. It lists the default account first, then sorts the remaining accounts by name. Arta applies that ordering within each page, so ask for a page_size large enough to hold all your accounts if you depend on the default coming first.

Retrieve one account when you need its current details:

curl https://api.arta.io/purchase_payout_accounts/dbefc07d-ad38-45b1-a82b-fb5d02042b9b \
  --header 'Authorization: ARTA_APIKey ulbZij4WHmZK4pC8xR2nQ6sT'
{
  "id": "dbefc07d-ad38-45b1-a82b-fb5d02042b9b",
  "name": "Reserve",
  "account_type": "checking",
  "account_holder_name": "Patton Gallery LLC",
  "routing_number": "026009593",
  "account_number_last_four": "4412",
  "is_default": false,
  "created_at": "2026-09-21T15:45:23.171826"
}

Arta does not return the full account number after the account is configured.

When you create a purchase request, omit purchase_payout_account_id to use the default account. Pass another account’s ID to direct that purchase’s payout to the other account. The account must belong to your organization.

Arta resolves and stores the selected account when it creates the purchase request. Changing the organization’s default later does not redirect an existing request or purchase.

Create a purchase request

Create one purchase request when the buyer selects Arta Pay. Make this request from your server with a private API key.

curl https://api.arta.io/purchase_requests \
  --request POST \
  --header 'Authorization: ARTA_APIKey ulbZij4WHmZK4pC8xR2nQ6sT' \
  --header 'Content-Type: application/json' \
  --data '{
    "purchase_request": {
      "public_reference": "HG-20260514-7421",
      "purchase_payout_account_id": "dbefc07d-ad38-45b1-a82b-fb5d02042b9b",
      "amount": {
        "subtotal": "11068.53",
        "shipping": "635.00",
        "tax": "165.92",
        "total": "11869.45",
        "currency": "USD"
      },
      "buyer": {
        "email_address": "elena@mori.studio"
      },
      "line_items": [
        {
          "id": "lamp-1952",
          "name": "Hans-Agne Jakobsson Brass Floor Lamp",
          "quantity": 1,
          "unit_price": "11068.53",
          "images": [
            {
              "url": "https://patton-gallery.com/images/lamp-1952.jpg",
              "alt_text": "Brass floor lamp, front"
            }
          ]
        }
      ],
      "return_url": "https://patton-gallery.com/orders/HG-20260514-7421"
    }
  }'

Arta returns the new request and a short-lived browser token:

{
  "id": "56235020-d3c3-4709-bf09-7a20d3c33b8e",
  "public_reference": "HG-20260514-7421",
  "purchase_payout_account_id": "dbefc07d-ad38-45b1-a82b-fb5d02042b9b",
  "status": "created",
  "amount": {
    "subtotal": "11068.53",
    "shipping": "635.00",
    "tax": "165.92",
    "other_charges": "0.00",
    "total": "11869.45",
    "currency": "USD"
  },
  "buyer": {
    "email_address": "elena@mori.studio"
  },
  "line_items": [
    {
      "id": "lamp-1952",
      "name": "Hans-Agne Jakobsson Brass Floor Lamp",
      "quantity": 1,
      "unit_price": "11068.53",
      "total": "11068.53",
      "images": [
        {
          "id": "28ed93cc-ab57-4361-a5a7-62013a39b2f1",
          "url": null,
          "alt_text": "Brass floor lamp, front",
          "caption": null,
          "display_position": 0,
          "image_type": "work",
          "import_status": "pending"
        }
      ],
      "image_import_status": "pending",
      "image_url": null
    }
  ],
  "return_url": "https://patton-gallery.com/orders/HG-20260514-7421",
  "purchase_id": null,
  "client_token": "Jd8qM4vX2rP7sL9cT5nB3kF6",
  "client_token_expires_at": "2026-07-20T19:15:00.118442",
  "created_at": "2026-07-20T19:00:00.242185",
  "updated_at": "2026-07-20T19:00:00.242185"
}

The client token can open only this purchase request. Send the request ID and token to your browser. Keep the private API key on your server.

Arta returns client_token only when it creates the request. List and retrieve responses do not expose it.

Images are still importing when this response returns, so every image comes back with a null url and import_status of pending, and the line item’s image_url and image_import_status match. Read the request again for the hosted URLs. Do not hold up the checkout for them: the buyer can complete a purchase while images are still importing.

Put your own order reference in the return_url query string. The buyer comes back to that URL with no other context, so it is the only thing tying the return to an order in your system.

Token and request lifetimes

The create response reports the token’s expiry in client_token_expires_at, 15 minutes after Arta creates the request. If the token expires before the buyer opens the widget, create a new purchase request.

A request that does not reach confirmed expires 24 hours after creation and moves to the expired status. A request already in processing is never expired, because Arta is mid-confirmation on it.

Purchase request fields

  • public_reference — Optional. Your stable order or invoice ID.
  • purchase_payout_account_id — Optional. Payout account for this purchase; omit it to use your default account.
  • amount.subtotal — Required. Item subtotal as a decimal string.
  • amount.shipping — Required. Shipping amount; use 0.00 when none.
  • amount.tax — Required. Tax amount; use 0.00 when none.
  • amount.total — Required. Total the buyer will pay.
  • amount.currency — Required. Three-letter ISO currency code.
  • buyer.email_address — Optional. Email hint to prefill before Arta sign-in.
  • amount.other_charges — Optional. Any remaining charge that is not shipping or tax; defaults to 0.00 and is always returned.
  • line_items — Required. Items in the seller order.
  • return_url — Required. HTTPS page to show after checkout. Arta rejects plain HTTP, so a localhost store needs a tunnel or a separate HTTPS destination while you build.

Until a buyer signs in, the request echoes back exactly what you sent. Once a buyer authenticates, Arta replaces buyer.email_address on the request itself with the authenticated buyer’s address — not just on the purchase.

Line item fields

Each line item takes id, name, quantity, unit_price and an optional images array of {url, alt_text}, with at most six images per item.

Arta imports the images you supply and re-hosts its own resized copies. A source URL must be public http or https on port 80 or 443. Arta rejects localhost, loopback and private addresses when it validates the request, so while developing you either expose the images through a tunnel or omit them.

Arta imports JPEG and PNG only, up to 25 MB and 40 megapixels each. An image that fails to import keeps its place in the array with import_status set to failed, and the rest of the checkout is unaffected. A line item can carry a single image_url string instead of an images array, but sending both fails.

Responses return a richer images array — with Arta’s url, id, display_position, image_type and per-image import_status — plus a derived image_url for the first image and an image_import_status for the item. Read those fields; do not send them.

Responses also add a total per line item.

Treat the returned image URLs as opaque. Render them as given; do not construct, parse or cache their path structure, which is an Arta implementation detail and will change.

POST /purchase_requests does not read an Idempotency-Key header, and public_reference is not unique, so a retry after a network timeout creates a second request for the same order. Before you retry, list your purchase requests filtered on public_reference and reuse the one you already created. Only the test-mode endpoints take an idempotency key. They require it, and answer 422 {"code": "idempotency_key_required"} without one.

Arta validates the amounts on creation and rejects the request with 422 if they do not reconcile. subtotal + shipping + tax + other_charges must equal total, and the line items’ quantity times unit_price must equal the subtotal.

The failures come back as field errors:

{ "errors": { "base": ["order total mismatch"] } }
{ "errors": { "base": ["line item subtotal mismatch"] } }
{ "errors": { "base": ["line items required"] } }
{ "errors": { "base": ["invalid order"] } }
{ "errors": { "base": ["default payout account not found"] } }
{ "errors": { "base": ["payout account not found"] } }
{ "errors": { "return_url": ["is required"] } }
{ "errors": { "return_url": ["must be an https URL"] } }

default payout account not found is the one most first integrations hit. It means your organization has no payout account marked as the default in that mode, so either set one or name an account with purchase_payout_account_id.

A purchase request is immutable from creation, and no endpoint updates one. Create a new request if the order, the payout account or the return URL changes, and cancel the request you no longer need.

List purchase requests

List purchase requests when you need to reconcile incomplete or unsuccessful checkouts:

curl 'https://api.arta.io/purchase_requests?page=1&page_size=20' \
  --header 'Authorization: ARTA_APIKey ulbZij4WHmZK4pC8xR2nQ6sT'
{
  "items": [
    {
      "id": "56235020-d3c3-4709-bf09-7a20d3c33b8e",
      "public_reference": "HG-20260514-7421",
      "purchase_payout_account_id": "dbefc07d-ad38-45b1-a82b-fb5d02042b9b",
      "status": "processing",
      "amount": {
        "total": "11869.45",
        "currency": "USD",
        "subtotal": "11068.53",
        "shipping": "635.00",
        "tax": "165.92",
        "other_charges": "0.00"
      },
      "buyer": {
        "email_address": "elena@mori.studio"
      },
      "line_items": [ ],
      "return_url": "https://patton-gallery.com/orders/HG-20260514-7421",
      "purchase_id": null,
      "created_at": "2026-07-20T19:00:00.242185",
      "updated_at": "2026-07-20T19:04:31.062312"
    }
  ],
  "metadata": {
    "is_estimate": null,
    "page": 1,
    "page_size": 20,
    "total_count": 1
  }
}

The endpoint supports page and page_size. Results sort by created_at descending.

List items are the full purchase request, not a summary: each one carries the complete amount object and its line_items with imported images.

Purchase request statuses

  • created — The seller created the request. Purchase created: No.
  • in_progress — The buyer opened the widget. Purchase created: No.
  • processing — The buyer finished and Arta is confirming the transaction. Purchase created: No.
  • confirmed — Arta confirmed the transaction. Purchase created: Yes.
  • declined — Arta Pay could not complete the request. Purchase created: No.
  • cancelled — The buyer or seller cancelled the request. Purchase created: No.
  • expired — The request expired before confirmation. Purchase created: No.

Retrieve a purchase request

Retrieve the request after an SDK callback or webhook:

curl https://api.arta.io/purchase_requests/56235020-d3c3-4709-bf09-7a20d3c33b8e \
  --header 'Authorization: ARTA_APIKey ulbZij4WHmZK4pC8xR2nQ6sT'

A confirmed request includes the ID of the purchase Arta created:

{
  "id": "56235020-d3c3-4709-bf09-7a20d3c33b8e",
  "public_reference": "HG-20260514-7421",
  "purchase_payout_account_id": "dbefc07d-ad38-45b1-a82b-fb5d02042b9b",
  "status": "confirmed",
  "amount": {
    "subtotal": "11068.53",
    "shipping": "635.00",
    "tax": "165.92",
    "other_charges": "0.00",
    "total": "11869.45",
    "currency": "USD"
  },
  "buyer": {
    "email_address": "elena@mori.studio"
  },
  "line_items": [
    {
      "id": "lamp-1952",
      "name": "Hans-Agne Jakobsson Brass Floor Lamp",
      "quantity": 1,
      "unit_price": "11068.53",
      "total": "11068.53",
      "images": [
        {
          "id": "28ed93cc-ab57-4361-a5a7-62013a39b2f1",
          "url": "https://api.arta.io/purchase_images/400x400/resize/394fb2a9.jpeg",
          "alt_text": "Brass floor lamp, front",
          "caption": null,
          "display_position": 0,
          "image_type": "work",
          "import_status": "ready"
        }
      ],
      "image_import_status": "ready",
      "image_url": "https://api.arta.io/purchase_images/400x400/resize/394fb2a9.jpeg"
    }
  ],
  "return_url": "https://patton-gallery.com/orders/HG-20260514-7421",
  "purchase_id": "51370fa3-b10a-4df3-93ef-9809a3baa0c9",
  "created_at": "2026-07-20T19:00:00.242185",
  "updated_at": "2026-07-20T19:04:31.062312"
}

Treat the request as fulfillment-safe only when status is confirmed and purchase_id is present.

Cancel a purchase request

Cancel a request when the buyer abandons the order or your order changes before confirmation:

curl https://api.arta.io/purchase_requests/56235020-d3c3-4709-bf09-7a20d3c33b8e/cancel \
  --request POST \
  --header 'Authorization: ARTA_APIKey ulbZij4WHmZK4pC8xR2nQ6sT'

Arta returns the request with status cancelled. You can cancel a request in the created, in_progress or processing status. A processing cancellation can fail with a 422 error if Arta has already confirmed the transaction; retrieve the request and handle the confirmed purchase instead.

You cannot cancel a confirmed request. Contact Arta to reverse a purchase before fulfillment.

Open the Arta Pay checkout

Create a checkout instance with the request ID and client token:

let confirmedOrder = null;

const checkout = Arta.pay(
  {
    purchaseRequestId: '56235020-d3c3-4709-bf09-7a20d3c33b8e',
    clientToken: 'Jd8qM4vX2rP7sL9cT5nB3kF6'
  },
  {
    onReady() {
      document.querySelector('#arta-pay-button').disabled = false;
    },

    // Fires while the buyer is still looking at the outcome screen, so
    // confirm here but do not navigate yet.
    async onComplete(result) {
      const response = await fetch('/api/orders/HG-20260514-7421/artapay-confirm', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ purchaseRequestId: result.purchaseRequestId })
      });

      confirmedOrder = await response.json();
    },

    // The buyer has dismissed the modal. Now move them on.
    onClose(event) {
      if (!confirmedOrder) {
        if (event.reason === 'customer') restorePaymentOptions();
        return;
      }

      if (confirmedOrder.paymentStatus === 'confirmed') {
        window.location.assign('/orders/HG-20260514-7421');
      } else {
        showArtaPayProcessingState();
      }
    },

    onError(error) {
      showArtaPayError(error.code, error.recoverable);
    }
  }
);

document.querySelector('#arta-pay-button').addEventListener('click', () => {
  checkout.open();
});

The SDK opens an Arta-hosted modal over your checkout. Arta handles sign-in, credit-line checks, deposit selection and payment details.

Arta.pay takes an optional third argument, {position}, which accepts center (the default), left, right or full_screen.

Constructing the instance mounts the widget immediately but hidden, so it can validate the purchase request before the buyer sees anything. That is why onReady — not construction — is the signal to enable your button.

The instance provides open(), close() and destroy() methods. close() hides the modal without tearing down the frame, so reopening resumes where the buyer left off; destroy() removes it for good. Closing the modal does not cancel a confirmed request.

onComplete fires at most once per instance, the moment the outcome screen renders — which is before the buyer dismisses the modal. Start your server-side confirmation there, but wait for onClose before navigating away, or you will pull the widget out from under the buyer mid-success.

Handle SDK callbacks

Callbacks update your page while the buyer is present. They are not proof of purchase.

type ArtaPayCompletion = {
  purchaseRequestId: string;
  purchaseId?: string;
  status: 'confirmed' | 'processing' | 'declined';
};

type ArtaPayCloseEvent = {
  purchaseRequestId: string;
  reason: 'customer' | 'complete' | 'error';
};

type ArtaPayError = {
  code: string;
  message: string;
  recoverable: boolean;
  requestId?: string;
};
  • onReady — Called when the token is valid and the widget can open. Enable your Arta Pay button.
  • onComplete — Called when the interactive flow reaches an outcome. Ask your server to retrieve the request.
  • onClose — Called when the modal closes. Restore your checkout UI if needed.
  • onError — Called when the SDK, token or network fails. Show a recoverable error or another payment method.

onComplete can return confirmed, processing or declined.

A callback error does not reverse or cancel a transaction.

Confirm the result on your server

When onComplete runs, send the purchase request ID to your server. Your server should retrieve the request with its private key.

If the request is confirmed, store its purchase_id and mark your order paid with Arta Pay. Make this update safe to apply more than once because a webhook may run at the same time.

If the request is processing, show a pending order page and wait for a webhook. Do not ask the buyer to pay again while Arta is deciding the request.

If the request is declined, let the buyer select another payment method.

Receive purchase webhooks

Webhooks notify your server when a request, purchase or payout changes. Use them even if you retrieve the request after onComplete. The buyer may close the browser before the callback runs.

Arta Pay sends these events:

  • purchase_request.status.updated — Sent when a purchase request changes status.
  • purchase.created — Sent when Arta creates a confirmed purchase.
  • purchase.status.updated — Sent when a purchase changes status.
  • purchase.payout.updated — Sent when the seller payout changes status.
  • ping — Sent when you trigger a test delivery from the dashboard or POST /webhooks/{id}/ping.
{
  "object": "purchase",
  "type": "purchase.status.updated",
  "data": {
    "id": "51370fa3-b10a-4df3-93ef-9809a3baa0c9",
    "purchase_request_id": "56235020-d3c3-4709-bf09-7a20d3c33b8e",
    "purchase_payout_account_id": "dbefc07d-ad38-45b1-a82b-fb5d02042b9b",
    "public_reference": "HG-20260514-7421",
    "status": "confirmed",
    "payout_status": "pending",
    "updated_at": "2026-07-20T19:04:31.062312Z"
  }
}

purchase.created and purchase.payout.updated carry the same data shape. purchase_request.status.updated carries a smaller one: id, public_reference, status, purchase_id and updated_at, where id is the purchase request’s own ID.

Arta signs the exact request body and sends these headers:

ARTA-SIGNATURE: t=1784574272,s=<base64-hmac-sha256>
ARTA-WEBHOOK-ID: 1842
Content-Type: application/json; charset=utf-8

ARTA-WEBHOOK-ID identifies your Arta webhook configuration.

Verify the signature before parsing or acting on the event:

  1. Read the raw request body.
  2. Parse t and s from ARTA-SIGNATURE.
  3. Reject timestamps outside your allowed replay window.
  4. Join the timestamp, a period and the raw body: <timestamp>.<raw-body>.
  5. Calculate a Base64-encoded HMAC SHA-256 with your webhook secret.
  6. Compare your signature with s using a constant-time comparison.

Return a 2xx response promptly. Treat the webhook as a notification and retrieve the current resource before changing your order. Arta may retry failed deliveries, so applying the same resource state more than once must be safe.

Do not rely on webhook delivery order.

You can manage webhook endpoints through the existing customer context API:

  • POST /webhooks — Create a webhook endpoint.
  • POST /webhooks/{id}/ping — Send a test event.
  • GET /webhooks/{id}/secret_token — Retrieve its signing secret.
  • PATCH /webhooks/{id}/secret_token/reset — Rotate its signing secret.
  • GET /webhook_deliveries — Inspect recent deliveries.

List purchases

The purchases endpoint returns confirmed financing obligations. It does not return abandoned, declined or expired purchase requests.

curl 'https://api.arta.io/purchases?page=1&page_size=20' \
  --header 'Authorization: ARTA_APIKey ulbZij4WHmZK4pC8xR2nQ6sT'
{
  "items": [
    {
      "id": "51370fa3-b10a-4df3-93ef-9809a3baa0c9",
  "purchase_request_id": "56235020-d3c3-4709-bf09-7a20d3c33b8e",
  "purchase_payout_account_id": "dbefc07d-ad38-45b1-a82b-fb5d02042b9b",
  "public_reference": "HG-20260514-7421",
  "status": "confirmed",
  "buyer": {
    "name": "Elena Mori",
    "email_address": "elena@mori.studio"
  },
  "amount": {
    "total": "11869.45",
    "currency": "USD",
    "deposit": "6534.20",
    "deposit_currency": "USD",
    "financed": "5335.25",
    "financed_currency": "USD"
  },
  "payout": {
    "id": "c9885820-60b5-4eb0-8ece-499e297e88bd",
    "status": "paid",
    "amount": "11869.45",
    "currency": "USD",
    "reference": "TRF-88204117",
    "account": {
      "id": "dbefc07d-ad38-45b1-a82b-fb5d02042b9b",
      "name": "Reserve",
      "type": "checking",
      "last_four": "4412"
    },
    "submitted_at": "2026-07-22T14:05:44.084812",
    "settled_at": "2026-07-22T14:12:09.526227",
    "returned_at": null,
    "failure_code": null,
    "failure_reason": null,
    "transfer_status": "settled",
    "updated_at": "2026-07-22T14:12:09.527422"
  },
  "payout_status": "paid",
  "created_at": "2026-07-20T19:04:31.062312",
  "updated_at": "2026-07-22T14:12:09.527422"
    }
  ],
  "metadata": {
    "is_estimate": null,
    "page": 1,
    "page_size": 20,
    "total_count": 1
  }
}

The endpoint supports page and page_size. Results sort by created_at descending.

Retrieve a purchase

Retrieve the purchase when you need its confirmed amounts, items or payout:

curl https://api.arta.io/purchases/51370fa3-b10a-4df3-93ef-9809a3baa0c9 \
  --header 'Authorization: ARTA_APIKey ulbZij4WHmZK4pC8xR2nQ6sT'
{
  "id": "51370fa3-b10a-4df3-93ef-9809a3baa0c9",
  "purchase_request_id": "56235020-d3c3-4709-bf09-7a20d3c33b8e",
  "purchase_payout_account_id": "dbefc07d-ad38-45b1-a82b-fb5d02042b9b",
  "public_reference": "HG-20260514-7421",
  "status": "confirmed",
  "buyer": {
    "name": "Elena Mori",
    "email_address": "elena@mori.studio"
  },
  "amount": {
    "total": "11869.45",
    "currency": "USD",
    "deposit": "6534.20",
    "deposit_currency": "USD",
    "financed": "5335.25",
    "financed_currency": "USD"
  },
  "payout": {
    "id": "c9885820-60b5-4eb0-8ece-499e297e88bd",
    "status": "paid",
    "amount": "11869.45",
    "currency": "USD",
    "reference": "TRF-88204117",
    "account": {
      "id": "dbefc07d-ad38-45b1-a82b-fb5d02042b9b",
      "name": "Reserve",
      "type": "checking",
      "last_four": "4412"
    },
    "submitted_at": "2026-07-22T14:05:44.084812",
    "settled_at": "2026-07-22T14:12:09.526227",
    "returned_at": null,
    "failure_code": null,
    "failure_reason": null,
    "transfer_status": "settled",
    "updated_at": "2026-07-22T14:12:09.527422"
  },
  "payout_status": "paid",
  "created_at": "2026-07-20T19:04:31.062312",
  "updated_at": "2026-07-22T14:12:09.527422"
}

The purchase exposes the authenticated buyer’s name and email so you can fulfill the order and provide support. The seller API does not expose the buyer’s credit limit, application data, payment method or repayment history.

Its buyer and financing fields come from Arta’s completed checkout, not from the buyer hint on the purchase request.

The purchase reports the financing split, not the order breakdown. amount carries total, deposit and financed — each with its own currency field — and does not repeat subtotal, shipping or tax. The purchase also carries no line_items and no description. Retrieve the linked purchase request when you need the confirmed item detail or the amount breakdown.

deposit is the amount the buyer actually paid up front, not the minimum, so do not infer it from the total. The minimum itself moves: it is the greater of 10% of the total and the part of the total the buyer’s available credit does not cover. A buyer who is short on credit has to put down more than 10%.

Purchase statuses

  • confirmed — Arta created the financing obligation. Seller action: Fulfill the order.

confirmed is the only purchase status the API returns. If Arta reverses a purchase before fulfillment, that purchase stops being retrievable: GET /purchases/{id} answers 404 and the purchase leaves the list. You learn about the reversal from the purchase request, which moves to cancelled and sends a purchase_request.status.updated webhook. Treat that as the signal to release inventory and stop fulfillment.

Refunds, disputes and cancellation after fulfillment are out of scope for the initial release. Arta handles them with you directly and will specify API support separately.

Payout statuses

Purchase and payout states remain separate. A confirmed purchase stays confirmed while its payout advances.

  • not_started — Arta has not created the payout. Seller action: No action required.
  • pending — The payout is queued. Seller action: No action required.
  • in_progress — The payout is moving to your account. Seller action: No action required.
  • paid — The payout settled. Seller action: Reconcile the payout.
  • failed — The payout failed, or the receiving bank returned it after it settled. Seller action: Follow the instructions from Arta.
  • cancelled — Arta cancelled the payout. Seller action: Follow the instructions from Arta.

The payout object also carries transfer_status, submitted_at, settled_at, returned_at, failure_code and failure_reason. Read status for the seller-facing state and treat the rest as detail for reconciliation and support.

A return and an outright failure both read as failed, so status alone does not separate them. A return sets returned_at and puts the ACH return code in failure_code. A failure leaves returned_at null and reports payout_failed or transfer_failed.

Show Arta Pay before checkout

You can present Arta Pay next to a price so a buyer knows financing is available before they reach checkout. The SDK gives you two pieces for this, and neither needs a purchase request.

The monthly figure

getArtaPayMonthlyEstimate returns the “from $X/mo” figure for a price. It is pure client-side arithmetic — no init, no network:

const { amountPerMonth, amountPerMonthCurrency } =
  Arta.getArtaPayMonthlyEstimate({ totalPrice: 11869.45 });

priceNote.textContent = `From $${amountPerMonth}/mo with Arta Pay`;

totalPrice is a number in major units, not cents. The result is whole currency units, rounded up.

Take this figure from the SDK. Do not compute your own: the terms are Arta’s to change, and a partner-calculated payment that disagrees with the checkout is a compliance problem, not a rounding difference. The estimate assumes a 10% deposit and Arta’s standard term, so treat it as a floor. The buyer’s own schedule comes from the deposit they choose at checkout, and a buyer who is short on credit has to deposit more than 10%.

The informational modal

openArtaPayInfoModal opens an Arta-hosted modal explaining how Arta Pay works. It needs only the public key from Arta.init:

// With a price, the modal shows a financing estimate for it.
Arta.openArtaPayInfoModal({ totalPrice: 11869.45 });

// Without one, it shows the plain marketing modal.
Arta.openArtaPayInfoModal();

The call opens the modal and returns the instance, which provides open(), close() and destroy(). It takes no callbacks: the modal is informational and reports no outcome. Destroy the instance when you tear down the page around it.

Build against test mode

Test mode gives you the whole lifecycle without a real buyer, a real credit line or real money. The widget supplies a fictional collector and payment methods, and you drive the outcome.

Your API keys select the mode. Nothing else does — not payOrigin, not a flag on the request. A test key produces test records; a live key produces real ones.

Check which mode a key is in

The public key validation endpoint takes your public key and reports its mode. It is the only check you can run from the browser:

curl https://api.arta.io/arta_pay/keys/validate \
  --header 'Authorization: ARTA_APIKey fpFJOll1kQ7mN3vB8sD2xR5T'
{ "mode": "test", "valid": true, "seller_name": "Patton Gallery" }

For a private key, use test-helper access as the signal: a test key can reach /arta_pay/test_helpers/*, and a live key is refused with 403. That is the reliable way for your server to catch a live key sitting in a staging environment.

Choose a buyer scenario

List the scenarios Arta can simulate:

curl https://api.arta.io/arta_pay/test_helpers/scenarios \
  --header 'Authorization: ARTA_APIKey ulbZij4WHmZK4pC8xR2nQ6sT'

Each scenario has an id, a name and a version:

  • debit_success — Successful debit deposit.
  • credit_success — Successful credit card deposit.
  • ach_pending — Bank deposit awaiting settlement; the request sits in processing.
  • deposit_failure — Failed deposit, then a successful retry.
  • no_credit_line — Buyer without a credit line.
  • limited_credit — Buyer with $1,000 available credit.
  • identity_required — Identity verification required.
  • no_payment_method — Payment method required.
  • declined — Checkout declined.

Select one after creating the request and before opening the widget:

curl https://api.arta.io/arta_pay/test_helpers/purchase_requests/56235020-d3c3-4709-bf09-7a20d3c33b8e/scenario \
  --request PUT \
  --header 'Authorization: ARTA_APIKey ulbZij4WHmZK4pC8xR2nQ6sT' \
  --header 'Content-Type: application/json' \
  --data '{ "scenario": "ach_pending" }'

This write requires an Idempotency-Key header; without one it fails with 422 {"code": "idempotency_key_required"}.

A newly created request already has a scenario assigned before you choose one, so a request you never configured still runs a simulation. Set the scenario explicitly rather than relying on the default.

If the selection fails, treat the checkout as unusable and create a new request. A request that opens without the scenario you intended is running a different simulation than the one on your screen.

Use ach_pending to exercise the case sellers get wrong most often: a checkout that succeeds into processing and confirms minutes or days later. An integration that treats anything other than confirmed as a failure will be wrong for every bank deposit.

Create a fictional payout account

If the organization has no payout account, make one:

curl https://api.arta.io/arta_pay/test_helpers/payout_accounts \
  --request POST \
  --header 'Authorization: ARTA_APIKey ulbZij4WHmZK4pC8xR2nQ6sT' \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: create-test-payout-1' \
  --data '{ "name": "Test payout" }'

The account belongs to your organization and appears in its test dashboard.

Inspect a simulated checkout

Retrieve the simulation record to see where a request stands and what you can do to it next:

curl https://api.arta.io/arta_pay/test_helpers/purchase_requests/56235020-d3c3-4709-bf09-7a20d3c33b8e \
  --header 'Authorization: ARTA_APIKey ulbZij4WHmZK4pC8xR2nQ6sT'
{
  "purchase_request_id": "56235020-d3c3-4709-bf09-7a20d3c33b8e",
  "public_reference": "HG-20260514-7421",
  "status": "confirmed",
  "scenario": "debit_success",
  "scenario_version": 1,
  "purchase_id": "51370fa3-b10a-4df3-93ef-9809a3baa0c9",
  "actions": ["return_payout"],
  "purchase": { },
  "transfers": [ ],
  "history": [
    {
      "id": "1f3b3cf5-2413-4c51-90e5-cd029772a948",
      "actor": "widget:ccfe7e63-cb90-4382-b317-f0929a8af770",
      "created_at": "2026-07-20T19:04:31.092474"
    }
  ],
  "started_at": "2026-07-20T19:00:16.854102"
}

history records who moved the record — widget: and the run ID for the buyer flow, api_key: and the key ID for an action your server triggered, and user: and the user ID for one taken from the Sellers Dashboard.

Drive the lifecycle

actions lists the transitions that are available right now. It is state-dependent: a paid purchase may offer only return_payout, and a declined request offers nothing at all. Read the list before acting; do not hard-code the buttons in your tooling.

Apply one through the transformations endpoint:

curl https://api.arta.io/test_mode_transformations \
  --request POST \
  --header 'Authorization: ARTA_APIKey ulbZij4WHmZK4pC8xR2nQ6sT' \
  --header 'Content-Type: application/json' \
  --data '{
    "resource_type": "purchase",
    "resource_id": "51370fa3-b10a-4df3-93ef-9809a3baa0c9",
    "transformation_id": "settle_deposit"
  }'

Use resource_type: "purchase" with the purchase ID for deposit and payout transitions, and resource_type: "purchase_request" with the request ID to expire a request.

A transformation answers 204 with no body. Retrieve the request or the purchase to see the new state.

Test seller checkouts never expire on their own, because the scheduled expirer skips them. The expire transformation is the only way to see an expired request in test mode.

Each transformation fires the same webhooks as the real event, so this is how you test your webhook handling end to end.

What test mode will not do

Test mode does not move money and does not create a real credit obligation. It also cannot prove your two keys belong to the same organization: /arta_pay/keys/validate confirms the public key’s mode and seller, but the pair is only settled when the widget exchanges the client token at checkout.

Protect the integration

Follow these requirements in production:

  • keep private API keys on your server
  • create purchase amounts on your server
  • allow only HTTPS return and webhook URLs
  • register the origins that may embed Arta Pay in your Sellers Dashboard settings (proposed; not enforced yet)
  • use a short-lived client token for each request
  • confirm a purchase from your server before fulfillment
  • use the buyer’s name and email for fulfillment and support only
  • verify every webhook signature against the raw body
  • reject old webhook timestamps to prevent replay
  • make callback and webhook processing safe to repeat
  • log Arta request IDs without logging tokens or buyer financial data
  • load the SDK from npm or Arta’s documented CDN URL

The SDK uses a cross-origin Arta frame for the buyer flow. It validates the frame origin and message source before invoking callbacks. Do not add your own message listener for the widget.