Urbit Docs
  • What is Urbit?
  • Get on Urbit
  • Build on Urbit
    • Contents
    • Environment Setup
    • Hoon School
      • 1. Hoon Syntax
      • 2. Azimuth (Urbit ID)
      • 3. Gates (Functions)
      • 4. Molds (Types)
      • 5. Cores
      • 6. Trees and Addressing
      • 7. Libraries
      • 8. Testing Code
      • 9. Text Processing I
      • 10. Cores and Doors
      • 11. Data Structures
      • 12. Type Checking
      • 13. Conditional Logic
      • 14. Subject-Oriented Programming
      • 15. Text Processing II
      • 16. Functional Programming
      • 17. Text Processing III
      • 18. Generic and Variant Cores
      • 19. Mathematics
    • App School I
      • 1. Arvo
      • 2. The Agent Core
      • 3. Imports and Aliases
      • 4. Lifecycle
      • 5. Cards
      • 6. Pokes
      • 7. Structures and Marks
      • 8. Subscriptions
      • 9. Vanes
      • 10. Scries
      • 11. Failure
      • 12. Next Steps
      • Appendix: Types
    • App School II (Full-Stack)
      • 1. Types
      • 2. Agent
      • 3. JSON
      • 4. Marks
      • 5. Eyre
      • 6. React app setup
      • 7. React app logic
      • 8. Desk and glob
      • 9. Summary
    • Core Academy
      • 1. Evaluating Nock
      • 2. Building Hoon
      • 3. The Core Stack
      • 4. Arvo I: The Main Sequence
      • 5. Arvo II: The Boot Sequence
      • 6. Vere I: u3 and the Serf
      • 7. Vere II: The Loom
      • 8. Vanes I: Behn, Dill, Kahn, Lick
      • 9. Vanes II: Ames
      • 10. Vanes III: Eyre, Iris
      • 11. Vanes IV: Clay
      • 12. Vanes V: Gall and Userspace
      • 13. Vanes VI: Khan, Lick
      • 14. Vanes VII: Jael, Azimuth
    • Runtime
      • U3
      • Conn.c Guide
      • How to Write a Jet
      • API Overview by Prefix
      • C in Urbit
      • Cryptography
      • Land of Nouns
    • Tools
      • Useful Links
      • JS Libraries
        • HTTP API
      • Docs App
        • File Format
        • Index File
        • Suggested Structure
    • Userspace
      • Command-Line App Tutorial
      • Remote Scry
      • Unit Tests
      • Software Distribution
        • Software Distribution Guide
        • Docket File
        • Glob
      • Examples
        • Building a CLI App
        • Debugging Wrapper
        • Host a Website
        • Serving a JS Game
        • Ship Monitoring
        • Styled Text
  • Urbit ID
    • What is Urbit ID?
    • Azimuth Data Flow
    • Life and Rift
    • Urbit HD Wallet
    • Advanced Azimuth Tools
    • Custom Roller Tutorial
    • Azimuth.eth Reference
    • Ecliptic.eth Reference
    • Layer 2
      • L2 Actions
      • L2 Rollers
      • L2 Roller HTTP RPC-API
      • L2 Transaction Format
  • Urbit OS
    • What is Urbit OS?
    • Base
      • Hood
      • Threads
        • Basics Tutorial
          • Bind
          • Fundamentals
          • Input
          • Output
          • Summary
        • HTTP API Guide
        • Spider API Reference
        • Strandio Reference
        • Examples
          • Child Thread
          • Fetch JSON
          • Gall
            • Poke Thread
            • Start Thread
            • Stop Thread
            • Take Facts
            • Take Result
          • Main-loop
          • Poke Agent
          • Scry
          • Take Fact
    • Kernel
      • Arvo
        • Cryptography
        • Move Trace
        • Scries
        • Subscriptions
      • Ames
        • Ames API Reference
        • Ames Cryptography
        • Ames Data Types
        • Ames Scry Reference
      • Behn
        • Behn API Reference
        • Behn Examples
        • Behn Scry Reference
      • Clay
        • Clay API Reference
        • Clay Architecture
        • Clay Data Types
        • Clay Examples
        • Clay Scry Reference
        • Filesystem Hierarchy
        • Marks
          • Mark Examples
          • Using Marks
          • Writing Marks
        • Using Clay
      • Dill
        • Dill API Reference
        • Dill Data Types
        • Dill Scry Reference
      • Eyre
        • EAuth
        • Eyre Data Types
        • Eyre External API
        • Eyre Internal API
        • Eyre Scry Reference
        • Low-Level Eyre Guide
        • Noun channels
      • Gall
        • Gall API Reference
        • Gall Data Types
        • Gall Scry Reference
      • Iris
        • Iris API Reference
        • Iris Data Types
        • Iris Example
      • Jael
        • Jael API Reference
        • Jael Data Types
        • Jael Examples
        • Jael Scry Reference
      • Khan
        • Khan API Reference
        • Khan Data Types
        • Khan Example
      • Lick
        • Lick API Reference
        • Lick Guide
        • Lick Examples
        • Lick Scry Reference
  • Hoon
    • Why Hoon?
    • Advanced Types
    • Arvo
    • Auras
    • Basic Types
    • Cheat Sheet
    • Cryptography
    • Examples
      • ABC Blocks
      • Competitive Programming
      • Emirp
      • Gleichniszahlenreihe
      • Islands
      • Luhn Number
      • Minimum Path Sum
      • Phone Letters
      • Restore IP
      • Rhonda Numbers
      • Roman Numerals
      • Solitaire Cipher
      • Water Towers
    • Generators
    • Hoon Errors
    • Hoon Style Guide
    • Implementing an Aura
    • Irregular forms
    • JSON
    • Limbs and wings
      • Limbs
      • Wings
    • Mips (Maps of Maps)
    • Parsing Text
    • Runes
      • | bar · Cores
      • $ buc · Structures
      • % cen · Calls
      • : col · Cells
      • . dot · Nock
      • / fas · Imports
      • ^ ket · Casts
      • + lus · Arms
      • ; mic · Make
      • ~ sig · Hints
      • = tis · Subject
      • ? wut · Conditionals
      • ! zap · Wild
      • Constants (Atoms and Strings)
      • --, == · Terminators
    • Sail (HTML)
    • Serialization
    • Sets
    • Standard Library
      • 1a: Basic Arithmetic
      • 1b: Tree Addressing
      • 1c: Molds and Mold-Builders
      • 2a: Unit Logic
      • 2b: List Logic
      • 2c: Bit Arithmetic
      • 2d: Bit Logic
      • 2e: Insecure Hashing
      • 2f: Noun Ordering
      • 2g: Unsigned Powers
      • 2h: Set Logic
      • 2i: Map Logic
      • 2j: Jar and Jug Logic
      • 2k: Queue Logic
      • 2l: Container from Container
      • 2m: Container from Noun
      • 2n: Functional Hacks
      • 2o: Normalizing Containers
      • 2p: Serialization
      • 2q: Molds and Mold-Builders
      • 3a: Modular and Signed Ints
      • 3b: Floating Point
      • 3c: Urbit Time
      • 3d: SHA Hash Family
      • 3e: AES encryption (Removed)
      • 3f: Scrambling
      • 3g: Molds and Mold-Builders
      • 4a: Exotic Bases
      • 4b: Text Processing
      • 4c: Tank Printer
      • 4d: Parsing (Tracing)
      • 4e: Parsing (Combinators)
      • 4f: Parsing (Rule-Builders)
      • 4g: Parsing (Outside Caller)
      • 4h: Parsing (ASCII Glyphs)
      • 4i: Parsing (Useful Idioms)
      • 4j: Parsing (Bases and Base Digits)
      • 4k: Atom Printing
      • 4l: Atom Parsing
      • 4m: Formatting Functions
      • 4n: Virtualization
      • 4o: Molds
      • 5a: Compiler Utilities
      • 5b: Macro Expansion
      • 5c: Compiler Backend & Prettyprinter
      • 5d: Parser
      • 5e: Molds and mold builders
      • 5f: Profiling support
    • Strings
    • The Engine Pattern
    • Udon (Markdown-esque)
    • Vases
    • Zuse
      • 2d(1-5): To JSON, Wains
      • 2d(6): From JSON
      • 2d(7): From JSON (unit)
      • 2e(2-3): Print & Parse JSON
      • 2m: Ordered Maps
  • Nock
    • What is Nock?
    • Decrement
    • Definition
    • Fast Hints and Jets
    • Implementations
    • Specification
  • User Manual
    • Contents
    • Running Urbit
      • Cloud Hosting
      • Home Servers
      • Runtime Reference
      • Self-hosting S3 Storage with MinIO
    • Urbit ID
      • Bridge Troubleshooting
      • Creating an Invite Pool
      • Get an Urbit ID
      • Guide to Factory Resets
      • HD Wallet (Master Ticket)
      • Layer 2 for planets
      • Layer 2 for stars
      • Proxies
      • Using Bridge
    • Urbit OS
      • Basics
      • Configuring S3 Storage
      • Dojo Tools
      • Filesystem
      • Shell
      • Ship Troubleshooting
      • Star and Galaxy Operations
      • Updates
