Signals Docs
Getting started

Requests and errors

Every Signals API endpoint follows the same conventions for requests, retries, concurrent edits, background work and errors. This page explains each one. The Twilio-compatible Calls API follows Twilio's conventions instead; see Moving from Twilio.

Base URL

Send every Signals API request to:

Code
https://api.signals.pressone.africa/v1

Paths in the API reference are relative to this base URL. For example, GET /trunks means GET https://api.signals.pressone.africa/v1/trunks. Every request needs an API key; see Authentication.

Request and response bodies

Requests and responses use JSON. Send Content-Type: application/json with every request that has a body.

Request bodies are strict. A body that is not valid JSON, misses a required field or contains a field the endpoint does not define is refused, usually with 422. Fix the body before you retry.

Headers you may send:

HeaderWhenPurpose
AuthorizationEvery requestBearer followed by your API key.
Content-TypeRequests with a bodyapplication/json.
Idempotency-KeyCreates and actions, as the reference marksMakes a retry safe. See Idempotency.
If-MatchChanges to versioned resources, as the reference marksRefuses the change if the resource changed since you read it. See ETag and If-Match.
X-Project-IDOptionalThe project the request acts on. See Choosing a project.
X-Request-IDOptionalYour own ID for the request. See Request IDs.

Request IDs

Every response carries an X-Request-ID header. If you send X-Request-ID yourself, with up to 128 printable ASCII characters and no spaces, Signals uses your value. Otherwise it generates one, such as req_0192b7e93c4f7a2e9d1b6c8f0a5e3d27.

Log the request ID with every failure. Error bodies repeat it in request_id, and PressOne support uses it to find the request.

Timestamps

Send timestamps in RFC 3339 format with an explicit offset. Both of these name the same instant:

Code
2026-10-01T10:30:00+01:00 2026-10-01T09:30:00Z

A timestamp without an offset, such as 2026-10-01T10:30:00, is refused, because Signals cannot know which time zone you meant.

Signals returns every timestamp in UTC with the Z suffix, such as 2026-10-01T09:30:00Z. Some timestamps carry fractional seconds. Convert to Africa/Lagos time, or any other zone, when you display them.

Money

Signals never sends money as a decimal number. Money fields end in _minor and hold a whole number of minor units: kobo for naira, where 100 kobo is ₦1. A currency field alongside names the currency.

JSONCode
{ "daily_spend_cap_minor": 500000, "currency": "NGN" }

This cap is 500,000 kobo, which is ₦5,000.00.

Every money field is a JSON integer, such as 500000, with a leading - for a negative amount. Requests send money the same way; a string such as "500000" is refused with 422. An amount can be larger than 2^53, which a JavaScript JSON.parse or any floating-point type rounds, so read money into a 64-bit integer or an arbitrary-precision type and no kobo is lost.

Durations follow the same idea, with the unit at the end of the name and always a JSON integer. Fields ending in _ns are whole nanoseconds, which can also pass 2^53: "observed_duration_ns": 18000000000 is 18 seconds. Durations ending in _ms are whole milliseconds and those ending in _seconds whole seconds. Quality measurements such as mos and jitter_ms are JSON numbers, and percentages ending in _pct are decimal strings such as "62.4".

IDs

Every Signals resource ID starts with a prefix that names its type, followed by a UUID:

Code
trk_0192b7e1-4c2a-7f58-b913-2d6e0a9c5f41
PrefixResourcePrefixResource
trk_Trunkstr_Subtrunk
pol_Outbound policypat_Policy attachment
nbk_Number blocknun_Individual number
quo_Number quotesub_Subaccount
cal_Callrec_Recording
vap_Voice applicationva_Voice account
whe_Webhook endpointwhd_Webhook delivery
rlp_Webhook replayevt_Event
alr_Alertalp_Alert policy
stm_Statementinv_Invoice
use_Usage recordkey_API key
tst_Trunk testcrq_Credit request
aud_Audit event

A path that expects one type refuses an ID with another prefix. Project IDs are plain UUIDs without a prefix. Programmable Voice keeps Twilio-style identifiers: AC for the voice account, CA for calls and RE for recordings.

Treat IDs as opaque strings. Compare them exactly and do not parse them.

Idempotency

A network failure can leave you unsure whether a request reached Signals. Idempotency lets you send the request again without doing the work twice.

Send an Idempotency-Key header with every create and action request. The API reference marks the requests that require it, including creating trunks, policies, subaccounts and webhook endpoints, suspending and resuming trunks, and buying and releasing numbers. Use a fresh random value, such as a UUID, for each new piece of work:

