> For the complete documentation index, see [llms.txt](https://api.shopwaive.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://api.shopwaive.com/quick-start.md).

# Quick Start

With this Quick Start guide, make your first request in just minutes

{% hint style="info" %}
**Tip:** Shopwaive's quick start guide helps developers setup their first request in minutes. Use this guide to create an API key and make your first request.
{% endhint %}

## Get your API keys

Every API request is authenticated with a Shopwaive API key, sent in the `X-Shopwaive-Access-Token` header. Any request that doesn't include a valid key will return an error.

To create a key:

{% stepper %}
{% step %}
**Open Developers**

In your Shopify admin, open the **Shopwaive** app. Click **Settings**, then **Developers**.
{% endstep %}

{% step %}
**Create a key**

Under **Shopwaive API keys**, type a label (for example `ERP sync`). If your integration changes balances (a deposit, a set, a void or an order), tick **Change balances through the REST API (wallets:write)**. Then click **Create key**.
{% endstep %}

{% step %}
**Copy the key**

Click **Copy** and keep the key somewhere safe. The full key is shown only once.
{% endstep %}
{% endstepper %}

A key starts with `swak_`. Only admin users of the Shopwaive app can see and create keys. Every key can read balances, so the request in this guide needs no box under **What the new key can do**. A request that changes a balance needs **Change balances through the REST API (wallets:write)**. The other boxes are for the [Loyalty API](/reference/rest-api-documentation/loyalty-api.md).

One key is enough. You can create more, for example one for each developer or integration. To stop a key from working, select it in the list and click **Revoke**. Your other keys are not affected. A lost key cannot be shown again: revoke it and create a new one.

{% hint style="warning" %}
**Still sending an old access token?** Shopify now gives apps access tokens that expire. The old store token stops working as an API credential. Create a key and send it in place of the old token. With a key, every request that changes something also needs an `Idempotency-Key` header: see [Change a balance](#change-a-balance). [Read why](/api-keys-and-shopify-expiring-tokens.md)
{% endhint %}

**Good to know:** API keys should be handled with care and never exposed in unsecured environments. They should never be shared as they provide access to your store's data. Never put a key in your theme or in any code that runs in a browser. Anyone can read it there.

### If a key is refused

A `swak_` key that is refused returns HTTP `401`. The message names the reason:

```json
{
  "status": "failed",
  "message": "Invalid X-Shopwaive-Access-Token: key-revoked."
}
```

| Reason                    | What it means                                                                                 |
| ------------------------- | --------------------------------------------------------------------------------------------- |
| `not-a-shopwaive-api-key` | The value starts with `swak_` but is not a complete key. Check that you pasted the whole key. |
| `key-not-recognised`      | No store has this key, for example because it was deleted.                                    |
| `key-revoked`             | The key was revoked. Create a new one.                                                        |
| `store-has-no-token`      | Contact <support@shopwaive.com>.                                                              |

**A paused key.** While Shopwaive is uninstalled from the store, the store's keys are paused, not revoked. A paused key returns HTTP `401` with the reason `app_uninstalled` in its own field, and it works again by itself when the app is reinstalled:

{% code overflow="wrap" %}

```json
{
  "status": "failed",
  "reason": "app_uninstalled",
  "message": "This API key is paused because the Shopwaive app is not installed on your-store.myshopify.com. The key has not been revoked: it works again automatically when the app is reinstalled."
}
```

{% endcode %}

**A key that cannot change balances.** A request that changes a balance (a deposit, a set, a void or an order) or starts a workflow (`POST /api/trigger`) needs a key with **Change balances through the REST API (wallets:write)**. A key without it returns HTTP `403` with the reason `missing_scope`, and nothing is changed. Create a key with the box ticked:

{% code overflow="wrap" %}

```json
{
  "status": "failed",
  "reason": "missing_scope",
  "scope": "wallets:write",
  "message": "This API key cannot change balances: it does not have the wallets:write scope. To change balances, create a key with Change balances through the REST API (wallets:write) ticked, in the Shopwaive app under Settings > Develop."
}
```

{% endcode %}

HTTP `503` means the key could not be checked. Retry the request.

No key, or a token Shopwaive does not know, returns HTTP `401` with `"error": "unknown_api_token"` and a message that links [API keys and Shopify expiring tokens](/api-keys-and-shopify-expiring-tokens.md).

## Make your first request

To make your first request, send an authenticated request to the get customer endpoint. This will fetch a `customer` by email address. Ensure to include the `X-Shopwaive-Access-Token` and `X-Shopwaive-Platform` headers.

## Get a customer's balance

> Returns the customer's balance. Add \`activity=true\`, \`orders=true\` or \`offers=true\` to include more.\
> \
> A customer with no Shopwaive balance yet returns HTTP 200 with \`balance\` 0 and a \`status\` that says the customer does not exist.<br>

```json
{"openapi":"3.1.0","info":{"title":"Shopwaive API","version":"2026-11"},"tags":[{"name":"Customer API","description":"Read a customer's balance, set it, or add to it and take from it."}],"servers":[{"url":"https://app.shopwaive.com","description":"Shopwaive API"}],"security":[{"ShopwaiveApiKey":[]}],"components":{"securitySchemes":{"ShopwaiveApiKey":{"type":"apiKey","in":"header","name":"X-Shopwaive-Access-Token","description":"Your Shopwaive API key. Create it in the Shopwaive app under Settings > Develop > Shopwaive API keys. It starts with swak_."}},"parameters":{"Platform":{"name":"X-Shopwaive-Platform","in":"header","required":true,"description":"Always `shopify`.","schema":{"type":"string","enum":["shopify"],"default":"shopify"}}},"headers":{"RequestId":{"description":"This answer's request id, 16 hexadecimal characters. Every answer carries it. Quote it when you contact support.","schema":{"type":"string","pattern":"^[0-9a-f]{16}$"}},"RateLimitLimit":{"description":"Requests allowed a second on this route.","schema":{"type":"integer"}},"RateLimitRemaining":{"description":"Requests left in the current second.","schema":{"type":"integer"}},"RetryAfter":{"description":"Seconds to wait before you retry.","schema":{"type":"integer"}}},"schemas":{"CustomerBalance":{"type":"object","properties":{"status":{"type":"string","const":"success"},"customerid":{"description":"The customer's Shopify id.","type":["string","number"]},"email":{"type":"string"},"balance":{"description":"The balance, rounded to 2 decimals.","type":"number"},"currency":{"description":"The currency code of the balance (or of `isocode` when it was converted).","type":"string"},"expirationdate":{"description":"When the whole balance expires, if `expires` is `true`.","type":"string"},"expires":{"$ref":"#/components/schemas/Expires"},"activity":{"description":"With `activity=true`. Every entry of the balance.","type":"array","items":{"$ref":"#/components/schemas/Entry"}},"orders":{"description":"With `orders=true`. The orders saved with the balance.","type":"array","items":{"type":"object","additionalProperties":true}},"offers":{"description":"With `offers=true`. The customer's Budgets that can still be spent.","type":"object","properties":{"total":{"description":"The sum of what can still be spent.","type":"number"},"items":{"type":"array","items":{"$ref":"#/components/schemas/Entry"}}}},"referralurl":{"description":"The customer's referral link, when the store's referral program is on.","type":"string"}}},"Expires":{"description":"`true` when the amount expires on `expirationdate`, else `false`. Sent and stored as text.","type":["string","boolean"]},"Entry":{"description":"One entry (action) of a balance, as Shopwaive stores it. Which fields are present depends on how the entry was made.","type":"object","additionalProperties":true,"properties":{"id":{"description":"The entry's id.","type":["string","number"]},"date":{"description":"When the entry was made or last changed.","type":"string"},"note":{"type":"string"},"type":{"description":"How the entry was made, for example `rest_api`.","type":"string"},"amount":{"description":"The amount the entry added (negative when it took).","type":["number","string"]},"expirationdate":{"type":"string"},"expires":{"$ref":"#/components/schemas/Expires"},"status":{"description":"For example `expired` or `redeemed`.","type":"string"},"media":{"type":"string"},"orderid":{"description":"The order of the latest spend from this entry.","type":["string","number"]},"lineitemid":{"description":"The order line that made this entry.","type":["string","number"]},"segment_query":{"type":"string"},"collectionids":{"type":"string"},"productids":{"type":"string"},"remaining":{"description":"What is left of the entry after earlier orders.","type":["number","string"]},"partial_amount":{"description":"What can be spent now, when the balance is lower than `remaining`.","type":["number","string"]},"validity":{"description":"Action API only. How long until the amount expires, in words: for example `expires in less than 3 months`, `expires in less than 24 hours`, `expired`, or `does not expire`.","type":"string"},"message":{"description":"Action API only. A note about this entry, for example why it does not expire or why it was not changed.","type":"string"},"transaction":{"description":"Not reliable. Do not use.","deprecated":true,"type":["string","number"]}}},"CustomerNotFound":{"type":"object","properties":{"status":{"type":"string","const":"Customer does not exist. Check case, all customers require lowercase"},"email":{"type":"string"},"balance":{"type":"number","const":0}}},"PlatformRefusedGet":{"description":"The X-Shopwaive-Platform header is missing or is not `shopify` (GET customer answers this with HTTP 200).","type":"object","properties":{"status":{"type":"string","const":"failed"},"message":{"type":"string","const":"Shopwaive API currently only supports stores on Shopify. Invalid X-Shopwaive-Platform header"}}},"KeyRefused":{"description":"A value that starts with swak_ was not accepted.","type":"object","properties":{"status":{"type":"string","const":"failed"},"reason":{"description":"Set only for a paused key (app_uninstalled: HTTP 401; the store uninstalled Shopwaive, and the key works again when the app is reinstalled) and for a key without the scope the request needs (missing_scope: HTTP 403).","type":"string","enum":["app_uninstalled","missing_scope"]},"scope":{"description":"With missing_scope. The scope the request needs, wallets:write.","type":"string"},"message":{"description":"Without reason: Invalid X-Shopwaive-Access-Token: followed by the reason: not-a-shopwaive-api-key, key-not-recognised, key-revoked or store-has-no-token. With reason: a sentence that says what to do.","type":"string"}}},"UnknownToken":{"description":"No key was sent, or Shopwaive does not know the token.","type":"object","properties":{"error":{"type":"string","const":"unknown_api_token"},"message":{"type":"string"}}},"RateLimited":{"type":"object","properties":{"error":{"type":"string"}}},"TemporarilyUnavailable":{"type":"object","properties":{"status":{"type":"string","const":"failed_temporary"},"message":{"type":"string"}}},"KeyCheckUnavailable":{"description":"A swak_ key could not be checked just now.","type":"object","properties":{"status":{"type":"string","const":"failed"},"message":{"type":"string","const":"Shopwaive API key lookup is temporarily unavailable. Retry."}}}},"responses":{"Unauthorized":{"description":"The API key was not accepted. A value that starts with `swak_` gets `status` `failed` and a `message` that names the reason. A key of a store that uninstalled Shopwaive is paused, not revoked: it gets `reason` `app_uninstalled` and works again when the app is reinstalled. No key, or a token Shopwaive does not know, gets `unknown_api_token`.\n","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/KeyRefused"},{"$ref":"#/components/schemas/UnknownToken"}]}}}},"RateLimited":{"description":"Too many requests. Wait the seconds in Retry-After.","headers":{"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimited"}}}},"CredentialCheckUnavailable":{"description":"The key could not be checked just now. Nothing was changed. Retry.","headers":{"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/TemporarilyUnavailable"},{"$ref":"#/components/schemas/KeyCheckUnavailable"}]}}}}}},"paths":{"/api/customer/{customer_email}":{"get":{"tags":["Customer API"],"operationId":"getCustomer","summary":"Get a customer's balance","description":"Returns the customer's balance. Add `activity=true`, `orders=true` or `offers=true` to include more.\n\nA customer with no Shopwaive balance yet returns HTTP 200 with `balance` 0 and a `status` that says the customer does not exist.\n","parameters":[{"name":"customer_email","in":"path","required":true,"description":"The customer's email address. Shopwaive reads it in lower case.","schema":{"type":"string","format":"email"}},{"name":"activity","in":"query","required":false,"description":"`true` adds `activity`: every entry of the balance.","schema":{"type":"string","enum":["true"]}},{"name":"orders","in":"query","required":false,"description":"`true` adds `orders`: the orders saved with the balance.","schema":{"type":"string","enum":["true"]}},{"name":"offers","in":"query","required":false,"description":"`true` adds `offers`: the customer's Budgets that can still be spent, with their total.","schema":{"type":"string","enum":["true"]}},{"name":"isocode","in":"query","required":false,"description":"A currency code, for example `EUR`. When it is not the store's currency, `balance` (and the amounts in `activity` and `offers`) are converted at Shopify's published exchange rate, and `currency` is that code. For a code Shopify does not list, the amounts are not converted and `currency` reads `Requested isocode currency not found`.\n","schema":{"type":"string"}},{"$ref":"#/components/parameters/Platform"}],"responses":{"200":{"description":"The customer's balance. Also HTTP 200 when the customer has no balance yet, and when the X-Shopwaive-Platform header is not `shopify`.","headers":{"X-Shopwaive-Request-Id":{"$ref":"#/components/headers/RequestId"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/CustomerBalance"},{"$ref":"#/components/schemas/CustomerNotFound"},{"$ref":"#/components/schemas/PlatformRefusedGet"}]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"405":{"description":"The email address is not valid. The body is the plain text `Method Not Allowed`.","content":{"text/plain":{"schema":{"type":"string","const":"Method Not Allowed"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"$ref":"#/components/responses/CredentialCheckUnavailable"}}}}}}
```

Press **Test it** on the request above, enter your key for `X-Shopwaive-Access-Token`, and send it. See [Test it with your own key](/reference/rest-api-documentation.md#test-it-with-your-own-key).

The tabs below show the same request in cURL, Node, PHP (Guzzle) and Python. Every example sends this header:

```
X-Shopwaive-Access-Token: swak_EXAMPLEKEY000000000000000000000000000000
```

`swak_EXAMPLEKEY000000000000000000000000000000` is an example, not a real key. Replace it with your own key. In your own code, read the key from an environment variable or a secrets store. Never commit a key to a repository.

{% tabs %}
{% tab title="cURL" %}

```bash
curl --location 'https://app.shopwaive.com/api/customer/customer@example.com' \
--header 'X-Shopwaive-Access-Token: swak_EXAMPLEKEY000000000000000000000000000000' \
--header 'X-Shopwaive-Platform: shopify' \
--header 'Content-Type: application/json'
```

{% endtab %}

{% tab title="Node" %}

```javascript
// Node 18 or later: fetch is built in.
const email = 'customer@example.com';

fetch(`https://app.shopwaive.com/api/customer/${encodeURIComponent(email)}`, {
  method: 'GET',
  headers: {
    'X-Shopwaive-Access-Token': 'swak_EXAMPLEKEY000000000000000000000000000000',
    'X-Shopwaive-Platform': 'shopify',
    'Content-Type': 'application/json',
  },
})
  .then(async (response) => {
    console.log(response.status, await response.json());
  })
  .catch((error) => console.error(error));
```

{% endtab %}

{% tab title="PHP (Guzzle)" %}

```php
<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';

$client = new GuzzleHttp\Client(['base_uri' => 'https://app.shopwaive.com']);

$response = $client->request('GET', '/api/customer/' . rawurlencode('customer@example.com'), [
    'headers' => [
        'X-Shopwaive-Access-Token' => 'swak_EXAMPLEKEY000000000000000000000000000000',
        'X-Shopwaive-Platform' => 'shopify',
        'Content-Type' => 'application/json',
    ],
    'http_errors' => false,
]);

echo $response->getStatusCode() . PHP_EOL;
echo $response->getBody() . PHP_EOL;
```

{% endtab %}

{% tab title="Python" %}

```python
import http.client
import json

conn = http.client.HTTPSConnection("app.shopwaive.com")
payload = ''
headers = {
  'X-Shopwaive-Access-Token': 'swak_EXAMPLEKEY000000000000000000000000000000',
  'X-Shopwaive-Platform': 'shopify',
  'Content-Type': 'application/json'
}
conn.request("GET", "/api/customer/customer@example.com", payload, headers)
res = conn.getresponse()
data = res.read()
print(data.decode("utf-8"))
```

{% endtab %}
{% endtabs %}

## Change a balance

A request that changes something needs a key with **Change balances through the REST API (wallets:write)** and an `Idempotency-Key` header. That is `POST` and `PUT /api/customer`, `POST` and `DELETE /api/action/{actionid}`, and `POST /api/trigger`. Send a new value for each change (a UUID is a good choice). Send the same value again when you retry: a retry with the same key is never applied twice.

This request adds 10 to a customer's balance:

```bash
curl --location --request PUT 'https://app.shopwaive.com/api/customer' \
--header 'X-Shopwaive-Access-Token: swak_EXAMPLEKEY000000000000000000000000000000' \
--header 'X-Shopwaive-Platform: shopify' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: 3f9c2b8e-1d4a-4c7e-9b2f-6a1e5d0c7b3a' \
--data '{"customer_email": "customer@example.com", "amount": 10, "note": "Thank you gift"}'
```

Without an `Idempotency-Key` header, the request returns HTTP `400` with the reason `idempotency_key_required`, and nothing is changed:

{% code overflow="wrap" %}

```json
{
  "status": "failed",
  "reason": "idempotency_key_required",
  "header": "Idempotency-Key",
  "message": "A request made with a Shopwaive API key that can change a balance needs an Idempotency-Key header: a unique value per change, for example a UUID, sent again unchanged on a retry. Nothing was changed."
}
```

{% endcode %}

The answers to a retry are in [Retry safely](/reference/rest-api-documentation/customer-api.md#retry-safely).