Powered by GitBook

GitHub

  • Urbit ID
  • Urbit OS
  • Runtime

Resources

  • YouTube
  • Whitepaper
  • Awesome Urbit

Contact

  • X
  • Email
  • Gather
On this page
  • Background
  • Clay
  • Gall agents
  • Threads
  • HTTP API basics
  • The Urbit() object
  • /session.js
  • Channels
  • Connection state
  • Tutorial setup
  • Types
  • Marks
  • Agent
  • Committing the code
  • Using the HTTP API
  • Importing the HTTP API
  • Authenticate
  • Poke
  • Scry
  • Subscribe and unsubscribe
  • Subscribe once
  • Run a thread
  • Delete a channel
  • Reset
  • Further reading
Edit on GitHub
  1. Build on Urbit
  2. Tools
  3. JS Libraries

HTTP API

Urbit's Eyre vane is an HTTP server which our web frontends can talk to. In this guide, we'll create a simple Urbit app and use the @urbit/http-api JavaScript module to interact with it from a web app.

Background

Eyre's API is a fairly thin overlay on some of Arvo's internal systems, so there's some basic things to understand.

Clay

Clay is the filesystem vane. It's typed, and it's revision-controlled in a similar way to git. Clay contains a number of desks, which are a bit like git repositories. Each app on your ship's home screen corresponds to a desk in Clay. That desk contains the source code and resources for that app.

Marks

Most of Clay's workings aren't relevant to frontend development, but there's one important concept to understand: marks. Clay is a typed filesystem, and marks are the filetypes. There's a mark for .hoon files, a mark for .txt files, and so on. The mark specifies the datatype for those files, and it also specifies conversion methods between different types. Marks aren't just used for files saved in Clay, but also for data that goes to and from the web through Eyre.

When you send a poke or run a thread through Eyre's HTTP API, Clay will look at the mark specified (for example ui-action, contact-action-1, etc.) and use the corresponding mark file in the relevant desk to convert the given JSON to that type, before passing it to the target agent or thread. The same conversion will happen in reverse for responses.