TerminalCode
curl -X POST https://api.signals.pressone.africa/v1/subaccounts \ -H "Authorization: Bearer $SIGNALS_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 6f1d2c9a-8b3e-4f70-a5d1-2e9c7b0f4a63" \ -d '{ "name": "branch-office", "configuration": { "max_concurrent": 10, "max_cps": 2, "max_call_duration_seconds": 3600, "daily_spend_cap_minor": 500000, "max_destination_rate_minor": null, "currency": "NGN" } }'

The key must be one non-blank value of at most 128 characters. A missing, blank, repeated or longer key fails with 400 and code invalid_idempotency_key.

Signals remembers each key for 24 hours, scoped to your organization, the credential that sent it, the method and the path. Within that window:

You sendSignals does
The same key with the same bodyReturns the result of the first request and does not repeat the work. A replayed create can answer 200 where the first answered 201.
The same key with a different bodyRefuses with 409 and code idempotency_conflict or conflict_idempotency, and changes nothing.
A new keyTreats the request as new work.

Reuse a key only to retry the exact same request. Generate a new key when you mean to do something new.

A replay never returns a one-time secret again. When you retry a request that returns a secret once, such as creating a webhook endpoint, the replay carries "secret": null and "secret_unavailable": true. If the first response was lost, rotate the secret to get a new one.

ETag and If-Match

Two people, or two programs, can edit the same resource at once. ETags stop one from silently overwriting the other.

A versioned resource, such as a trunk, an outbound policy, a voice application or a webhook endpoint, returns an ETag header with every read and write. The ETag is the resource's revision, a quoted integer that goes up with every saved change:

Code
HTTP/1.1 200 OK ETag: "3"

To change the resource, send that value back in If-Match:

TerminalCode
curl -X PATCH "https://api.signals.pressone.africa/v1/voice/applications/vap_0192b6f4-7e2a-7c18-9d3b-4a5e6f0c1b28" \ -H "Authorization: Bearer $SIGNALS_API_KEY" \ -H "Content-Type: application/json" \ -H 'If-Match: "3"' \ -d '{ "name": "order-tracking-line" }'

Copy the ETag exactly, quotes included. The revision is only a concurrency token: it tells Signals which version you read, not whether a change has reached the network.

What happensResponse
The resource has not changed since your readThe change applies, and the response carries the new ETag.
You leave out If-Match where the reference requires it428 Precondition Required with code precondition_required.
The If-Match value is malformed, for example not a quoted integer400 with code invalid_precondition.
Someone changed the resource since your read412 Precondition Failed. Codes include revision_conflict and precondition_failed, plus resource-specific codes such as outbound_policy_revision_conflict.

After a 412, read the resource again, reapply your change to the new version, and send it with the new ETag.

Saved changes are live

There is no draft, preview or publish step. A create or edit to a trunk, an outbound policy, a number route, a subtrunk, a subaccount or a voice application answers 200 OK, or 201 Created for a create, with the resource as stored. Changes take effect within a minute. If a change cannot be applied, PressOne staff are alerted and the previous configuration keeps serving.

A trunk change that would leave the trunk unable to run is refused with 422 and a detail that starts with the trunk would not apply:. Nothing is saved.

Conflicts

A 409 Conflict means the request is valid but the current state of your account does not allow it. Examples:

CodeMeaning
trunk_name_conflictAnother trunk already has that name.
number_block_in_useThe number block is still routed or delegated. The problem lists what uses it.
number_quote_expiredThe price quote expired. Request a new quote.
conflict_target_versionA route target cannot take the route; reason says why.

Read the detail and the current state of the resource, resolve the conflict, then retry with a new Idempotency-Key.

Background work

Two requests start work that takes longer than one request: running a trunk test and deleting a recording. For these, Signals answers 202 Accepted, and the Location header points to where the result appears.

For a trunk test, read GET /v1/trunks/{trunk_id}/tests/{test_id} until the run's result is no longer running:

TerminalCode
curl https://api.signals.pressone.africa/v1/trunks/$TRUNK_ID/tests/tst_0192b7ea-3f1c-7b2d-8e4a-5c6d7e8f9a01 \ -H "Authorization: Bearer $SIGNALS_API_KEY"

Read every second or two while you wait. A passed or failed result is final.

Pagination

Every list answers a JSON object, never a bare array. The rows are under data. Fields that describe the whole list, such as as_of, totals or currency, sit beside data.

Most lists return every row in one response. Collections that grow without bound are paged instead:

EndpointPage sizeHow to page
GET /v1/callslimit, 1 to 100, default 50cursor
GET /v1/auditlimit, 1 to 100, default 50cursor
GET /v1/webhooks/deliverieslimit, 1 to 100, default 100Narrow with from and to

