{"id":13509620,"url":"https://github.com/nsweeting/shopify","last_synced_at":"2025-04-05T11:09:13.497Z","repository":{"id":47033817,"uuid":"78172488","full_name":"nsweeting/shopify","owner":"nsweeting","description":"Easily access the Shopify API with Elixir.","archived":false,"fork":false,"pushed_at":"2023-01-10T21:05:11.000Z","size":295,"stargazers_count":102,"open_issues_count":27,"forks_count":55,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-29T10:06:32.395Z","etag":null,"topics":["elixir","elixir-lang","oauth","shopify","shopify-api","shopify-apps"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nsweeting.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-01-06T04:04:16.000Z","updated_at":"2024-11-05T18:31:47.000Z","dependencies_parsed_at":"2023-02-08T20:15:58.494Z","dependency_job_id":null,"html_url":"https://github.com/nsweeting/shopify","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nsweeting%2Fshopify","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nsweeting%2Fshopify/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nsweeting%2Fshopify/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nsweeting%2Fshopify/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nsweeting","download_url":"https://codeload.github.com/nsweeting/shopify/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247325693,"owners_count":20920714,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["elixir","elixir-lang","oauth","shopify","shopify-api","shopify-apps"],"created_at":"2024-08-01T02:01:10.469Z","updated_at":"2025-04-05T11:09:13.465Z","avatar_url":"https://github.com/nsweeting.png","language":"Elixir","funding_links":[],"categories":["Third Party APIs","Elixir"],"sub_categories":[],"readme":"# Shopify API\n\n[![Build Status](https://travis-ci.org/nsweeting/shopify.svg?branch=master)](https://travis-ci.org/nsweeting/shopify)\n[![Hex.pm](https://img.shields.io/hexpm/v/shopify.svg)](https://hex.pm/packages/shopify)\n\nThis package allows Elixir developers to easily access the admin Shopify API.\n\n## Installation\n\nThe package can be installed by adding `shopify` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [{:shopify, \"~\u003e 0.4\"}]\nend\n```\n\n## Getting Started\n\nThe Shopify API can be accessed in two ways - either with private apps via basic auth, or with oauth.\n\n### Private Apps\n\nOnce you have a valid API key and password, setup your `config/config.exs`.\n\n```elixir\nconfig :shopify, [\n  shop_name: \"my-shop\",\n  api_key: System.get_env(\"SHOPIFY_API_KEY\"),\n  password: System.get_env(\"SHOPIFY_API_PASSWORD\")\n]\n```\n\nWe can now easily create a new API session.\n\n```elixir\nShopify.session\n```\n\nAlternatively, we can create a one-off session.\n\n```elixir\nShopify.session(\"my-shop-name\", \"my-api-key\", \"my-password\")\n```\n\n### OAuth Apps\n\nOnce you have a shopify app client ID and secret, setup your `config/config.exs`.\n\n```elixir\nconfig :shopify, [\n  client_id: System.get_env(\"SHOPIFY_CLIENT_ID\"),\n  client_secret: System.get_env(\"SHOPIFY_CLIENT_SECRET\")\n]\n```\n\nTo gain access to a shop via OAuth, first, generate a permission url based on your requirments.\n\n```elixir\nparams = %{scope: \"read_orders,read_products\", redirect_uri: \"http://my-redirect_uri.com/\"}\npermission_url = \"shop-name\" |\u003e Shopify.session() |\u003e Shopify.OAuth.permission_url(params)\n```\n\nAfter a shop has authorized access, they will be redirected to your URI above. The redirect will include\na payload that contains a 'code'. We can now generate an access token.\n\n```elixir\n{:ok, %Shopify.Response{data: oauth}} = \"shop-name\" |\u003e Shopify.session() |\u003e Shopify.OAuth.request_token(code)\n```\n\nWe can now easily create a new OAuth API session.\n\n```elixir\nShopify.session(\"shop-name\", oauth.access_token)\n```\n\n## Making Requests\n\nAll API requests require a session struct to begin.\n\n```elixir\n\"shop-name\" |\u003e Shopify.session(\"access-token\") |\u003e Shopify.Product.find(1)\n\n# OR\n\nsession = Shopify.session(\"shop-name\", \"access-token\")\nShopify.Product.find(session, 1)\n```\n\nHere are some examples of the various types of requests that can be made.\n\n```elixir\n# Create a session struct\nsession = Shopify.session(\"shop-name\", \"access-token\")\n\n# Find a resource by ID\n{:ok, %Shopify.Response{data: product}} = session |\u003e Shopify.Product.find(id)\n\n# Find a resource and select fields\n{:ok, %Shopify.Response{data: product}} = session |\u003e Shopify.Product.find(id, %{fields: \"id,images,title\"})\n\n# All resources\n{:ok, %Shopify.Response{data: products}} = session |\u003e Shopify.Product.all\n\n# All resources with query params\n{:ok, %Shopify.Response{data: products}} = session |\u003e Shopify.Product.all(%{page: 1, limit: 5})\n\n# Find a resource and update it\n{:ok, %Shopify.Response{data: product}} = session |\u003e Shopify.Product.find(id)\nupdated_product = %{product | title: \"New Title\"}\n{:ok, response} = session |\u003e Shopify.Product.update(product.id, updated_product)\n\n# Update a resource without finding it\n{:ok, response} = session |\u003e Shopify.Product.update(id, %{title: \"New Title\"})\n\n# Create a resource from the resource struct\nnew_product = %Shopify.Product{\n    title: \"Fancy Shirt\",\n    body_html: \"\u003cstrong\u003eGood shirt!\u003c\\/strong\u003e\",\n    vendor: \"Fancy Vendor\",\n    product_type: \"shirt\",\n    variants: [\n    \t%{\n   \t\tprice: \"10.00\",\n    \t\tsku: 123\n   \t}]\n    }\n{:ok, response} = session |\u003e Shopify.Product.create(new_product)\n\n# Create a resource from a simple map\nnew_product_args = %{\n    title: \"Fancy Shirt\",\n    body_html: \"\u003cstrong\u003eGood shirt!\u003c\\/strong\u003e\",\n    vendor: \"Fancy Vendor\",\n    product_type: \"shirt\",\n    variants: [\n    \t%{\n   \t\tprice: \"10.00\",\n    \t\tsku: 123\n   \t}]\n    }\n{:ok, response} = session |\u003e Shopify.Product.create(new_product_args)\n\n# Count resources\n{:ok, %Shopify.Response{data: count}} = session |\u003e Shopify.Product.count\n\n# Count resources with query params\n{:ok, %Shopify.Response{data: count}} = session |\u003e Shopify.Product.count(%{vendor: \"Fancy Vendor\"})\n\n# Search for resources\n{:ok, %Shopify.Response{data: customers}} = session |\u003e Shopify.Customer.search(%{query: \"country:United States\"})\n\n# Delete a resource\n{:ok, _} = session |\u003e Shopify.Product.delete(id)\n```\n\n## API Versioning\n\nShopify supports [API versioning](https://help.shopify.com/en/api/versioning). By\ndefault, if you dont specify an api version, your request defaults to the oldest\nsupported stable version.\n\nYou can specify a default version through application config.\n\n```elixir\nconfig :shopify, [\n  api_version: \"2019-04\"\n]\n```\n\nYou can also set a specific version per session.\n\n```elixir\nShopify.session(\"shop-name\", \"access-token\") |\u003e Shopify.Session.put_api_version(\"2019-04\")\n```\n\n## Handling Responses\n\nResponses are all returned in the form of a two-item tuple. Any response that has a status\ncode below 300 returns `{:ok, response}`. Codes above 300 are returned as `{:error, response}`.\n\n```elixir\n# Create a session struct\nsession = Shopify.session(\"shop-name\", \"access-token\")\n\n# 'data' is returned as a %Shopify.Product struct\n{:ok, %Shopify.Response{code: 200, data: data}} = session |\u003e Shopify.Product.find(id)\n\n# 'data' is returned as a list of %Shopify.Product structs\n{:ok, %Shopify.Response{code: 200, data: data}} = session |\u003e Shopify.Product.all\n\n# 'message' is a text description of the error.\n{:error, %Shopify.Response{code: 404, data: message}} = session |\u003e Shopify.Product.find(1)\n\n# Failed requests return %Shopify.Error struct\n{:error, %Shopify.Error{reason: :econnrefused, source: :httpoison}} = session |\u003e Shopify.Product.find(1)\n\n```\n\nThe `%Shopify.Response{}` struct contains two fields: code and data. Code is the HTTP\nstatus code that is returned from Shopify. A successful request will either set the data field\nwith a single struct, or list of structs of the resource or resources requested.\n\n## Multipass\n\nThe [Multipass](https://help.shopify.com/en/api/reference/plus/multipass) is available to Shopify Plus plans. It allows your non-Shopify site to be the source of truth for authentication and login. After your site has successfully authenticated a user, redirect their browser to Shopify using the special Multipass URL: this will upsert the customer data in Shopify and log them in.\n\nUnlike other API requests, this does not require a session: it relies on a shared secret to do decryption.\n\nYour customer data must at a minimum provide an email address and a current datetime in 8601 format.\n\n```elixir\ncustomer_data = %{\n  email: \"something@test.shopify.com\",\n  created_at: DateTime.to_iso8601(Timex.now())\n}\n\n# From your store's checkout settings\nmultipass_secret = Application.get_env(\"MULTIPASS_SECRET\")\n\nurl = Shopify.Multipass.get_url(\"myteststore\", customer_data, multipass_secret)\n\n# Redirect the browser immediately to the resulting URL:\n\"https://myteststore.myshopify.com/account/login/multipass/moaqEVx1Yu9hsvYvVpj-LeRYDtOo6ikicfTZd8tR8-xBMRg8tFjGEfllEcjj2VdbsezmT0XuEdglyQzi_biQPkfLJnP1dkxhNtfzwtt6IMQzu3W0qCPzbrUMD_gLaytPVP-zZZuYiSBqEMNdvzFg3zf0TOQHwbizX2D7It02sFI7ZpTRhfX4m_crV0b-DmmF\"\n```\n\n## Testing\n\nFor testing a mock adapter can be configured to use fixture json files instead of doing real requests.\n\nLets say you have a test config file in `your_project/config/test.exs` and tests in `your_project/test` you could use this configuration:\n\n```elixir\n# your_project/config/test.exs\nconfig :shopify, [\n  shop_name: \"test\",\n  api_key: \"test-key\",\n  password: \"test-paswword\",\n  client_secret: \"test-secret\",\n  client_adapter: Shopify.Adapters.Mock, # Use included Mock adapter\n  fixtures_path: Path.expand(\"../test/fixtures/shopify\", __DIR__) # Use fixures in this directory\n]\n```\n\nWhen using oauth, make sure the token passed is `test`, otherwise authentication will fail.\n\n```elixir\nShopify.session(\"my-shop.myshopify.com\", \"test\")\n|\u003e Product.all()\n```\n\n### Test Adapter\n\nThis plugin provides a test adapter called `Shopify.Adapters.Mock` to use out of the box. It makes certain assumptions about your fixtures and is limited to the responses provided in corresponding fixture files, and for create actions it will put the resource id as 1.\n\nIf you would like to roll your own adapter, you can do so by implementing `@behaviour Shopify.Adapters.Base`.\n\n```elixir\ndefmodule Shopify.Adapters.Mock do\n  @moduledoc false\n\n  @behaviour Shopify.Adapters.Base\n\n  def get(%Shopify.Request{} = request) do\n    data =  %{resource: %{id: 123, attribute: \"attribute\"}}\n    {:ok,  %Shopify.Response{code: 200, data: data}}\n  end\n\n  # ...\nend\n```\n\n### Fixtures\n\nFixture files must follow a certain structure, so the adapter is able to find them. If your resource is `Shopify.Product.all()` you need to provide a file at `path_you_provided_in_config/products.json` and must include a valid response json\n\n```\n{\n  \"orders\": [\n    {\n      \"buyer_accepts_marketing\": false,\n      \"cancel_reason\": null,\n      \"cancelled_at\": null,\n      ...\n    }\n  ]\n}\n```\n\nOr for `Shopify.Product.find(1)`\n\n```\n# path_you_provided_in_config/products/1.json\n{\n  \"order\": {\n    \"id\": 1,\n    \"email\": \"bob.mctest@test.com\",\n    ...\n  }\n}\n```\n\n## Current Resources\n\n- Address\n- ApplicationCharge (find, all, create, activate)\n- ApplicationCredit (find, all, create)\n- Article (find, all, create, update, delete, count)\n- Article.Author (all)\n- Article.Tag (all)\n- Attribute\n- BillingAddress\n- Blog (find, all, create, update, delete, count)\n- CarrierService (find, all, create, update, delete)\n- Checkout (all, find, create, update, count, shipping_rates, complete, count)\n- ClientDetails\n- Collect (find, all, create, delete, count)\n- CollectionListing (find, all)\n- Comment (find, all, create, update, spam, not_spam, approve, remove, restore)\n- Country (find, all, create, update, delete, count)\n- Country.Province (find, all, update, count)\n- CustomCollection (find, all, create, update, delete, count)\n- Customer (find, all, create, update, delete, count, search)\n- CustomerAddress (find, all, create, delete)\n- CustomerSavedSearch (find, all, create, update, delete, count)\n- CustomerSavedSearch.Customer (all)\n- DiscountCode\n- DraftOrder (find, all, create, update, delete, count, complete, send_invoice) *`send_invoice` is an alias of `DraftOrder.DraftOrderInvoice.create/3`*\n- DraftOrder.DraftOrderInvoice (create)\n- MarketingEvent.Engagement (create_multiple)\n- Event (find, all, count)\n- Order.Fullfillment (find, all, count, create, update, complete, open, cancel)\n- Order.Fullfillment.Event (find, all, delete)\n- FulfillmentService (find, all, create, update, delete)\n- Image (ProductImage) (find, all, create, update, delete, count)\n- InventoryLevel (all, delete)\n- LineItem\n- Location (find, all, count)\n- MarketingEvent (find, all, count, create, update, delete, create_multiple_engagements) *`create_multiple_engagements` is an alias of `MarketingEvent.Engagement.create_multiple/3`*\n- Metafield\n- OAuth.AccessScope (all)\n- Option\n- Order (find, all, create, update, delete, count)\n- Order.Event (all)\n- Order.Risk (create, find, all, update, delete)\n- Page (create, find, all, update, delete, count)\n- PaymentDetails\n- Policy (all)\n- PriceRule (find, all, create, update, delete)\n- PriceRule.DiscountCode (find, all, create, update, delete)\n- Product (find, all, create, update, delete, count)\n- Product.Event (all)\n- ProductListing (find, all, create, update, delete, count, product_ids)\n- RecurringApplicationCharge (find, all, create, activate, delete)\n- Redirect (find, all, create, update, delete, count)\n- Refund (create, find, all)\n- Report (create, find, all, update, delete)\n- ScriptTag (find, all, create, count, delete)\n- ShippingAddress\n- ShippingLine\n- Shop (current)\n- SmartCollection (find, all, create, count, update, delete)\n- TaxLine\n- Theme (find, all, create, update, delete)\n- Theme.Asset (find, all, delete)\n- Transaction (find, all, create, count)\n- UsageCharge (find, all, create)\n- Variant (find, all, create, update, delete, count)\n- Webhook (find, all, create, update, delete, count)\n\n## Contributors\n\n\u003c!-- Contributors START\nNick_Sweeting nsweeting https://github.com/nsweeting code prReview doc infra\nMarcelo_Oliveira overallduka https://github.com/overallduka code\nFabian_Zitter Ninigi https://github.com/Ninigi code prReview doc\nZach_Garwood zachgarwood https://github.com/zachgarwood code\nDavid_Becerra DavidVII https://github.com/DavidVII code\nBryan_Bryce BryanJBryce https://github.com/BryanJBryce doc\nhumancopy humancopy https://github.com/humancopy code\nContributors END --\u003e\n\u003c!-- Contributors table START --\u003e\n| \u003cimg src=\"https://avatars.githubusercontent.com/nsweeting?s=100\" width=\"100\" alt=\"Nick Sweeting\" /\u003e\u003cbr /\u003e[\u003csub\u003eNick Sweeting\u003c/sub\u003e](https://github.com/nsweeting)\u003cbr /\u003e[💻](git@github.com:nsweeting/shopify/commits?author=nsweeting) 👀 [📖](git@github.com:nsweeting/shopify/commits?author=nsweeting) 🚇 | \u003cimg src=\"https://avatars.githubusercontent.com/overallduka?s=100\" width=\"100\" alt=\"Marcelo Oliveira\" /\u003e\u003cbr /\u003e[\u003csub\u003eMarcelo Oliveira\u003c/sub\u003e](https://github.com/overallduka)\u003cbr /\u003e[💻](git@github.com:nsweeting/shopify/commits?author=overallduka) | \u003cimg src=\"https://avatars.githubusercontent.com/Ninigi?s=100\" width=\"100\" alt=\"Fabian Zitter\" /\u003e\u003cbr /\u003e[\u003csub\u003eFabian Zitter\u003c/sub\u003e](https://github.com/Ninigi)\u003cbr /\u003e[💻](git@github.com:nsweeting/shopify/commits?author=Ninigi) 👀 [📖](git@github.com:nsweeting/shopify/commits?author=Ninigi) | \u003cimg src=\"https://avatars.githubusercontent.com/zachgarwood?s=100\" width=\"100\" alt=\"Zach Garwood\" /\u003e\u003cbr /\u003e[\u003csub\u003eZach Garwood\u003c/sub\u003e](https://github.com/zachgarwood)\u003cbr /\u003e[💻](git@github.com:nsweeting/shopify/commits?author=zachgarwood) | \u003cimg src=\"https://avatars.githubusercontent.com/DavidVII?s=100\" width=\"100\" alt=\"David Becerra\" /\u003e\u003cbr /\u003e[\u003csub\u003eDavid Becerra\u003c/sub\u003e](https://github.com/DavidVII)\u003cbr /\u003e[💻](git@github.com:nsweeting/shopify/commits?author=DavidVII) | \u003cimg src=\"https://avatars.githubusercontent.com/BryanJBryce?s=100\" width=\"100\" alt=\"Bryan Bryce\" /\u003e\u003cbr /\u003e[\u003csub\u003eBryan Bryce\u003c/sub\u003e](https://github.com/BryanJBryce)\u003cbr /\u003e[📖](git@github.com:nsweeting/shopify/commits?author=BryanJBryce) | \u003cimg src=\"https://avatars.githubusercontent.com/humancopy?s=100\" width=\"100\" alt=\"humancopy\" /\u003e\u003cbr /\u003e[\u003csub\u003ehumancopy\u003c/sub\u003e](https://github.com/humancopy)\u003cbr /\u003e[💻](git@github.com:nsweeting/shopify/commits?author=humancopy) | \u003cimg src=\"https://avatars.githubusercontent.com/Cmeurer10?s=100\" width=\"100\" alt=\"Cmeurer10\" /\u003e\u003cbr /\u003e[\u003csub\u003eCmeurer10\u003c/sub\u003e](https://github.com/Cmeurer10)\u003cbr /\u003e[💻](git@github.com:nsweeting/shopify/commits?author=Cmeurer10) | \u003cimg src=\"https://avatars.githubusercontent.com/lewisf?s=100\" width=\"100\" alt=\"lewisf\" /\u003e\u003cbr /\u003e[\u003csub\u003elewisf\u003c/sub\u003e](https://github.com/lewisf)\u003cbr /\u003e[💻](git@github.com:nsweeting/shopify/commits?author=lewisf) | \u003cimg src=\"https://avatars.githubusercontent.com/vladimir-e?s=100\" width=\"100\" alt=\"vladimir-e\" /\u003e\u003cbr /\u003e[\u003csub\u003evladimir-e\u003c/sub\u003e](https://github.com/vladimir-e)\u003cbr /\u003e[💻](git@github.com:nsweeting/shopify/commits?author=vladimir-e) | \u003cimg src=\"https://avatars.githubusercontent.com/furqanaziz?s=100\" width=\"100\" alt=\"furqanaziz\" /\u003e\u003cbr /\u003e[\u003csub\u003efurqanaziz\u003c/sub\u003e](https://github.com/furqanaziz)\u003cbr /\u003e[💻](git@github.com:nsweeting/shopify/commits?author=furqanaziz) | \u003cimg src=\"https://avatars.githubusercontent.com/balexand?s=100\" width=\"100\" alt=\"balexand\" /\u003e\u003cbr /\u003e[\u003csub\u003ebalexand\u003c/sub\u003e](https://github.com/balexand)\u003cbr /\u003e[💻](git@github.com:nsweeting/shopify/commits?author=balexand)\n| :---: | :---: | :---: | :---: | :---: | :---: | :---: |\n\n\u003c!-- Contributors table END --\u003e\nThis project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification.\n\nDocumentation is generated with [ExDoc](https://github.com/elixir-lang/ex_doc).\nThey can be found at [https://hexdocs.pm/shopify](https://hexdocs.pm/shopify).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnsweeting%2Fshopify","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnsweeting%2Fshopify","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnsweeting%2Fshopify/lists"}