Note that Eyre makes a best effort attempt to convert data to and from JSON. If the marks in question do not contain appropriate JSON conversion functions, it will fail. Not all scry endpoints, subscription paths, and pokes are intended to be used from a frontend, so not all of them use marks which can convert to and from JSON. (The noun mark for example). The majority of things you'll want to interact with through Eyre will work with JSON.

Gall agents

An agent is a userspace application managed by the Gall vane. A desk may contain multiple agents that do different things. The Tlon Messenger app, for example, has the %contacts, %profile, and %lanyard agents in its desk, among others. Agents are the main thing you'll interact with through Eyre. They have a simple interface with three main parts:

Interface
Description

Pokes

One-off message to an agent. Pokes often represent actions, commands, requests, etc.

Subscriptions

An agent may have a number of different paths to which you may subscribe.

Scries

Scries are one-time, read-only requests for data. Like subscriptions, they are organized by path. Scry requests will be fulfilled immediately.

Pokes

Pokes are single, standalone messages to agents. Pokes are how you send data and requests to agents. Agents will send back either a positive acknowledgement (ack) or a negative acknowledgement (nack). The agent can't send actual data back in the acks. If they have any response to give back, it will be sent out to subscribers on a subscription path instead.

The pokes an agent accepts will be defined in the +on-poke section of its source code in the desk's /app directory, or maybe in its type definition file in the /sur directory.

@urbit/http-api includes a poke() function which allows you to perform pokes through Eyre, and is detailed below.

Subscriptions

Agents define subscription paths which you can subscribe to through Eyre. A path might be simple and fixed like /foo/bar, or it might have dynamic elements where you can specify a date, a user, a key in a key-value store, etc. Each agent will define its subscription paths in the +on-watch section of its source code.

You can subscribe by sending a request to the agent with the desired path specifed. The agent will apply some logic (such as checking permissions) to the request and then ack or nack it. If acked, you'll be subscribed. You might receive an initial payload of data defined in +on-watch. Then you'll begin receiving any updates the agent sends out on that path in future. What you'll receive on a given path depends entirely on the agent.

Agents can kick subscribers, and you can unsubscribe at any time.

@urbit/http-api includes a subscribe() function which allows you to subscribe and unsubscribe to paths through Eyre, and is detailed below.

Scry Endpoints

Pokes and subscriptions can modify the state of the agent. A third kind of interaction called a scry does not. It simply retrieves data from the agent without any side-effects. Agents can define scry endpoints which, like subscriptions, are paths. A scry to one of these endpoints will retrieve some data as determined by the agent. Like subscription paths, scry paths can be simple like /foo/bar or contain dynamic elements. Unlike subscriptions, a scry is a one-off request and the data will come back immediately.

Scry endpoints are defined in the +on-peek section of an agent's source code. Scry endpoints will be written with a leading letter like /x/foo/bar. That letter is a care which tells Gall what kind of request this is. All scries through Eyre have a care of /x, so that letter needn't be specified.

@urbit/http-api includes a scry() function which allows you to perform scries through Eyre, and is detailed below.

Threads

A thread is a monadic function in Arvo that takes arguments and produces a result. Threads are conceptually similar to Javascript promises: they can perform one or more asynchronous I/O operations, which can be chained together, and will notify the agent that started them whether they succeedeed or failed. Threads are often used to handle complex I/O operations for agents. Threads live in the /ted directory of a desk.

@urbit/http-api includes a thread() function which allows you to run threads through Eyre, and is detailed below.

HTTP API basics

Now that we've covered the backend concepts, let's see how @urbit/http-api communicates with the server.

The Urbit() object

All functionality is contained within the Urbit() object. There are two ways to instantiate it, depending on whether your web app is served directly from the ship or whether it's served externally. The reason for the difference is that you require a session cookie to talk to the ship.

If your app is served from the ship, the user will already be logged in and they'll have a session cookie that Urbit() will use automatically.

If your app isn't served from the ship, you'll need to authenticate with the user's ship, which is detailed separately below.

In the case of a frontend served from the ship, the Urbit() class contains a constructor which takes 1-3 arguments:

Argument
Type
Description
Example

url

string

The host of the ship. This string is mandatory, but is typically left empty as requests will still work if they're root-relative paths.

"example.com", "http://localhost:8080", ""

code

string

(Optional.) The web login code of the ship. Not needed if your frontend is served from the ship. In practice this should never be set by the frontend (if you need the user to log into their ship, use EAuth), but if you had to you'd want to import this from a secure environment variable to avoid putting it in the source code.

"", "lidlut-tabwed-pillex-ridrup"

desk

string

(Optional.) The desk on which you want to run threads. This is only used if you want to run threads from the frontend, rather than run them from the agent.

"landscape", ""

To create an Urbit() instance, you can simply do:

const api = new Urbit("");

If you want to specify a desk, you can do:

const api = new Urbit("", "", "landscape");

/session.js

Most functions of Urbit() need to know the ship's Urbit ID or they will fail. This is given explicitly with the external authentication method detailed below, but that's unnecessary when using the Urbit() object in a web app served directly from the ship, because the ship serves a JS library at /session.js that contains the following:

window.ship = "zod";

"zod" will be replaced with the actual name of the ship in question. You can import this file like so:

<script src="/session.js"></script>

Then you need to set the ship field in the Urbit() object. You would typically do it immediately after instantiating it:

const api = new Urbit("");
api.ship = window.ship;

Channels

With the exception of scries and threads, all communication with Eyre happens through its channel system.

When it's constructed, the Urbit() object will generate a random channel ID like 1646295453-e1bdfd, and use a path of /~/channel/1646295453-e1bdfd to talk to Eyre. Pokes and subscription requests will be sent to that channel. Responses and subscription updates will be sent out to the frontend on that channel too.