A cursor page adds next_cursor beside data: the page after this one, in the list's order. Where the list can also step back, it adds previous_cursor: the page before this one. Pass either value as cursor to move. A null cursor means there is no page in that direction.

TerminalCode
curl "https://api.signals.pressone.africa/v1/calls?window=last_24h&limit=2&cursor=eyJpZCI6ImNhbF8wMTkyIn0" \ -H "Authorization: Bearer $SIGNALS_API_KEY"
JSONCode
{ "as_of": "2026-10-01T09:20:05Z", "data": [ { "id": "cal_0192b7f2-6d81-7a3b-9c4e-1f0a8d5e2b63", "outcome": "answered" }, { "id": "cal_0192b7f0-2a4c-7e91-8b5d-3c6f0e1a9d42", "outcome": "unanswered" } ], "total": 37, "answered": 31, "failed": 6, "next_cursor": "eyJpZCI6ImNhbF8wMTkyYjdmMC0yYTRjIn0", "previous_cursor": "eyJpZCI6ImNhbF8wMTkyYjdmMi02ZDgxIn0" }

Treat cursors as opaque and keep the same filters while you follow them. An unknown filter value or an out-of-range limit fails with 422.

Endpoints that take after_sequence, such as call events and trunk test checks, return items in order. Pass the highest sequence number you have received to get only newer items.

Errors

Every Signals API error uses the problem format from RFC 9457, with the content type application/problem+json:

Code
HTTP/1.1 412 Precondition Failed Content-Type: application/problem+json X-Request-ID: req_0192b7ec5d2a7f3e8c1b9d4a6e0f2c51 { "type": "https://signals.pressone.africa/problems/revision_conflict", "title": "Precondition Failed", "status": 412, "detail": "The trunk changed. Read it and retry against its current ETag.", "code": "revision_conflict", "request_id": "req_0192b7ec5d2a7f3e8c1b9d4a6e0f2c51" }
FieldMeaning
typeA URL that names the problem. It always ends in the code.
titleThe standard name of the HTTP status.
statusThe HTTP status code, repeated in the body.
detailA sentence for a person, explaining this occurrence. It can change; do not match on it.
codeA stable identifier for the problem. Match on this in your code.
request_idThe request ID, as in the X-Request-ID header.

Some problems add fields:

FieldPresent on
available_projects400 project_context_required: the projects you can name in X-Project-ID.
reason409 conflict_target_version: why a route target refused the route.
caused_bySome 503 problems: which dependency is unavailable, such as identity_provider.
expensive_filter422 validation_query_cost: the filter that made the query too expensive.

Problems never include internal hostnames, stack traces or secrets.

Error codes by status

Handle errors by status first, then by code where you need to tell cases apart. Codes follow a pattern: <resource>_forbidden for 403, <resource>_not_found for 404, invalid_<resource> for 422, <resource>_unavailable for 503.

StatusMeaningCommon codesWhat to do
400The request is malformed.invalid_idempotency_key, invalid_precondition, project_context_required, project_invalid, invalid_queryFix the request.
401The credential is missing or not valid.unauthenticatedCheck the API key. Do not retry with the same key.
403The key is valid but may not do this.forbidden_capability, trunk_diagnostics_forbidden, webhook_write_forbiddenUse a key with the needed role and scope.
404The resource does not exist or belongs to another organization.trunk_not_found, project_not_found, not_foundCheck the ID and the project.
409The current state does not allow the request.idempotency_conflict, trunk_name_conflict, number_block_in_useSee Conflicts.
412The resource changed since you read it.revision_conflict, precondition_failedRead again and reapply.
422The request is well formed but its values are not valid.invalid_trunk, invalid_outbound_policy, invalid_call_filter, validation_subaccount, validation_query_costRead detail, fix the values.
428If-Match is required.precondition_requiredRead the resource and send its ETag.
429A usage cap was reached.egress_cap_reachedWait and retry later.
500Signals found inconsistent stored state.invalid_<resource>_stateContact support with the request_id.
502, 504A platform dependency failed or timed out.upstream_unavailable, upstream_timeout, upstream_invalidRetry with backoff.
503A Signals service is temporarily unavailable.trunks_unavailable, calls_unavailable, identity_unavailable, project_context_unavailableRetry with backoff.

A 404 covers both "does not exist" and "belongs to someone else", so an ID from another organization reveals nothing about that organization.

Retrying safely

  • Retry 502, 503 and 504 with exponential backoff, starting at about one second.
  • Retry network failures and timeouts the same way.
  • When you retry a request that took an Idempotency-Key, send the same key and the same body, so the work happens once.
  • Do not retry 400, 401, 403, 404, 409, 412, 422 or 428 unchanged. Fix the cause first.
Last modified on