Eyre sends out updates and responses on an SSE (Server Sent Event) stream for that channel. The Urbit() object handles this internally with an eventSource object, so you won't deal with it directly. Eyre requires all events it sends out be acknowledged by the client, and will eventually close the channel if enough unacknowledged events accumulate. The Urbit() object handles event acknowledgement automatically.

Eyre automatically creates a channel when a poke or subscription request is first sent to /~/channel/[unknown-channel-id]. If your web app is served outside a ship, you could use the authenticate() function described below which will automatically send a poke and open the new channel. If your web app is served directly from the ship and you use the Urbit() object, it won't open the channel right away. Instead, the channel will be opened whenever you first send a poke or subscription request.

Connection state

The Urbit() object includes three optional callback functions that fire when the SSE connection state changes:

Callback
Description

onOpen()

Called when an SSE channel connection is successfully established.

onRetry()

Called when a reconnection attempt is made due to an interruption, e.g. if there are network problems.

onError()

Called when there is an unrecoverable error, e.g. after enough reconnection attemps have failed.

As mentioned in the previous section, typically a channel will be opened and an SSE connection established after you first poke the ship or make a subscription request. If successful, whatever function you provided to onOpen() will be called. If at some point the connection is interrupted, a reconnection attempt will be made three times:

  1. Instantly.

  2. 750ms after the first.

  3. 3000ms after the second.

Each attempt will call the function you provided to onRetry(), if any. If all three reconnection attempts failed, or if a fatal error occurred, the function you provided onError() will be called with an Error object containing an error message as its argument.

How you use these, if at all, is up to you. If you want to try reconnecting when onError() fires, note that Eyre will delete a channel if it's had no messages from the client in the last 12 hours. The timeout is reset whenever it receives a message, including the acks that are automatically sent by the Urbit() object in response to subscription updates.

If you don't want to account for the possibility of the channel having been deleted, you can just call the reset() function before you try reconnecting and consequently open a brand new channel.

Tutorial setup

Start a fake ~zod and we'll add a Gall agent to its %base desk. This agent will store a trivial key-value database. It will provide endpoints for pokes, scries, and subscriptions. We'll define its poke and update types in a /sur file, and create marks that convert those types to and from JSON. We'll use @urbit/http-api to interact with the agent.

Types

In the /sur folder of the %base desk, create a file /api-demo.hoon and define the following types:

  • $api-action: User actions sent from the frontend to the Gall agent. We just want to put new k-v pairs into the state, and delete them by their keys.

  • $api-update: Updates sent from the Gall agent to the frontend. Updates tagged with %store will contain the entire updated k-v store. Updates tagged with %key-value will contain one key and a unit of a value.

/sur/api-demo.hoon
|%
+$  api-action
  $%  [%put key=@tas val=@t]
      [%del key=@tas]
  ==
+$  api-update
  $%  [%store store=(map @tas @t)]
      [%key-value key=@tas val=(unit @t)]
  ==
--

Marks

In the /mar folder of the %base desk, create two new files /api-action.hoon and /api-update.hoon. Both of these marks will contain functions for converting their respective types in /sur to and from JSON. Don't worry if you don't understand every line of this.

/mar/api-action.hoon
/-  *api-demo
|_  act=api-action
++  grab
  |%
  ++  noun  api-action
  ++  json
    |=  jon=^json
    %-  api-action
    =,  format
    %.  jon
    %-  of:dejs
    :~  :-  %put
        %-  ot:dejs
        :~  [%key (se:dejs %tas)]
            [%val so:dejs]
        ==
        :-  %del
        %-  ot:dejs
        :~  [%key (se:dejs %tas)]
        ==
    ==
  --
++  grow
  |%
  ++  noun  act
  ++  json
    ^-  ^json
    =,  format
    ?-  -.act
        %put
      %-  frond:enjs
      :-  'put'
      %-  pairs:enjs
      :~  ['key' [%s key.act]]
          ['val' [%s val.act]]
      ==
    ::
        %del
      %-  frond:enjs
      :-  'del'
      %-  frond:enjs
      :-  'key'
      [%s key.act]
    ==
  --
++  grad  %noun
--
/mar/api-update.hoon
/-  *api-demo
|_  upd=api-update
++  grab
  |%
  ++  noun  api-update
  --
++  grow
  |%
  ++  noun  upd
  ++  json
    =,  format
    ^-  ^json
    ?-  -.upd
        %store
      %-  frond:enjs
      :-  'store'
      %-  pairs:enjs
      %+  turn
        ~(tap by store.upd)
      |=  [k=@tas v=@t]
      [(@t k) [%s v]]
    ::
        %key-value
      %-  frond:enjs
      :-  'key-value'
      %-  pairs:enjs
      :~  ['key' [%s (@t key.upd)]]
          ['val' ?~(val.upd [%s ''] [%s u.val.upd])]
      ==
    ==
  --
++  grad  %noun
--

Agent

In the %base desk's /app directory, create a new file called /api-demo.hoon and paste in the code below.

/app/api-demo.hoon
/-  *api-demo
/+  default-agent, dbug
::
|%
+$  card  card:agent:gall
+$  versioned-state
  $%  state-0
  ==
+$  state-0
  $:  %0
      store=(map @tas @t)
  ==
--
::
%-  agent:dbug
=|  state-0
=*  state  -
::
^-  agent:gall
|_  =bowl:gall
+*  this  .
    def   ~(. (default-agent this %.n) bowl)
::
++  on-init
  ^-  (quip card _this)
  :-  ~
  %=  this
    state  [%0 ~]
  ==
::
++  on-save  !>(state)
++  on-load
  |=  old-state=vase
  ^-  (quip card _this)
  =/  old  !<(versioned-state old-state)
  ?-  -.old
      %0  `this(state old)
  ==
::
++  on-poke
  |=  [=mark =vase]
  ^-  (quip card _this)
  ?+  mark
    (on-poke:def mark vase)
  ::
      %api-action
    =/  act  !<(api-action vase)
    ?-  -.act
        %put
      ~&  >  "api-demo: putting [{<key.act>} {<val.act>}]"
      =/  new-store
        (~(put by store) key.act val.act)
      :_  %=  this
            store  new-store
          ==
      :~  :*  %give
              %fact
              [/updates]~
              %api-update
              !>  ^-  api-update
              [%store new-store]
          ==
          :*  %give
              %fact
              [(welp /updates [key.act]~)]~
              %api-update
              !>  ^-  api-update
              [%key-value key.act (some val.act)]
          ==
      ==
    ::
        %del
      ~&  >  "api-demo: deleting {<key.act>}"
      :_  %=  this
            store  (~(del by store) key.act)
          ==
      :~  :*  %give
              %fact
              [(welp /updates [key.act]~)]~
              %api-update
              !>  ^-  api-update
              [%key-value key.act ~]
          ==
      ==
    ==
  ==
::
++  on-watch
  |=  =(pole knot)
  ^-  (quip card _this)
  ?+  pole
    (on-watch:def pole)
  ::
      [%updates ~]
    ~&  >  "api-demo: subscribed to /updates"
    :_  this
    :~  :*  %give
            %fact
            ~
            %api-update
            !>  ^-  api-update
            [%store store]
        ==
    ==
  ::
      [%updates key=@tas ~]
    ~&  >  "api-demo: subscribed to {<`path`(welp /updates [key.pole]~)>}"
    :_  this
    :~  :*  %give
            %fact
            ~
            %api-update
            !>  ^-  api-update
            [%key-value key.pole (~(get by store) key.pole)]
        ==
    ==
  ==
::
++  on-peek
  |=  =(pole knot)
  ^-  (unit (unit cage))
  ~&  >  "api-demo: scry on {<`path`pole>}"
  ?+  pole
    (on-peek:def pole)
  ::
      [%x %store ~]
    %-  some
    %-  some
    :-  %api-update
    !>  ^-  api-update
    [%store store]
  ::
      [%x %store key=@tas ~]
    %-  some
    %-  some
    :-  %api-update
    !>  ^-  api-update
    [%key-value key.pole (~(get by store) key.pole)]
  ==
::
++  on-leave
  |=  =(pole knot)
  ~&  >  "api-demo: unsubscribed from {<`path`pole>}"
  `this
::
++  on-agent  on-agent:def
++  on-arvo   on-arvo:def
++  on-fail   on-fail:def
--

Committing the code

Finally, run |commit %base in the ship's dojo to commit your changes to the desk, then run |start %api-demo to initialise the agent.

Using the HTTP API

Create and serve the HTML examples below from a local URL to talk to your fake ~zod via the HTTP API.

An Urbit ship will deny CORS requests from external URLs by default. In order to run the examples below, you'll need to serve them from a URL (for example, with Python's http.server module) and approve that URL in the ship's dojo. If serving the example page from http://localhost:8000, you'll need to run:

|eyre/cors/approve 'http://localhost:8000'

Importing the HTTP API

The http-api module is available in npm as @urbit/http-api, and can be installed with:

npm i @urbit/http-api

Once installed, you can import it into your app with:

import Urbit from '@urbit/http-api';

Note that the examples in this guide are simple HTML documents with vanilla Javascript in <script> tags, so they use unpkg.com to import @urbit/http-api. This is not typical, and is just done here for purposes of simplicity.

Authenticate

If your frontend is served directly from the Urbit ship, this can be skipped.

If your web app is served externally to the ship, you must authenticate and obtain a session cookie before commencing communications with the ship.

The Urbit() object includes an authenticate function which does the following:

  1. Login to the user's ship with their code and obtain a session cookie.

  2. Generate a random channel ID for the connection.

  3. Poke the user's ship and print "opening airlock" in the dojo to initialize the channel.

The authenticate function takes four arguments in an object: ship, url, code and verbose:

Argument
Type
Description
Example

ship

string

(Optional.) The ship ID (@p) without the leading ~.

"sampel-palnet" or "zod"

url

string

The base URL for the ship.

"http://localhost:8080" or "example.com"

code

string

(Optional.) The user's web login code.

"lidlut-tabwed-pillex-ridrup"

verbose

boolean

(Optional.) Whether to log details to the console. This field is optional and defaults to false.

true

This function returns a promise that if successful, produces an Urbit() object which can then be used for communications with the ship.

authenticate() example

auth-test.html
<html>
  <head>
    <script src="https://unpkg.com/@urbit/http-api"></script>
  </head>
  <body>
    <button id="start" type="button" onClick="connect()" >Connect</button>
  </body>
  <script>
    async function connect() {
      window.api = await UrbitHttpApi.Urbit.authenticate({
          ship: "zod",
          url: "http://localhost:8080",
          code: "lidlut-tabwed-pillex-ridrup",
          verbose: true
      });
      document.body.innerHTML = "Connected!";
    };
  </script>
</html>

Poke

For poking a ship, Urbit() includes a poke function. The poke function takes six arguments in a object:

Argument
Type
Description
Example

app

string

The Gall agent to poke.

"api-demo"

mark

string

The mark of the data to poke the agent with.

"api-action"

json

any JSON

The data to poke the agent with.

{ put: { key: "foo", val: "bar" } }

ship

string

(Optional.) The Urbit ID (@p) of the ship without the ~. This may be ommitted if it's already been set for the whole Urbit() object.

"zod"

onSuccess

A function.

(Optional.) This is called if the poke succeeded (the ship ack'd the poke).

someFunction()

onError

A function.

(Optional.) This is called if the poke failed (the ship nack'd the poke).

anotherFunction()

poke() example

poke-test.html
<html>
  <head>
    <script src="https://unpkg.com/@urbit/http-api"></script>
  </head>
  <body>
    <h2>HTTP API - Pokes</h2>
    <br>
    <div>
      <input id="put-key" type="text" placeholder="Key" />
      <input id="put-value" type="text" placeholder="Value" />
      <button id="put-button" type="button" onClick="putByKey()">Store Value</button>
    </div>
    <br>
    <div>
      <input id="del-key" type="text" placeholder="Key to delete" />
      <button id="del-button" type="button" onClick="delByKey()">Delete Value</button>
    </div>
    <br>
    <p id="status"></p>
  </body>
  <script>
    const api = new UrbitHttpApi.Urbit("");
    api.ship = "zod";
    api.url = "http://localhost:8080";
    api.code = "lidlut-tabwed-pillex-ridrup";

    function putByKey() {
      const key = document.getElementById("put-key").value;
      const value = document.getElementById("put-value").value;
      
      if (!key) {
        document.getElementById("status").innerHTML = "Error: Key is required";
        return;
      }

      if (!value) {
        document.getElementById("status").innerHTML = "Error: Value is required for deletion";
        return;
      }

      try {
        console.log(key)
        console.log(value)

        api.poke({
          app: "api-demo",
          mark: "api-action",
          json: { put: { key: key, val: value } },
          onSuccess: pokeSuccess,
          onError: pokeError,
        });
      } catch (err) {
        document.getElementById("status").innerHTML = "Poke error: " + err.message;
      }
    }

    function delByKey() {
      const key = document.getElementById("del-key").value;

      if (!key) {
        document.getElementById("status").innerHTML = "Error: Key is required for deletion";
        return;
      }

      try {
        api.poke({
          app: "api-demo",
          mark: "api-action",
          json: { del: { key: key } },
          onSuccess: successDelete,
          onError: pokeError,
        });
      } catch (err) {
        document.getElementById("status").innerHTML = "Poke error: " + err.message;
      }
    }

    function pokeSuccess() {
      document.getElementById("put-key").value = "";
      document.getElementById("put-value").value = "";
      document.getElementById("del-key").value = "";
      document.getElementById("status").innerHTML = "Value stored successfully!";
    }

    function successDelete() {
      document.getElementById("put-key").value = "";
      document.getElementById("put-value").value = "";
      document.getElementById("del-key").value = "";
      document.getElementById("status").innerHTML = "Value deleted successfully!";
    }

    function pokeError() {
      document.getElementById("status").innerHTML = "Poke failed, see dojo";
    }
  </script>
</html>

Scry

To scry agents on the ship, Urbit() includes a scry function. The scry function takes two arguments in a object:

Argument
Type
Description
Example

app

string

The agent to scry.

"api-demo"

path

string

The path to scry, sans the care.

"/store"

The scry function returns a promise that, if successful, contains the requested data as JSON. If the scry failed, for example due to a non-existent scry endpoint, connection problem, or mark conversion failure, the promise will fail.

scry() example

scry-test.html
<html>
  <head>
    <script src="https://unpkg.com/@urbit/http-api"></script>
  </head>
  <body>
    <h1>HTTP API - Scry Endpoints</h1>
    <div>
      <h2>Fetch entire store</h2>
      <div>
        <button
          id="store-scry-btn"
          type="button"
          onClick="scryStore()"
        >
          Scry /store
        </button>
      </div>
      <br />
      <div id="store-status">No data requested yet</div>
      <pre id="store-data">Results will appear here</pre>
    </div>
    <br />
    <div>
      <h2>Fetch a specific key</h2>
      <input
        id="key-input"
        type="text"
        placeholder="Enter key to fetch"
      />
      <div>
      <br />
        <button
          id="key-scry-btn"
          type="button"
          onClick="scryKey()"
        >
          Scry key
        </button>
      </div>
      <br />
      <div id="key-status">No data requested yet</div>
      <pre id="key-data">Results will appear here</pre>
    </div>
  </body>
  <script>
    const api = new UrbitHttpApi.Urbit("");
    api.ship = "zod";
    api.url = "http://localhost:8080";
    api.code = "lidlut-tabwed-pillex-ridrup";

    // Scry the entire store
    async function scryStore() {
      const statusEl = document.getElementById("store-status");
      const dataEl = document.getElementById("store-data");
      
      statusEl.innerText = "Fetching data...";
      
      try {
        const result = await api.scry({
          app: "api-demo",
          path: "/store"
        });
        
        statusEl.innerText = "Data fetched successfully";
        dataEl.innerText = JSON.stringify(result, null, 2);
      } catch (err) {
        statusEl.innerText = "Error fetching data";
        dataEl.innerText = "Error: " + (err.message || "Unknown error");
      }
    }

    // Scry a specific key
    async function scryKey() {
      const key = document.getElementById("key-input").value;
      const statusEl = document.getElementById("key-status");
      const dataEl = document.getElementById("key-data");
      
      if (!key) {
        statusEl.innerText = "Error: Please enter a key";
        return;
      }
      
      statusEl.innerText = "Fetching data...";
      
      try {
        const result = await api.scry({
          app: "api-demo",
          path: `/store/${key}`
        });
        
        statusEl.innerText = "Data fetched successfully";
        if (result === null) {
          dataEl.innerText = "Key not found";
        } else {
          dataEl.innerText = JSON.stringify(result, null, 2);
        }
      } catch (err) {
        statusEl.innerText = "Error fetching data";
        dataEl.innerText = "Error: " + (err.message || "Unknown error");
      }
    }
  </script>
</html>

Subscribe and unsubscribe

For subscribing to a particular path in an agent, Urbit() includes a subscribe function. The subscribe function takes six arguments in a object:

Argument
Type
Description
Example

app

string

The Gall agent to which you'll subscribe.

"api-demo"

path

string

The subscription path.

"/updates"

ship

string

(Optional.) The Urbit ID (@p) of the ship without the ~. This may be ommitted if it's already been set for the whole Urbit() object.

"zod"

err

A function.

(Optional.) This is called if the subscription request fails.

someFunction()

event

A function.

(Optional.) This is the function to handle each update you receive for this subscription. The function's argument is the update's JSON data.

anotherFunction()

quit

A function.

(Optional.) This is called if you are kicked from the subscription.

yetAnotherFunction()

The subscribe function returns a subscription ID, which is just a number. This ID can be used to unsubscribe down the line.

If the subscription request is successful, you'll continue to receive updates until you either unsubscribe or are kicked by the agent. You may subscribe to multiple different agents and subscription paths by calling the subscribe function for each one.

If you wish to unsubscribe from a particular subscription, Urbit() includes an unsubscribe function. This function just takes a single argument: the subscription ID number of an existing subscription. Once unsubscribed, you'll stop receiving updates for the specified subscription.

subscribe() example

subscribe-test.html
<html>
  <head>
    <script src="https://unpkg.com/@urbit/http-api"></script>
  </head>
  <body>
    <h1>HTTP API - Subscriptions</h1>
    <div>
      <h2>Subscribe to the /updates wire</h2>
      <div>
        <button
          id="store-sub-btn"
          type="button"
          onClick="toggleStoreSubscription()"
        >
          Subscribe to /updates
        </button>
      </div>
      <br />
      <div id="store-status">Not subscribed</div>
      <pre id="store-data">No data yet</pre>
    </div>
    <br />
    <div>
      <h2>Subscribe to a wire for a specific key</h2>
      <input
        id="key-input"
        type="text"
        placeholder="Enter key to subscribe to"
      />
      <div>
      <br />
        <button
          id="key-sub-btn"
          type="button"
          onClick="toggleKeySubscription()"
        >
          Subscribe to key
        </button>
      </div>
      <br />
      <div id="key-status">Not subscribed</div>
      <pre id="key-data">No data yet</pre>
    </div>
  </body>
  <script>
    const api = new UrbitHttpApi.Urbit("");
    api.ship = "zod";
    api.url = "http://localhost:8080";
    api.code = "lidlut-tabwed-pillex-ridrup";

    let storeSubId = null;
    let keySubId = null;
    let keyName = null;

    // Subscribe to all store updates
    function toggleStoreSubscription() {
      const statusEl = document.getElementById("store-status");
      const btnEl = document.getElementById("store-sub-btn");
      const dataEl = document.getElementById("store-data");

      if (storeSubId === null) {
        // Subscribe
        statusEl.innerText = "Subscribing...";

        storeSubId = api.subscribe({
          app: "api-demo",
          path: "/updates",
          err: () => {
            storeSubId = null;
            statusEl.innerText = "Subscription failed!";
            btnEl.innerText = "Subscribe to /updates";
          },
          event: (update) => {
            statusEl.innerText = "Receiving updates";
            dataEl.innerText = JSON.stringify(update, null, 2);
          },
          quit: () => {
            storeSubId = null;
            statusEl.innerText = "Kicked from subscription";
            btnEl.innerText = "Subscribe to /updates";
            dataEl.innerText = "No data - subscription ended";
          },
        });

        btnEl.innerText = "Unsubscribe from /updates";
        statusEl.innerText = "Subscribed, awaiting events...";
      } else {
        // Unsubscribe
        api.unsubscribe(storeSubId);
        storeSubId = null;
        btnEl.innerText = "Subscribe to /updates";
        statusEl.innerText = "Not subscribed";
        dataEl.innerText = "No data - unsubscribed";
      }
    }

    // Subscribe to a specific key
    function toggleKeySubscription() {
      const key = document.getElementById("key-input").value;
      const statusEl = document.getElementById("key-status");
      const btnEl = document.getElementById("key-sub-btn");
      const dataEl = document.getElementById("key-data");

      if (!key) {
        statusEl.innerText = "Error: Please enter a key";
        return;
      }

      if (keySubId === null) {
        // Subscribe
        statusEl.innerText = "Subscribing...";

        keySubId = api.subscribe({
          app: "api-demo",
          path: `/updates/${key}`,
          err: () => {
            keySubId = null;
            keyName = null;
            statusEl.innerText = "Subscription failed!";
            btnEl.innerText = "Subscribe to key";
          },
          event: (update) => {
            statusEl.innerText = "Receiving updates";
            dataEl.innerText = JSON.stringify(update, null, 2);
          },
          quit: () => {
            keySubId = null;
            keyName = null;
            statusEl.innerText = "Kicked from subscription";
            btnEl.innerText = "Subscribe to key";
            dataEl.innerText = "No data";
          },
        });

        keyName = key;
        btnEl.innerText = `Unsubscribe from ${key}`;
        statusEl.innerText = "Subscribed, awaiting events...";
      } else {
        // Unsubscribe
        api.unsubscribe(keySubId);
        keySubId = null;
        keyName = null;
        btnEl.innerText = "Subscribe to key";
        statusEl.innerText = "Unsubscribed";
        dataEl.innerText = "No data";
      }
    }
  </script>
</html>

Subscribe once

The subscribeOnce() function is a variation on the ordinary subscribe function. Rather than keeping the subscription going and receiving an arbitrary number of updates, instead it waits to receive a single update and then closes the subscription. This is useful if, for example, you send a poke and just want a response to that one poke.

The subscribeOnce() function also takes an optional timeout argument, which specifies the number of milliseconds to wait for an update before closing the subscription. If omitted, subscribeOnce() will wait indefinitely.

subscribeOnce() takes three arguments (these can't be in an object like most other Urbit() functions):

Argument
Type
Description
Example

app

string

The Gall agent to which you'll subscribe.

"api-demo"

path

string

The subscription path.

"/updates"

timeout

number

(Optional.) The number of milliseconds to wait for an update before closing the subscription. If omitted, it will wait indefinitely.

5000

subscribeOnce() returns a Promise. If successful, the Promise produces the JSON data of the update it received. If it failed due to either timing out or getting kicked from the subscription, it will return an error message of either "timeout" or "quit".

subscribeOnce() example

auth-test.html
<html>
  <head>
    <script src="https://unpkg.com/@urbit/http-api"></script>
  </head>
  <body>
    <h1>HTTP API - Subscribe Once</h1>
    <div>
      <h2>Subscribe once to the /updates wire</h2>
      <div>
        <button
          id="store-sub-btn"
          type="button"
          onClick="subscribeOnceToStore()"
        >
          Subscribe to /updates and unsubscribe
        </button>
      </div>
      <br />
      <div id="store-status">No data requested yet</div>
      <pre id="store-data">Results will appear here</pre>
    </div>
  </body>
  <script>
    const api = new UrbitHttpApi.Urbit("");
    api.ship = "zod";
    api.url = "http://localhost:8080";
    api.code = "lidlut-tabwed-pillex-ridrup";

    // Subscribe once to all store updates
    async function subscribeOnceToStore() {
      const statusEl = document.getElementById("store-status");
      const dataEl = document.getElementById("store-data");

      statusEl.innerText = "Subscribing once...";
      dataEl.innerText = "Waiting for one update...";

      try {
        const result = await api.subscribeOnce("api-demo", "/updates", 5000);
        statusEl.innerText = "Received one update and closed subscription";
        dataEl.innerText = JSON.stringify(result, null, 2);
      } catch (err) {
        if (err === "timeout") {
          statusEl.innerText = "Subscription timed out";
          dataEl.innerText = "No update received within the timeout period";
        } else if (err === "quit") {
          statusEl.innerText = "Kicked from subscription";
          dataEl.innerText = "The agent kicked us from the subscription";
        } else {
          statusEl.innerText = "Subscription error";
          dataEl.innerText = "Error: " + (err.message || "Unknown error");
        }
      }
    }
  </script>
</html>

Run a thread

To run a thread, Urbit() includes a thread function. The thread function takes five arguments in an object:

Argument
Type
Description
Example

inputMark

string

The mark to convert your JSON data to before giving it to the thread as its argument.

"ship"

outputMark

string

The result of the thread should be converted to this mark before being converted to JSON and returned to you.

"tang"

threadName

string

The name of the thread to run.

"hi"

body

any JSON

The data to give to the thread as its argument.

"~bud"

desk

string

(Optional.) The desk in which the thread resides. This may be ommitted if previously set for the whole Urbit() object.

"base"

The thread function will produce a promise that, if successful, contains the JSON result of the thread. If the thread failed, a connection error occurred, or mark conversion failed, the promise will fail.

thread() example

thread-test.html
<html>
  <head>
    <script src="https://unpkg.com/@urbit/http-api"></script>
  </head>
  <body>
    <h1>HTTP API - Threads</h1>
    <div>
      <h2>Run the "hi" thread in %base</h2>
      <div>
        <input id="name-input" type="text" placeholder="~bud" />
        <br />
        <br />
        <button id="run-thread-btn" type="button" onClick="runHiThread()">
          Say hi
        </button>
      </div>
      <br />
      <div id="thread-status">No thread run yet</div>
      <pre id="thread-result">Results will appear here</pre>
    </div>
  </body>
  <script>
    const api = new UrbitHttpApi.Urbit("");
    api.ship = "zod";
    api.desk = "base";
    api.url = "http://localhost:8080";
    api.code = "lidlut-tabwed-pillex-ridrup";

    async function runHiThread() {
      const name = document.getElementById("name-input").value;
      const statusEl = document.getElementById("thread-status");
      const resultEl = document.getElementById("thread-result");

      if (!name) {
        statusEl.innerText = "Input cannot be empty";
        return;
      }

      statusEl.innerText = "Running thread...";
      resultEl.innerText = "Waiting for thread to complete...";

      try {
        const result = await api.thread({
          inputMark: "ship",
          outputMark: "tang",
          threadName: "hi",
          body: name,
        });

        if (result) {
          statusEl.innerText = "Thread completed!";
          resultEl.innerText = "Check the recipient's dojo";
        }
      } catch (err) {
        statusEl.innerText = "Thread error";
        resultEl.innerText = "Error: " + (err.message || "Unknown error");
      }
    }
  </script>
</html>

Delete a channel

Rather than just closing individual subscriptions, the entire channel can be closed with the delete() function in Urbit(). When a channel is closed, all subscriptions are cancelled and all pending updates are discarded. The function takes no arguments, and can be called like api.delete().

Reset

An existing Urbit() object can be reset with its reset() function. This function takes no arguments, and can be called like api.reset(). When a channel is reset, all subscriptions are cancelled and all pending updates are discarded. Additionally, all outstanding outbound pokes to the agent will be discarded, and a fresh channel ID will be generated.

Further reading

  • @urbit/http-api on Github - The source code for the JS HTTP API package.

  • Eyre External API Reference - Lower-level documentation of Eyre's external API.

  • Eyre Guide - Lower-level examples of using Eyre's external API with curl.

PreviousJS LibrariesNextDocs App

Last updated 1 day ago